feat(gameplay): add crazy dave casino and bank, and zombie mutation lab
This commit is contained in:
+714
-82
@@ -47,6 +47,12 @@ window.GameEngine = class GameEngine {
|
||||
this.feedback = window.PvZFeedback;
|
||||
this.feedback.init();
|
||||
}
|
||||
if (window.CasinoSystem) {
|
||||
this.casino = new window.CasinoSystem(this.economy);
|
||||
}
|
||||
if (window.ZombieLabSystem) {
|
||||
this.zombieLab = new window.ZombieLabSystem(this.economy, this.workshop);
|
||||
}
|
||||
this.wave = new window.WaveSystem();
|
||||
this.combat = new window.CombatSystem();
|
||||
if (window.ProjectilePool) {
|
||||
@@ -97,6 +103,14 @@ window.GameEngine = class GameEngine {
|
||||
}
|
||||
window._pvzEngine = this; // allow auth.js to trigger profile reload
|
||||
|
||||
// Dynamic profile sync event listener
|
||||
window.addEventListener('pvz:profile-updated', () => {
|
||||
this.loadUserData();
|
||||
this.updateProfileUI();
|
||||
this.updateRecordDisplay();
|
||||
this.updateShopUI();
|
||||
});
|
||||
|
||||
// Bind events
|
||||
this.bindEvents();
|
||||
this.bindUI();
|
||||
@@ -131,6 +145,8 @@ window.GameEngine = class GameEngine {
|
||||
if (this.achievements) this.achievements.loadFromAuth();
|
||||
if (this.tree) this.tree.loadFromAuth();
|
||||
if (this.leaderboards) this.leaderboards.loadFromAuth();
|
||||
if (this.casino) this.casino.loadFromAuth();
|
||||
if (this.zombieLab) this.zombieLab.loadFromAuth();
|
||||
} else {
|
||||
this.currentLevel = parseInt(localStorage.getItem('pvz_level') || '1', 10);
|
||||
this.maxUnlockedLevel = parseInt(localStorage.getItem('pvz_max_level') || this.currentLevel.toString(), 10);
|
||||
@@ -162,9 +178,12 @@ window.GameEngine = class GameEngine {
|
||||
|
||||
saveUserData() {
|
||||
if (window.PvZAuth) {
|
||||
if (this.workshop) this.workshop.saveToAuth();
|
||||
if (this.achievements) this.achievements.saveToAuth();
|
||||
if (this.tree) this.tree.saveToAuth();
|
||||
if (this.leaderboards) this.leaderboards.saveToAuth();
|
||||
if (this.casino) this.casino.saveToAuth();
|
||||
if (this.zombieLab) this.zombieLab.saveToAuth();
|
||||
window.PvZAuth.saveUserData({
|
||||
coins: this.economy ? this.economy.totalCoins : undefined,
|
||||
level: this.currentLevel,
|
||||
@@ -319,11 +338,107 @@ window.GameEngine = class GameEngine {
|
||||
if (statPlants) statPlants.textContent = (stats.plantsPlanted || 0).toLocaleString('ru-RU');
|
||||
if (statLevelsWon) statLevelsWon.textContent = (stats.levelsCompleted || 0).toLocaleString('ru-RU');
|
||||
if (statCoinsEarned) statCoinsEarned.textContent = (stats.coinsEarned || 0).toLocaleString('ru-RU');
|
||||
|
||||
this.renderUserSessionsUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async renderUserSessionsUI() {
|
||||
const listEl = document.getElementById('auth-sessions-list');
|
||||
if (!listEl) return;
|
||||
if (!window.PvZAuth || window.PvZAuth.isGuest()) {
|
||||
listEl.innerHTML = '<div style="font-size: 12px; color: #b0bec5; text-align: center; padding: 8px;">Войдите в аккаунт для просмотра сессий</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
const sessions = await window.PvZAuth.fetchSessions();
|
||||
if (!sessions || sessions.length === 0) {
|
||||
listEl.innerHTML = '<div style="font-size: 12px; color: #b0bec5; text-align: center; padding: 8px;">Нет активных сессий</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
listEl.innerHTML = '';
|
||||
sessions.forEach(s => {
|
||||
const card = document.createElement('div');
|
||||
card.className = `session-card ${s.isCurrent ? 'is-current' : ''}`;
|
||||
|
||||
// Choose icon based on device name
|
||||
let icon = '🖥️';
|
||||
const dName = (s.deviceName || '').toLowerCase();
|
||||
if (dName.includes('iphone') || dName.includes('android') || dName.includes('phone') || dName.includes('mobile')) {
|
||||
icon = '📱';
|
||||
} else if (dName.includes('ipad') || dName.includes('tablet')) {
|
||||
icon = '📟';
|
||||
} else if (dName.includes('mac') || dName.includes('apple')) {
|
||||
icon = '🍎';
|
||||
} else if (dName.includes('windows') || dName.includes('linux') || dName.includes('chrome') || dName.includes('pc')) {
|
||||
icon = '💻';
|
||||
}
|
||||
|
||||
const iconDiv = document.createElement('div');
|
||||
iconDiv.className = 'session-icon';
|
||||
iconDiv.textContent = icon;
|
||||
|
||||
const detailsDiv = document.createElement('div');
|
||||
detailsDiv.className = 'session-details';
|
||||
|
||||
const devNameDiv = document.createElement('div');
|
||||
devNameDiv.className = 'session-dev-name';
|
||||
devNameDiv.textContent = s.deviceName || 'Браузер / Компьютер';
|
||||
|
||||
const metaDiv = document.createElement('div');
|
||||
metaDiv.className = 'session-meta';
|
||||
|
||||
const ipSpan = document.createElement('span');
|
||||
ipSpan.textContent = `IP: ${s.ipAddress || '127.0.0.1'}`;
|
||||
|
||||
const activeSpan = document.createElement('span');
|
||||
activeSpan.textContent = `Активность: ${s.lastActive || s.createdAt || 'Только что'}`;
|
||||
|
||||
metaDiv.appendChild(ipSpan);
|
||||
metaDiv.appendChild(activeSpan);
|
||||
|
||||
detailsDiv.appendChild(devNameDiv);
|
||||
detailsDiv.appendChild(metaDiv);
|
||||
|
||||
card.appendChild(iconDiv);
|
||||
card.appendChild(detailsDiv);
|
||||
|
||||
if (s.isCurrent) {
|
||||
const badge = document.createElement('span');
|
||||
badge.className = 'session-current-badge';
|
||||
badge.textContent = '🟢 Текущее устройство';
|
||||
card.appendChild(badge);
|
||||
} else {
|
||||
const btnTerm = document.createElement('button');
|
||||
btnTerm.type = 'button';
|
||||
btnTerm.className = 'btn btn-danger btn-terminate-session';
|
||||
btnTerm.textContent = 'Завершить';
|
||||
btnTerm.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
btnTerm.disabled = true;
|
||||
btnTerm.textContent = '⏳';
|
||||
const res = await window.PvZAuth.terminateSession(s.token);
|
||||
if (res && res.success) {
|
||||
if (window.PvZAuth && window.PvZAuth.showNotification) {
|
||||
window.PvZAuth.showNotification('Сессия устройства завершена', s.deviceName, '🔒');
|
||||
}
|
||||
} else {
|
||||
if (window.PvZAuth && window.PvZAuth.showNotification) {
|
||||
window.PvZAuth.showNotification('Ошибка завершения сессии', res?.message || 'Сбой', '❌');
|
||||
}
|
||||
}
|
||||
await this.renderUserSessionsUI();
|
||||
});
|
||||
card.appendChild(btnTerm);
|
||||
}
|
||||
|
||||
listEl.appendChild(card);
|
||||
});
|
||||
}
|
||||
|
||||
openMainMenu() {
|
||||
this.isRunning = false;
|
||||
if (window.PvZAudio) window.PvZAudio.stopMusic();
|
||||
@@ -333,7 +448,7 @@ window.GameEngine = class GameEngine {
|
||||
// Close any game-phase modals that might have been left open
|
||||
['game-over-modal', 'victory-modal', 'draft-modal', 'zengarden-modal',
|
||||
'workshop-modal', 'leaderboard-modal', 'achievement-modal', 'almanac-modal',
|
||||
'bugreport-modal', 'campaign-modal', 'tree-modal'].forEach(id => {
|
||||
'bugreport-modal', 'campaign-modal', 'tree-modal', 'casino-modal'].forEach(id => {
|
||||
const el = document.getElementById(id);
|
||||
if (el) el.classList.add('hidden');
|
||||
});
|
||||
@@ -417,75 +532,133 @@ window.GameEngine = class GameEngine {
|
||||
this.rakeCol = 7;
|
||||
}
|
||||
|
||||
if (this.wave && this.wave.startingSun !== null && this.wave.startingSun !== undefined && this.wave.startingSun >= 0) {
|
||||
this.economy.sun = this.wave.startingSun;
|
||||
}
|
||||
|
||||
this.isRunning = true;
|
||||
this.lastTime = performance.now();
|
||||
const musicTheme = (mode === 'boss' || mode === 'bossrush') ? 'boss' : this.wave.biome;
|
||||
if (window.PvZAudio) window.PvZAudio.startMusic(musicTheme);
|
||||
}
|
||||
|
||||
setupIZombieLawn() {
|
||||
setupIZombieLawn(stage) {
|
||||
this.plants = [];
|
||||
this.izombieSetupDone = true;
|
||||
const currentStage = stage || (this.zombieLab ? (this.zombieLab.selectedStage || this.zombieLab.stage || 1) : 1);
|
||||
|
||||
// Col 0: Sunflowers in rows 0..4 (5 trophy targets)
|
||||
for (let r = 0; r < 5; r++) {
|
||||
const b0 = this.grid.getCellBounds(0, r);
|
||||
const sf = new window.Plant('sunflower', 0, r, b0.x, b0.y, b0.w, b0.h);
|
||||
this.grid.setPlant(0, r, sf);
|
||||
this.plants.push(sf);
|
||||
const addPlant = (type, col, row, options = {}) => {
|
||||
const b = this.grid.getCellBounds(col, row);
|
||||
const p = new window.Plant(type, col, row, b.x, b.y, b.w, b.h);
|
||||
if (options.isArmed) {
|
||||
p.isArmed = true;
|
||||
p.armTimer = 15;
|
||||
}
|
||||
this.grid.setPlant(col, row, p);
|
||||
this.plants.push(p);
|
||||
return p;
|
||||
};
|
||||
|
||||
if (currentStage === 5) {
|
||||
// Stage 5: RAID BOSS - Mega Sunflower (5000 HP) with Royal Defense!
|
||||
for (let r = 0; r < 5; r++) {
|
||||
if (r === 2) {
|
||||
addPlant('mega_sunflower', 0, r); // Center lane Raid Boss!
|
||||
} else {
|
||||
addPlant('sunflower', 0, r);
|
||||
}
|
||||
}
|
||||
addPlant('snowpea', 2, 0);
|
||||
addPlant('tallnut', 4, 0);
|
||||
|
||||
addPlant('repeater', 1, 1);
|
||||
addPlant('repeater', 3, 1);
|
||||
addPlant('tallnut', 4, 1);
|
||||
|
||||
// Lane 2: The Boss Lane
|
||||
addPlant('repeater', 1, 2);
|
||||
addPlant('snowpea', 2, 2);
|
||||
addPlant('tallnut', 4, 2);
|
||||
|
||||
addPlant('repeater', 1, 3);
|
||||
addPlant('repeater', 3, 3);
|
||||
addPlant('tallnut', 4, 3);
|
||||
|
||||
addPlant('snowpea', 2, 4);
|
||||
addPlant('tallnut', 4, 4);
|
||||
|
||||
} else if (currentStage === 4) {
|
||||
// Stage 4: The Great Wall (Tallnuts & repeaters)
|
||||
for (let r = 0; r < 5; r++) {
|
||||
addPlant('sunflower', 0, r);
|
||||
addPlant('repeater', 1, r);
|
||||
addPlant('tallnut', 3, r);
|
||||
}
|
||||
addPlant('potatomine', 4, 1, { isArmed: true });
|
||||
addPlant('potatomine', 4, 3, { isArmed: true });
|
||||
|
||||
} else if (currentStage === 3) {
|
||||
// Stage 3: Frost & Thorns (Snowpeas & armed mines)
|
||||
for (let r = 0; r < 5; r++) {
|
||||
addPlant('sunflower', 0, r);
|
||||
}
|
||||
addPlant('snowpea', 2, 0);
|
||||
addPlant('wallnut', 4, 0);
|
||||
|
||||
addPlant('repeater', 1, 1);
|
||||
addPlant('snowpea', 3, 1);
|
||||
addPlant('potatomine', 4, 1, { isArmed: true });
|
||||
|
||||
addPlant('snowpea', 2, 2);
|
||||
addPlant('tallnut', 4, 2);
|
||||
|
||||
addPlant('repeater', 1, 3);
|
||||
addPlant('potatomine', 3, 3, { isArmed: true });
|
||||
addPlant('wallnut', 4, 3);
|
||||
|
||||
addPlant('snowpea', 2, 4);
|
||||
addPlant('chomper', 4, 4);
|
||||
|
||||
} else if (currentStage === 2) {
|
||||
// Stage 2: Night Guard (Mushrooms & Peashooters)
|
||||
for (let r = 0; r < 5; r++) {
|
||||
addPlant('sunflower', 0, r);
|
||||
}
|
||||
addPlant('peashooter', 1, 0);
|
||||
addPlant('fumeshroom', 3, 0);
|
||||
|
||||
addPlant('snowpea', 2, 1);
|
||||
addPlant('wallnut', 4, 1);
|
||||
|
||||
addPlant('repeater', 1, 2);
|
||||
addPlant('repeater', 3, 2);
|
||||
addPlant('wallnut', 5, 2);
|
||||
|
||||
addPlant('fumeshroom', 2, 3);
|
||||
addPlant('chomper', 4, 3);
|
||||
|
||||
addPlant('peashooter', 1, 4);
|
||||
addPlant('potatomine', 3, 4, { isArmed: true });
|
||||
|
||||
} else {
|
||||
// Stage 1: Classic Lawn
|
||||
for (let r = 0; r < 5; r++) {
|
||||
addPlant('sunflower', 0, r);
|
||||
}
|
||||
addPlant('peashooter', 2, 0);
|
||||
|
||||
addPlant('repeater', 2, 1);
|
||||
addPlant('wallnut', 4, 1);
|
||||
|
||||
addPlant('snowpea', 1, 2);
|
||||
addPlant('peashooter', 3, 2);
|
||||
|
||||
addPlant('peashooter', 2, 3);
|
||||
addPlant('wallnut', 4, 3);
|
||||
|
||||
addPlant('repeater', 1, 4);
|
||||
addPlant('potatomine', 3, 4, { isArmed: true });
|
||||
}
|
||||
|
||||
// Row 0 defense: Peashooter at col 2
|
||||
const b02 = this.grid.getCellBounds(2, 0);
|
||||
const p02 = new window.Plant('peashooter', 2, 0, b02.x, b02.y, b02.w, b02.h);
|
||||
this.grid.setPlant(2, 0, p02);
|
||||
this.plants.push(p02);
|
||||
|
||||
// Row 1 defense: Repeater at col 2, Wallnut at col 4
|
||||
const b12 = this.grid.getCellBounds(2, 1);
|
||||
const p12 = new window.Plant('repeater', 2, 1, b12.x, b12.y, b12.w, b12.h);
|
||||
this.grid.setPlant(2, 1, p12);
|
||||
this.plants.push(p12);
|
||||
|
||||
const b14 = this.grid.getCellBounds(4, 1);
|
||||
const p14 = new window.Plant('wallnut', 4, 1, b14.x, b14.y, b14.w, b14.h);
|
||||
this.grid.setPlant(4, 1, p14);
|
||||
this.plants.push(p14);
|
||||
|
||||
// Row 2 defense: Snowpea at col 1, Peashooter at col 3
|
||||
const b21 = this.grid.getCellBounds(1, 2);
|
||||
const p21 = new window.Plant('snowpea', 1, 2, b21.x, b21.y, b21.w, b21.h);
|
||||
this.grid.setPlant(1, 2, p21);
|
||||
this.plants.push(p21);
|
||||
|
||||
const b23 = this.grid.getCellBounds(3, 2);
|
||||
const p23 = new window.Plant('peashooter', 3, 2, b23.x, b23.y, b23.w, b23.h);
|
||||
this.grid.setPlant(3, 2, p23);
|
||||
this.plants.push(p23);
|
||||
|
||||
// Row 3 defense: Peashooter at col 2, Wallnut at col 4
|
||||
const b32 = this.grid.getCellBounds(2, 3);
|
||||
const p32 = new window.Plant('peashooter', 2, 3, b32.x, b32.y, b32.w, b32.h);
|
||||
this.grid.setPlant(2, 3, p32);
|
||||
this.plants.push(p32);
|
||||
|
||||
const b34 = this.grid.getCellBounds(4, 3);
|
||||
const p34 = new window.Plant('wallnut', 4, 3, b34.x, b34.y, b34.w, b34.h);
|
||||
this.grid.setPlant(4, 3, p34);
|
||||
this.plants.push(p34);
|
||||
|
||||
// Row 4 defense: Repeater at col 1, Potato Mine at col 3 (armed)
|
||||
const b41 = this.grid.getCellBounds(1, 4);
|
||||
const p41 = new window.Plant('repeater', 1, 4, b41.x, b41.y, b41.w, b41.h);
|
||||
this.grid.setPlant(1, 4, p41);
|
||||
this.plants.push(p41);
|
||||
|
||||
const b43 = this.grid.getCellBounds(3, 4);
|
||||
const p43 = new window.Plant('potatomine', 3, 4, b43.x, b43.y, b43.w, b43.h);
|
||||
p43.isArmed = true;
|
||||
p43.armTimer = 15;
|
||||
this.grid.setPlant(3, 4, p43);
|
||||
this.plants.push(p43);
|
||||
}
|
||||
|
||||
showDaveDialogue(level, mode, onContinue) {
|
||||
@@ -593,6 +766,15 @@ window.GameEngine = class GameEngine {
|
||||
const zombie = new window.Zombie(row, type);
|
||||
const bounds = this.grid.getCellBounds(0, row);
|
||||
zombie.y = bounds.centerY - 5;
|
||||
if (this.wave) {
|
||||
if (this.wave.hpMultiplier && this.wave.hpMultiplier !== 1.0) {
|
||||
zombie.maxHp = Math.round(zombie.maxHp * this.wave.hpMultiplier);
|
||||
zombie.hp = Math.round(zombie.hp * this.wave.hpMultiplier);
|
||||
}
|
||||
if (this.wave.speedMultiplier && this.wave.speedMultiplier !== 1.0) {
|
||||
zombie.speed *= this.wave.speedMultiplier;
|
||||
}
|
||||
}
|
||||
this.zombies.push(zombie);
|
||||
}
|
||||
|
||||
@@ -661,7 +843,7 @@ window.GameEngine = class GameEngine {
|
||||
this.economy.addCoins(100); // 100 coin bonus for level completion!
|
||||
|
||||
// Check achievement: clean_lawn (all lawnmowers intact)
|
||||
if (this.grid.lawnmowers && this.grid.lawnmowers.length > 0 && this.grid.lawnmowers.every(lm => lm && lm.active)) {
|
||||
if (this.grid.lawnmowers && this.grid.lawnmowers.length > 0 && this.grid.lawnmowers.every(lm => lm && !lm.used && !lm.active)) {
|
||||
if (this.achievements) this.achievements.trigger('clean_lawn');
|
||||
}
|
||||
|
||||
@@ -690,6 +872,27 @@ window.GameEngine = class GameEngine {
|
||||
|
||||
// Save level progression
|
||||
let levelReward = 100;
|
||||
let izombieNextUnlocked = false;
|
||||
let izombieCurrentStage = 1;
|
||||
let izombieRewardCoins = 150;
|
||||
|
||||
if (this.currentMode === 'izombie' && this.zombieLab) {
|
||||
izombieCurrentStage = this.zombieLab.selectedStage || 1;
|
||||
izombieNextUnlocked = this.zombieLab.unlockNextStage(izombieCurrentStage);
|
||||
const stDef = this.zombieLab.stages.find(s => s.id === izombieCurrentStage);
|
||||
if (stDef) {
|
||||
izombieRewardCoins = stDef.reward || 150;
|
||||
if (stDef.crystals && this.workshop) {
|
||||
this.workshop.addMaterial('crystals', stDef.crystals);
|
||||
}
|
||||
}
|
||||
if (this.workshop) {
|
||||
this.workshop.addMaterial('scrapMetal', 6);
|
||||
this.workshop.addMaterial('bioHumus', 4);
|
||||
}
|
||||
this.economy.addCoins(Math.max(0, izombieRewardCoins - 100));
|
||||
}
|
||||
|
||||
if (this.currentMode === 'adventure') {
|
||||
const campLevel = window.CampaignSystem ? window.CampaignSystem.getLevel(this.currentLevel) : null;
|
||||
if (campLevel && campLevel.reward) {
|
||||
@@ -726,7 +929,7 @@ window.GameEngine = class GameEngine {
|
||||
const statsEl = document.getElementById('victory-stats');
|
||||
if (statsEl) {
|
||||
if (this.currentMode === 'izombie') {
|
||||
statsEl.innerHTML = `Победа! Все подсолнухи съедены! Мозги захвачены!<br>Награда: <strong>+100 💰 монет</strong> (Всего: ${this.economy.totalCoins})`;
|
||||
statsEl.innerHTML = `Победа! Все цели уничтожены! Мозги захвачены!<br>Этап «Я, Зомби»: <strong>Этап ${izombieCurrentStage} из 5</strong>${izombieNextUnlocked ? ' (🎉 Открыт следующий этап!)' : ''}<br>Награда: <strong>+${izombieRewardCoins} 💰 монет</strong>, <strong>+6 ⚙️ деталей</strong>, <strong>+4 🧪 биогумуса</strong> (Всего: ${this.economy.totalCoins})`;
|
||||
} else if (this.currentMode === 'bossrush') {
|
||||
statsEl.innerHTML = `Триумф! Режим Boss Rush успешно завершён!<br>Награда: <strong>+500 💰 монет</strong> • <strong>+2 💎 кристалла</strong> (Всего: ${this.economy.totalCoins})`;
|
||||
} else if (this.currentMode === 'adventure' && window.CampaignSystem) {
|
||||
@@ -1174,6 +1377,11 @@ window.GameEngine = class GameEngine {
|
||||
authModal.classList.remove('hidden');
|
||||
if (authUser && window.PvZAuth && window.PvZAuth.isGuest()) {
|
||||
setTimeout(() => authUser.focus(), 50);
|
||||
} else if (window.PvZAuth && !window.PvZAuth.isGuest()) {
|
||||
window.PvZAuth.fetchProfile().then(() => {
|
||||
this.loadUserData();
|
||||
this.updateProfileUI();
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -1368,6 +1576,49 @@ window.GameEngine = class GameEngine {
|
||||
});
|
||||
}
|
||||
|
||||
// Active Sessions event listeners
|
||||
const btnRefreshSessions = document.getElementById('btn-refresh-sessions');
|
||||
if (btnRefreshSessions) {
|
||||
btnRefreshSessions.addEventListener('click', async () => {
|
||||
btnRefreshSessions.classList.add('rotating');
|
||||
await this.renderUserSessionsUI();
|
||||
setTimeout(() => btnRefreshSessions.classList.remove('rotating'), 500);
|
||||
});
|
||||
}
|
||||
|
||||
const btnTermOthers = document.getElementById('btn-terminate-other-sessions');
|
||||
if (btnTermOthers) {
|
||||
btnTermOthers.addEventListener('click', async () => {
|
||||
if (confirm('Завершить сессии на всех других устройствах?')) {
|
||||
btnTermOthers.disabled = true;
|
||||
btnTermOthers.textContent = '⏳ Завершение...';
|
||||
const res = await window.PvZAuth.terminateOtherSessions();
|
||||
btnTermOthers.disabled = false;
|
||||
btnTermOthers.textContent = '🔒 Завершить сессии на всех других устройствах';
|
||||
if (res && res.success) {
|
||||
if (window.PvZAuth && window.PvZAuth.showNotification) {
|
||||
window.PvZAuth.showNotification('Все другие сессии завершены!', 'Осталось только текущее устройство', '🔒');
|
||||
}
|
||||
} else {
|
||||
if (window.PvZAuth && window.PvZAuth.showNotification) {
|
||||
window.PvZAuth.showNotification('Ошибка', res?.message || 'Не удалось завершить сессии', '❌');
|
||||
}
|
||||
}
|
||||
await this.renderUserSessionsUI();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Real-time Profile change event subscription
|
||||
if (window.PvZAuth && typeof window.PvZAuth.onProfileChange === 'function') {
|
||||
window.PvZAuth.onProfileChange(() => {
|
||||
this.loadUserData();
|
||||
this.updateProfileUI();
|
||||
this.updateRecordDisplay();
|
||||
this.updateShopUI();
|
||||
});
|
||||
}
|
||||
|
||||
// Auto-save on page hide / tab switch / page unload
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'hidden') {
|
||||
@@ -1523,8 +1774,10 @@ window.GameEngine = class GameEngine {
|
||||
const btnCloseWorkshop = document.getElementById('btn-close-workshop');
|
||||
const tabPlants = document.getElementById('tab-plants');
|
||||
const tabPerks = document.getElementById('tab-perks');
|
||||
const tabZombies = document.getElementById('tab-zombies');
|
||||
const contentPlants = document.getElementById('ws-content-plants');
|
||||
const contentPerks = document.getElementById('ws-content-perks');
|
||||
const contentZombies = document.getElementById('ws-content-zombies');
|
||||
|
||||
const openWorkshop = () => {
|
||||
this.updateWorkshopUI();
|
||||
@@ -1540,18 +1793,194 @@ window.GameEngine = class GameEngine {
|
||||
});
|
||||
}
|
||||
|
||||
if (tabPlants && tabPerks && contentPlants && contentPerks) {
|
||||
tabPlants.addEventListener('click', () => {
|
||||
tabPlants.classList.add('active');
|
||||
tabPerks.classList.remove('active');
|
||||
contentPlants.classList.remove('hidden');
|
||||
contentPerks.classList.add('hidden');
|
||||
const switchWorkshopTab = (tab) => {
|
||||
if (tabPlants) tabPlants.classList.toggle('active', tab === 'plants');
|
||||
if (tabPerks) tabPerks.classList.toggle('active', tab === 'perks');
|
||||
if (tabZombies) tabZombies.classList.toggle('active', tab === 'zombies');
|
||||
|
||||
if (contentPlants) contentPlants.classList.toggle('hidden', tab !== 'plants');
|
||||
if (contentPerks) contentPerks.classList.toggle('hidden', tab !== 'perks');
|
||||
if (contentZombies) contentZombies.classList.toggle('hidden', tab !== 'zombies');
|
||||
};
|
||||
|
||||
if (tabPlants) tabPlants.addEventListener('click', () => switchWorkshopTab('plants'));
|
||||
if (tabPerks) tabPerks.addEventListener('click', () => switchWorkshopTab('perks'));
|
||||
if (tabZombies) tabZombies.addEventListener('click', () => switchWorkshopTab('zombies'));
|
||||
|
||||
// Casino Modal Handlers
|
||||
const casinoModal = document.getElementById('casino-modal');
|
||||
const btnCasinoStart = document.getElementById('btn-casino-start');
|
||||
const btnCasinoDrawer = document.getElementById('btn-drawer-casino');
|
||||
const btnCloseCasino = document.getElementById('btn-close-casino');
|
||||
const tabCasinoSlots = document.getElementById('tab-casino-slots');
|
||||
const tabCasinoBank = document.getElementById('tab-casino-bank');
|
||||
const contentCasinoSlots = document.getElementById('casino-content-slots');
|
||||
const contentCasinoBank = document.getElementById('casino-content-bank');
|
||||
|
||||
const openCasino = () => {
|
||||
this.updateCasinoUI();
|
||||
if (casinoModal) casinoModal.classList.remove('hidden');
|
||||
};
|
||||
|
||||
if (btnCasinoStart) btnCasinoStart.addEventListener('click', openCasino);
|
||||
if (btnCasinoDrawer) btnCasinoDrawer.addEventListener('click', () => {
|
||||
openCasino();
|
||||
const drawer = document.getElementById('mobile-drawer');
|
||||
if (drawer) drawer.classList.add('hidden');
|
||||
});
|
||||
if (btnCloseCasino && casinoModal) {
|
||||
btnCloseCasino.addEventListener('click', () => {
|
||||
casinoModal.classList.add('hidden');
|
||||
this.updateRecordDisplay();
|
||||
});
|
||||
tabPerks.addEventListener('click', () => {
|
||||
tabPerks.classList.add('active');
|
||||
tabPlants.classList.remove('active');
|
||||
contentPerks.classList.remove('hidden');
|
||||
contentPlants.classList.add('hidden');
|
||||
}
|
||||
|
||||
if (tabCasinoSlots && tabCasinoBank) {
|
||||
tabCasinoSlots.addEventListener('click', () => {
|
||||
tabCasinoSlots.classList.add('active');
|
||||
tabCasinoBank.classList.remove('active');
|
||||
if (contentCasinoSlots) contentCasinoSlots.classList.remove('hidden');
|
||||
if (contentCasinoBank) contentCasinoBank.classList.add('hidden');
|
||||
});
|
||||
tabCasinoBank.addEventListener('click', () => {
|
||||
tabCasinoBank.classList.add('active');
|
||||
tabCasinoSlots.classList.remove('active');
|
||||
if (contentCasinoBank) contentCasinoBank.classList.remove('hidden');
|
||||
if (contentCasinoSlots) contentCasinoSlots.classList.add('hidden');
|
||||
this.updateCasinoUI();
|
||||
});
|
||||
}
|
||||
|
||||
// Casino Bet Buttons
|
||||
const betBtns = document.querySelectorAll('.btn-bet');
|
||||
betBtns.forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
betBtns.forEach(b => b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const betVal = parseInt(btn.getAttribute('data-bet') || '50', 10);
|
||||
if (this.casino) this.casino.currentBet = betVal;
|
||||
});
|
||||
});
|
||||
|
||||
// Casino Spin Button
|
||||
const btnSpin = document.getElementById('btn-spin-slots');
|
||||
const reel1 = document.getElementById('reel-1');
|
||||
const reel2 = document.getElementById('reel-2');
|
||||
const reel3 = document.getElementById('reel-3');
|
||||
const resultBanner = document.getElementById('slot-result-banner');
|
||||
|
||||
if (btnSpin) {
|
||||
btnSpin.addEventListener('click', () => {
|
||||
if (!this.casino) return;
|
||||
if (this.casino.isSpinning) return;
|
||||
const bet = this.casino.currentBet || 50;
|
||||
if (this.economy.totalCoins < bet) {
|
||||
if (resultBanner) {
|
||||
resultBanner.textContent = '❌ Недостаточно монет для ставки! Возьмите займ в банке.';
|
||||
resultBanner.style.color = '#ef5350';
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Animate spin
|
||||
[reel1, reel2, reel3].forEach(r => { if (r) r.classList.add('spinning'); });
|
||||
if (btnSpin) btnSpin.disabled = true;
|
||||
if (resultBanner) {
|
||||
resultBanner.textContent = '🎰 Барабаны вращаются...';
|
||||
resultBanner.style.color = '#ffca28';
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
if (this.casino) {
|
||||
this.casino.isSpinning = false;
|
||||
const res = this.casino.spin(bet);
|
||||
[reel1, reel2, reel3].forEach(r => { if (r) r.classList.remove('spinning'); });
|
||||
if (btnSpin) btnSpin.disabled = false;
|
||||
|
||||
if (res.icons) {
|
||||
if (reel1) reel1.innerHTML = `<span class="reel-symbol">${res.icons[0]}</span>`;
|
||||
if (reel2) reel2.innerHTML = `<span class="reel-symbol">${res.icons[1]}</span>`;
|
||||
if (reel3) reel3.innerHTML = `<span class="reel-symbol">${res.icons[2]}</span>`;
|
||||
}
|
||||
|
||||
if (resultBanner) {
|
||||
resultBanner.textContent = res.message || '';
|
||||
if (res.isJackpot) resultBanner.style.color = '#ffd700';
|
||||
else if (res.payout > 0) resultBanner.style.color = '#69f0ae';
|
||||
else resultBanner.style.color = '#b0bec5';
|
||||
}
|
||||
|
||||
this.updateCasinoUI();
|
||||
this.updateRecordDisplay();
|
||||
}
|
||||
}, 500);
|
||||
});
|
||||
}
|
||||
|
||||
// Bank Deposit
|
||||
const btnDeposit = document.getElementById('btn-bank-deposit');
|
||||
const inputDeposit = document.getElementById('input-deposit-amount');
|
||||
if (btnDeposit) {
|
||||
btnDeposit.addEventListener('click', () => {
|
||||
if (!this.casino) return;
|
||||
const amt = parseInt(inputDeposit ? inputDeposit.value : '0', 10);
|
||||
const res = this.casino.depositCoins(amt);
|
||||
if (resultBanner) {
|
||||
resultBanner.textContent = res.message;
|
||||
resultBanner.style.color = res.success ? '#69f0ae' : '#ef5350';
|
||||
}
|
||||
if (inputDeposit && res.success) inputDeposit.value = '';
|
||||
this.updateCasinoUI();
|
||||
this.updateRecordDisplay();
|
||||
});
|
||||
}
|
||||
|
||||
// Bank Withdraw
|
||||
const btnWithdraw = document.getElementById('btn-bank-withdraw');
|
||||
if (btnWithdraw) {
|
||||
btnWithdraw.addEventListener('click', () => {
|
||||
if (!this.casino) return;
|
||||
const res = this.casino.withdrawDeposit();
|
||||
if (resultBanner) {
|
||||
resultBanner.textContent = res.message;
|
||||
resultBanner.style.color = res.success ? '#69f0ae' : '#ef5350';
|
||||
}
|
||||
this.updateCasinoUI();
|
||||
this.updateRecordDisplay();
|
||||
});
|
||||
}
|
||||
|
||||
// Bank Take Loan
|
||||
const btnTakeLoan = document.getElementById('btn-bank-take-loan');
|
||||
const inputLoan = document.getElementById('input-loan-amount');
|
||||
if (btnTakeLoan) {
|
||||
btnTakeLoan.addEventListener('click', () => {
|
||||
if (!this.casino) return;
|
||||
const amt = parseInt(inputLoan ? inputLoan.value : '0', 10);
|
||||
const res = this.casino.takeLoan(amt);
|
||||
if (resultBanner) {
|
||||
resultBanner.textContent = res.message;
|
||||
resultBanner.style.color = res.success ? '#69f0ae' : '#ef5350';
|
||||
}
|
||||
this.updateCasinoUI();
|
||||
this.updateRecordDisplay();
|
||||
});
|
||||
}
|
||||
|
||||
// Bank Repay Loan
|
||||
const btnRepayLoan = document.getElementById('btn-bank-repay-loan');
|
||||
if (btnRepayLoan) {
|
||||
btnRepayLoan.addEventListener('click', () => {
|
||||
if (!this.casino) return;
|
||||
const amt = parseInt(inputLoan && inputLoan.value ? inputLoan.value : String(this.casino.loan), 10);
|
||||
const res = this.casino.repayLoan(amt);
|
||||
if (resultBanner) {
|
||||
resultBanner.textContent = res.message;
|
||||
resultBanner.style.color = res.success ? '#69f0ae' : '#ef5350';
|
||||
}
|
||||
if (inputLoan && res.success) inputLoan.value = '';
|
||||
this.updateCasinoUI();
|
||||
this.updateRecordDisplay();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -2085,7 +2514,9 @@ window.GameEngine = class GameEngine {
|
||||
window.PLANT_CATALOGUE.forEach(p => {
|
||||
const isBasic = this.economy.defaultUnlocked.includes(p.id);
|
||||
const isUnlocked = this.economy.isPlantUnlocked(p.id);
|
||||
const canAfford = this.economy.totalCoins >= (p.price || 0);
|
||||
const price = window.PvZConfig ? window.PvZConfig.getPlantShopPrice(p.id) : (p.price || 0);
|
||||
const sunCost = window.PvZConfig ? window.PvZConfig.getPlantSunCost(p.id, p.cost) : p.cost;
|
||||
const canAfford = this.economy.totalCoins >= price;
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'shop-plant-card';
|
||||
@@ -2096,14 +2527,14 @@ window.GameEngine = class GameEngine {
|
||||
} else if (isUnlocked) {
|
||||
actionHtml = '<span class="badge-purchased">Куплено ✅</span>';
|
||||
} else {
|
||||
actionHtml = `<button type="button" class="btn-buy" data-plant-id="${p.id}" ${canAfford ? '' : 'disabled'}>Купить (${p.price} 💰)</button>`;
|
||||
actionHtml = `<button type="button" class="btn-buy" data-plant-id="${p.id}" ${canAfford ? '' : 'disabled'}>Купить (${price} 💰)</button>`;
|
||||
}
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="shop-plant-icon">${p.icon}</div>
|
||||
<div class="shop-plant-info">
|
||||
<h4>${p.name}</h4>
|
||||
<div class="shop-plant-cost">☀️ ${p.cost} солнца</div>
|
||||
<div class="shop-plant-cost">☀️ ${sunCost} солнца</div>
|
||||
<p>${p.desc}</p>
|
||||
</div>
|
||||
<div>${actionHtml}</div>
|
||||
@@ -2310,6 +2741,193 @@ window.GameEngine = class GameEngine {
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// 4. Zombie Lab mutations & campaign stages
|
||||
this.renderZombieLabUI();
|
||||
}
|
||||
|
||||
renderZombieLabUI() {
|
||||
const container = document.getElementById('zombies-upgrade-list');
|
||||
if (!container || !this.zombieLab) return;
|
||||
container.innerHTML = '';
|
||||
|
||||
// Campaign Stage Selector Banner
|
||||
const stageBanner = document.createElement('div');
|
||||
stageBanner.className = 'upgrade-card';
|
||||
stageBanner.style.gridColumn = '1 / -1';
|
||||
stageBanner.style.background = 'linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(38, 50, 56, 0.95))';
|
||||
stageBanner.style.border = '1px solid rgba(76, 175, 80, 0.5)';
|
||||
|
||||
const selStage = this.zombieLab.selectedStage || 1;
|
||||
const unlStage = this.zombieLab.unlockedStage || 1;
|
||||
|
||||
let stageBtnsHtml = '';
|
||||
this.zombieLab.stages.forEach(st => {
|
||||
const isUnlocked = st.id <= unlStage;
|
||||
const isSelected = st.id === selStage;
|
||||
const activeStyle = isSelected ? 'border: 2px solid #ffeb3b; background: rgba(255, 235, 59, 0.25); color: #fff;' : '';
|
||||
const lockIcon = isUnlocked ? (st.isBoss ? '👑' : '🧠') : '🔒';
|
||||
stageBtnsHtml += `
|
||||
<button class="btn btn-small btn-stage-select ${isSelected ? 'active' : ''}" data-stage-id="${st.id}" ${isUnlocked ? '' : 'disabled'} style="margin: 3px; font-size: 11px; padding: 5px 9px; ${activeStyle}">
|
||||
${lockIcon} Этап ${st.id} ${st.isBoss ? '(БОСС)' : ''}
|
||||
</button>
|
||||
`;
|
||||
});
|
||||
|
||||
const curStageDef = this.zombieLab.stages.find(s => s.id === selStage) || this.zombieLab.stages[0];
|
||||
|
||||
stageBanner.innerHTML = `
|
||||
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;">
|
||||
<div>
|
||||
<h3 style="margin: 0; color: #a5d6a7; font-size: 15px;">🗺️ Кампания «Я, Зомби» (Этап ${selStage} из 5)</h3>
|
||||
<p style="margin: 3px 0 0 0; font-size: 12px; color: #cfd8dc;">${curStageDef.title}: ${curStageDef.desc}</p>
|
||||
</div>
|
||||
<div style="display: flex; flex-wrap: wrap; gap: 4px;">
|
||||
${stageBtnsHtml}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
container.appendChild(stageBanner);
|
||||
|
||||
// Bind stage select buttons
|
||||
stageBanner.querySelectorAll('.btn-stage-select').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const stId = parseInt(e.currentTarget.getAttribute('data-stage-id'), 10);
|
||||
this.zombieLab.selectedStage = stId;
|
||||
this.renderZombieLabUI();
|
||||
});
|
||||
});
|
||||
|
||||
// Zombie Mutation Cards
|
||||
for (const [key, zDef] of Object.entries(this.zombieLab.zombies)) {
|
||||
const curTier = this.zombieLab.getZombieLevel(key);
|
||||
const isMax = curTier >= 3;
|
||||
const curData = zDef.tiers[curTier] || zDef.tiers[1];
|
||||
const nextData = isMax ? null : zDef.tiers[curTier + 1];
|
||||
|
||||
const card = document.createElement('div');
|
||||
card.className = 'upgrade-card zombie-tier-card';
|
||||
|
||||
let costHtml = '';
|
||||
if (nextData && nextData.cost) {
|
||||
const parts = [];
|
||||
if (nextData.cost.coins) parts.push(`💰 ${nextData.cost.coins}`);
|
||||
if (nextData.cost.scrapMetal) parts.push(`⚙️ ${nextData.cost.scrapMetal}`);
|
||||
if (nextData.cost.bioHumus) parts.push(`🧪 ${nextData.cost.bioHumus}`);
|
||||
if (nextData.cost.crystals) parts.push(`💎 ${nextData.cost.crystals}`);
|
||||
costHtml = parts.join(' • ');
|
||||
}
|
||||
|
||||
const stars = '★'.repeat(curTier) + '☆'.repeat(3 - curTier);
|
||||
const canAfford = this.zombieLab.canUpgrade(key);
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="upgrade-card-header">
|
||||
<div class="upgrade-plant-icon">${zDef.icon}</div>
|
||||
<div class="upgrade-title-box">
|
||||
<h4>${zDef.name}</h4>
|
||||
<div class="upgrade-stars" style="color: #69f0ae;">${stars} (Ур. ${curTier}/3)</div>
|
||||
</div>
|
||||
<div class="zombie-tier-badge">Ранг ${curTier}</div>
|
||||
</div>
|
||||
|
||||
<div class="upgrade-stats-current" style="font-size: 12px; color: #cfd8dc; margin-bottom: 6px;">
|
||||
<strong>Текущий бонус:</strong> ${curData.title} — ${curData.desc}
|
||||
</div>
|
||||
|
||||
${!isMax ? `
|
||||
<div class="upgrade-stats-next" style="font-size: 12px; color: #a5d6a7; margin-bottom: 8px; border-left-color: #69f0ae;">
|
||||
<strong>Мутация до Ур. ${curTier + 1}:</strong> ${nextData.title} — ${nextData.desc}
|
||||
</div>
|
||||
<button class="btn btn-upgrade" data-upgrade-zombie="${key}" ${canAfford ? '' : 'disabled'} style="width: 100%; font-size: 12px; padding: 7px;">
|
||||
Мутировать (${costHtml})
|
||||
</button>
|
||||
` : `
|
||||
<div class="upgrade-stats-next" style="border-left-color: #ffd700; color: #ffe082; font-size: 12px;">
|
||||
🧬 Максимальная мутация достигнута!
|
||||
</div>
|
||||
`}
|
||||
`;
|
||||
|
||||
container.appendChild(card);
|
||||
}
|
||||
|
||||
// Bind upgrade buttons for zombies
|
||||
container.querySelectorAll('[data-upgrade-zombie]').forEach(btn => {
|
||||
btn.addEventListener('click', (e) => {
|
||||
const zType = e.currentTarget.getAttribute('data-upgrade-zombie');
|
||||
const res = this.zombieLab.upgradeZombie(zType);
|
||||
if (res.success) {
|
||||
this.saveUserData();
|
||||
this.updateWorkshopUI();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
updateCasinoUI() {
|
||||
if (!this.casino) return;
|
||||
const coinsEl = document.getElementById('casino-coins');
|
||||
if (coinsEl) coinsEl.textContent = (this.economy ? this.economy.totalCoins : 0).toLocaleString();
|
||||
|
||||
const depStat = document.getElementById('bank-deposit-stat');
|
||||
if (depStat) depStat.textContent = this.casino.deposit.toLocaleString();
|
||||
|
||||
const intStat = document.getElementById('bank-interest-stat');
|
||||
if (intStat) intStat.textContent = `+${this.casino.getAccruedInterest().toLocaleString()}`;
|
||||
|
||||
const debtStat = document.getElementById('bank-debt-stat');
|
||||
if (debtStat) debtStat.textContent = this.casino.loan.toLocaleString();
|
||||
|
||||
const depVal = document.getElementById('bank-deposit-val');
|
||||
if (depVal) depVal.textContent = this.casino.deposit.toLocaleString();
|
||||
|
||||
const intVal = document.getElementById('bank-interest-val');
|
||||
if (intVal) intVal.textContent = this.casino.getAccruedInterest().toLocaleString();
|
||||
|
||||
const debtVal = document.getElementById('bank-debt-val');
|
||||
if (debtVal) debtVal.textContent = this.casino.loan.toLocaleString();
|
||||
}
|
||||
|
||||
triggerShrekEasterEgg() {
|
||||
if (window.PvZAudio && window.PvZAudio.playExplosion) {
|
||||
window.PvZAudio.playExplosion();
|
||||
}
|
||||
const canvas = document.getElementById('game-canvas');
|
||||
if (canvas) {
|
||||
canvas.classList.add('shake');
|
||||
setTimeout(() => canvas.classList.remove('shake'), 1200);
|
||||
}
|
||||
|
||||
let affected = 0;
|
||||
if (this.zombies && this.zombies.length > 0) {
|
||||
for (const z of this.zombies) {
|
||||
if (z && z.health > 0) {
|
||||
z.takeDamage(2000);
|
||||
affected++;
|
||||
if (this.combat && this.combat.createSplash) {
|
||||
this.combat.createSplash(z.x, z.y, '#7cb342', 12);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let toast = document.getElementById('achievement-toast');
|
||||
if (!toast) {
|
||||
toast = document.createElement('div');
|
||||
toast.id = 'achievement-toast';
|
||||
toast.className = 'achievement-toast';
|
||||
document.body.appendChild(toast);
|
||||
}
|
||||
toast.innerHTML = `
|
||||
<div class="ach-toast-icon">🧅</div>
|
||||
<div class="ach-toast-text">
|
||||
<div class="ach-toast-subtitle">«THIS IS MY SWAMP!»</div>
|
||||
<div class="ach-toast-title">ШРЕК СОТРЯС ЗЕМЛЮ! 2000 урона всем зомби! (${affected} поражено)</div>
|
||||
</div>
|
||||
`;
|
||||
toast.classList.add('visible');
|
||||
setTimeout(() => toast.classList.remove('visible'), 4000);
|
||||
}
|
||||
|
||||
initZenGarden() {
|
||||
@@ -2756,6 +3374,16 @@ window.GameEngine = class GameEngine {
|
||||
return;
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return '';
|
||||
return String(str)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
let rowsHtml = '';
|
||||
entries.forEach((item, index) => {
|
||||
let medal = `#${index + 1}`;
|
||||
@@ -2763,12 +3391,16 @@ window.GameEngine = class GameEngine {
|
||||
else if (index === 1) medal = '🥈 2';
|
||||
else if (index === 2) medal = '🥉 3';
|
||||
|
||||
const safeName = escapeHtml(item.name || 'Игрок');
|
||||
const safeDetail = escapeHtml(item.detail || item.score || '');
|
||||
const safeDate = escapeHtml(item.date || '');
|
||||
|
||||
rowsHtml += `
|
||||
<tr class="${index < 3 ? 'top-rank' : ''}">
|
||||
<td style="font-weight: bold; width: 60px; text-align: center;">${medal}</td>
|
||||
<td style="font-weight: bold; color: #fff;">${item.name}</td>
|
||||
<td style="color: #ffd54f; font-weight: bold;">${item.detail || item.score}</td>
|
||||
<td style="color: #b0bec5; font-size: 11px; text-align: right;">${item.date || ''}</td>
|
||||
<td style="font-weight: bold; color: #fff;">${safeName}</td>
|
||||
<td style="color: #ffd54f; font-weight: bold;">${safeDetail}</td>
|
||||
<td style="color: #b0bec5; font-size: 11px; text-align: right;">${safeDate}</td>
|
||||
</tr>
|
||||
`;
|
||||
});
|
||||
@@ -3117,14 +3749,14 @@ window.GameEngine = class GameEngine {
|
||||
|
||||
// 9. I, Zombie Win & Defeat Check
|
||||
if (this.currentMode === 'izombie' && this.izombieSetupDone && this.isRunning) {
|
||||
const sunflowers = this.plants.filter(p => p.type === 'sunflower');
|
||||
if (sunflowers.length === 0) {
|
||||
const targets = this.plants.filter(p => p.type === 'sunflower' || p.type === 'mega_sunflower');
|
||||
if (targets.length === 0) {
|
||||
this.triggerVictory();
|
||||
} else {
|
||||
const minZombieCost = 50;
|
||||
const hasAliveZombies = this.zombies.some(z => z.health > 0 && !z.toRemove);
|
||||
if (!hasAliveZombies && this.economy.sun < minZombieCost) {
|
||||
this.triggerGameOver('Закончились солнце и зомби! Подсолнухи уцелели.');
|
||||
this.triggerGameOver('Закончились солнце и зомби! Растения уцелели.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user