// Main Game Engine Controller (Sprint v1.2 - v1.5 Complete PvZ Platform)
window.GameEngine = class GameEngine {
constructor(canvasId) {
this.canvas = document.getElementById(canvasId);
this.ctx = this.canvas.getContext('2d');
// Logical dimensions
this.logicalWidth = 900;
this.logicalHeight = 600;
this.width = this.logicalWidth;
this.height = this.logicalHeight;
// HiDPI / Retina setup
this.setupHiDPI();
window.addEventListener('resize', () => this.setupHiDPI());
// Systems
this.grid = new window.GridSystem();
this.economy = new window.EconomySystem();
this.economy.engine = this;
if (window.WorkshopSystem) {
this.workshop = new window.WorkshopSystem(this.economy);
this.economy.workshop = this.workshop;
}
if (window.ZenGardenSystem) {
this.zengarden = new window.ZenGardenSystem(this.economy);
}
if (window.AlmanacSystem) {
this.almanac = new window.AlmanacSystem();
}
if (window.AchievementSystem) {
this.achievements = new window.AchievementSystem(this);
}
if (window.RoguelikeSystem) {
this.roguelike = new window.RoguelikeSystem(this);
}
if (window.TreeSystem) {
this.tree = new window.TreeSystem(this.economy);
}
if (window.BossSystem) {
this.boss = new window.BossSystem(this);
}
if (window.LeaderboardSystem) {
this.leaderboards = new window.LeaderboardSystem();
}
if (window.PvZFeedback) {
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) {
this.projectilePool = new window.ProjectilePool(60);
}
// Entities
this.plants = [];
this.zombies = [];
this.projectiles = [];
// State
this.currentMode = 'adventure'; // 'adventure', 'survival', 'bowling', 'izombie', 'roguelike'
this.currentLevel = 1;
this.maxUnlockedLevel = 1;
this.isRunning = false;
this.isGameOver = false;
this.isVictory = false;
this.gameSpeed = 1.0;
this.isPaused = false;
this.lastTime = 0;
this.hoverCell = null;
this.loopStarted = false;
this.izombieSetupDone = false;
// Rake state
this.hasRakeArmed = false;
this.rakeCol = 7;
this.rakeRow = 2;
// Score & Persistence
this.score = 0;
this.zombiesKilled = 0;
this.stats = {
zombiesKilled: 0,
sunProduced: 0,
plantsPlanted: 0,
levelsCompleted: 0,
coinsEarned: 0
};
this.loadUserData();
this.updateRecordDisplay();
this.updateProfileUI();
// Register with Test API
if (window.PvZTestAPI) {
window.PvZTestAPI.setEngine(this);
}
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();
}
setupHiDPI() {
this.dpr = Math.min(window.devicePixelRatio || 1, 2.5);
this.canvas.width = this.logicalWidth * this.dpr;
this.canvas.height = this.logicalHeight * this.dpr;
this.ctx.resetTransform();
this.ctx.scale(this.dpr, this.dpr);
}
loadUserData() {
if (window.PvZAuth) {
const uData = window.PvZAuth.getUserData();
this.currentLevel = uData.level || 1;
this.maxUnlockedLevel = uData.maxUnlockedLevel || Math.max(this.currentLevel, 1);
this.highScore = uData.highScore || 0;
this.currentTheme = uData.theme || 'classic';
this.stats = uData.stats || {
zombiesKilled: 0,
sunProduced: 0,
plantsPlanted: 0,
levelsCompleted: 0,
coinsEarned: 0
};
this.applyTheme(this.currentTheme);
if (this.economy) this.economy.reloadProfileData();
if (this.workshop) this.workshop.loadFromAuth();
if (this.zengarden) this.zengarden.loadFromAuth();
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);
this.highScore = parseInt(localStorage.getItem('pvz_highscore') || '0', 10);
this.currentTheme = localStorage.getItem('pvz_theme') || 'classic';
try {
const rawStats = localStorage.getItem('pvz_stats');
this.stats = rawStats ? JSON.parse(rawStats) : {
zombiesKilled: 0,
sunProduced: 0,
plantsPlanted: 0,
levelsCompleted: 0,
coinsEarned: 0
};
} catch (_) {
this.stats = { zombiesKilled: 0, sunProduced: 0, plantsPlanted: 0, levelsCompleted: 0, coinsEarned: 0 };
}
this.applyTheme(this.currentTheme);
}
}
recordStat(key, amount = 1) {
if (!this.stats) {
this.stats = { zombiesKilled: 0, sunProduced: 0, plantsPlanted: 0, levelsCompleted: 0, coinsEarned: 0 };
}
this.stats[key] = (this.stats[key] || 0) + amount;
this.saveUserData();
}
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,
maxUnlockedLevel: this.maxUnlockedLevel,
highScore: this.highScore,
theme: this.currentTheme,
stats: this.stats,
upgrades: this.economy ? this.economy.upgrades : undefined,
materials: this.workshop ? this.workshop.materials : undefined,
plantLevels: this.workshop ? this.workshop.plantLevels : undefined,
perks: this.workshop ? this.workshop.perks : undefined,
zenGarden: this.zengarden ? {
pots: this.zengarden.pots,
fertilizerCount: this.zengarden.fertilizerCount,
chocolateCount: this.zengarden.chocolateCount,
snail: this.zengarden.snail,
lastVisit: Date.now()
} : undefined
});
}
localStorage.setItem('pvz_level', this.currentLevel.toString());
localStorage.setItem('pvz_max_level', this.maxUnlockedLevel.toString());
localStorage.setItem('pvz_highscore', this.highScore.toString());
localStorage.setItem('pvz_theme', this.currentTheme);
localStorage.setItem('pvz_stats', JSON.stringify(this.stats));
if (this.economy) {
localStorage.setItem('pvz_coins', this.economy.totalCoins.toString());
}
}
applyTheme(theme) {
this.currentTheme = theme || 'classic';
const body = document.body;
body.classList.remove('theme-cyberpunk', 'theme-retro8bit', 'theme-winter');
if (this.currentTheme !== 'classic') {
body.classList.add(`theme-${this.currentTheme}`);
}
const btnTheme = document.getElementById('btn-theme');
if (btnTheme) {
const labels = {
classic: '🎨 Классика',
cyberpunk: '🎨 Киберпанк',
retro8bit: '🎨 Ретро 8-бит',
winter: '🎨 Зима'
};
btnTheme.textContent = labels[this.currentTheme] || '🎨 Тема';
}
if (this.grid) {
this.grid.theme = this.currentTheme;
}
}
cycleTheme() {
const themes = ['classic', 'cyberpunk', 'retro8bit', 'winter'];
const nextIdx = (themes.indexOf(this.currentTheme) + 1) % themes.length;
this.applyTheme(themes[nextIdx]);
this.saveUserData();
if (window.PvZAudio) window.PvZAudio.playCoin();
}
togglePause() {
if (!this.isRunning) return;
this.isPaused = !this.isPaused;
const btnPause = document.getElementById('btn-pause');
if (btnPause) {
btnPause.textContent = this.isPaused ? '▶️ Играть' : '⏸️ Пауза';
if (this.isPaused) btnPause.classList.add('active');
else btnPause.classList.remove('active');
}
this.triggerHaptic(15);
if (window.PvZAudio) window.PvZAudio.playButtonClick();
}
updateProfileUI() {
const badgeName = document.getElementById('badge-user-name');
const btnAuth = document.getElementById('btn-auth-open');
const btnLogoutNav = document.getElementById('btn-auth-logout-nav');
const loggedInView = document.getElementById('auth-logged-in-view');
const loginView = document.getElementById('auth-login-view') || document.getElementById('auth-guest-view');
const registerView = document.getElementById('auth-register-view');
const userTitle = document.getElementById('profile-user-title');
const statCoins = document.getElementById('profile-stat-coins');
const statLevel = document.getElementById('profile-stat-level');
const statScore = document.getElementById('profile-stat-score');
const statMaterials = document.getElementById('profile-stat-materials');
// Modern Dashboard Hero Profile elements
const heroName = document.getElementById('hero-profile-name');
const heroCloud = document.getElementById('hero-cloud-badge');
const heroCoins = document.getElementById('hero-stat-coins');
const heroLevel = document.getElementById('hero-stat-level');
const heroScore = document.getElementById('hero-stat-score');
if (window.PvZAuth) {
const user = window.PvZAuth.getCurrentUser();
const isGuest = window.PvZAuth.isGuest();
const uData = window.PvZAuth.getUserData() || {};
const currentCoins = uData.coins !== undefined ? uData.coins : (this.economy ? this.economy.totalCoins : 0);
const currentLevelNum = uData.level !== undefined ? uData.level : (this.currentLevel || 1);
const currentScoreNum = uData.highScore !== undefined ? uData.highScore : (this.highScore || 0);
if (badgeName) badgeName.textContent = isGuest ? '👤 Гость' : `👤 ${user}`;
if (btnAuth) btnAuth.textContent = isGuest ? 'Войти' : 'Профиль';
if (heroName) heroName.textContent = isGuest ? 'Гость' : user;
if (heroCloud) {
heroCloud.textContent = isGuest ? 'Локально' : 'Облако';
heroCloud.className = isGuest ? 'badge-offline' : 'badge-online';
}
if (heroCoins) heroCoins.textContent = currentCoins.toLocaleString('ru-RU');
if (heroLevel) heroLevel.textContent = `Ур. ${currentLevelNum}`;
if (heroScore) heroScore.textContent = currentScoreNum.toLocaleString('ru-RU');
if (btnLogoutNav) {
if (isGuest) btnLogoutNav.classList.add('hidden');
else btnLogoutNav.classList.remove('hidden');
}
if (loggedInView) {
if (isGuest) {
loggedInView.classList.add('hidden');
if (loginView) loginView.classList.remove('hidden');
if (registerView) registerView.classList.add('hidden');
} else {
loggedInView.classList.remove('hidden');
if (loginView) loginView.classList.add('hidden');
if (registerView) registerView.classList.add('hidden');
if (userTitle) userTitle.textContent = user;
if (statCoins) statCoins.textContent = currentCoins.toLocaleString('ru-RU');
if (statLevel) statLevel.textContent = `Уровень ${currentLevelNum}`;
if (statScore) statScore.textContent = currentScoreNum.toLocaleString('ru-RU');
if (statMaterials) {
const mats = uData.materials || (this.workshop ? this.workshop.materials : {});
const totalMats = (mats.bioHumus || 0) + (mats.scrapMetal || 0) + (mats.sunEssence || 0) + (mats.crystals || 0);
statMaterials.textContent = `${totalMats} шт.`;
}
// Extended lifetime player stats
const stats = uData.stats || this.stats || {};
const statZombies = document.getElementById('profile-stat-zombies');
const statSun = document.getElementById('profile-stat-sun');
const statPlants = document.getElementById('profile-stat-plants');
const statLevelsWon = document.getElementById('profile-stat-levels-won');
const statCoinsEarned = document.getElementById('profile-stat-coins-earned');
if (statZombies) statZombies.textContent = (stats.zombiesKilled || 0).toLocaleString('ru-RU');
if (statSun) statSun.textContent = (stats.sunProduced || 0).toLocaleString('ru-RU');
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 = '
Войдите в аккаунт для просмотра сессий
';
return;
}
const sessions = await window.PvZAuth.fetchSessions();
if (!sessions || sessions.length === 0) {
listEl.innerHTML = 'Нет активных сессий
';
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();
const startModal = document.getElementById('start-modal');
if (startModal) startModal.classList.remove('hidden');
// 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', 'casino-modal'].forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.add('hidden');
});
// Highlight the mode button that matches currentMode
const modeMap = {
adventure: 'btn-mode-adv',
survival: 'btn-mode-surv',
bowling: 'btn-mode-bowl',
izombie: 'btn-mode-izombie',
roguelike: 'btn-mode-rogue',
boss: 'btn-mode-boss',
bossrush: 'btn-mode-bossrush'
};
const activeBtnId = modeMap[this.currentMode] || 'btn-mode-adv';
this.highlightModeBtn(activeBtnId);
this.updateRecordDisplay();
this.updateProfileUI();
}
updateRecordDisplay() {
const elRecord = document.getElementById('start-record');
if (elRecord) {
const lvlLabel = (window.CampaignSystem && this.currentMode === 'adventure')
? window.CampaignSystem.formatLevel(this.currentLevel).fullLabel
: this.currentLevel;
elRecord.innerHTML = `🏆 Рекорд: ${this.highScore} очков | 💰 Монеты: ${this.economy.totalCoins} | Уровень: ${lvlLabel}`;
}
}
addScore(pts) {
this.score += pts;
if (this.score > this.highScore) {
this.highScore = this.score;
this.saveUserData();
}
}
startLevel(level = 1, mode = 'adventure') {
this.currentLevel = level;
this.currentMode = mode;
// Pre-set economy mode flags BEFORE reset() so economy.reset() computes
// the correct baseSun (avoids stale isConveyor/isIZombie from previous game)
this.economy.isConveyor = (mode === 'bowling' || mode === 'boss' || mode === 'bossrush');
this.economy.isIZombie = (mode === 'izombie');
this.economy.isNight = (mode === 'izombie');
this.reset();
this.wave.setLevel(level, mode);
this.grid.initBiome(this.wave.biome);
if (mode === 'bowling') {
this.economy.setConveyorDeck();
this.hasRakeArmed = false;
} else if (mode === 'izombie') {
this.economy.setIZombieDeck();
this.grid.lawnmowers = [];
this.hasRakeArmed = false;
this.setupIZombieLawn(); // place lawn plants for I, Zombie mode
} else if (mode === 'boss' || mode === 'bossrush') {
this.economy.setBossDeck();
if (this.boss) this.boss.reset();
this.hasRakeArmed = false;
} else if (mode === 'roguelike') {
this.economy.isConveyor = false;
this.economy.isIZombie = false;
if (!this.economy.cards || this.economy.cards.length === 0 || (this.roguelike && this.roguelike.currentFloor === 1) || this.economy.cards.some(c => c.id.startsWith('z_') || c.id.startsWith('bowling_'))) {
this.economy.cards = this.economy.allCards
.filter(c => ['sunflower', 'peashooter'].includes(c.id))
.map(c => ({ ...c, timer: 0 }));
}
this.hasRakeArmed = this.economy.upgrades.hasRake;
this.rakeRow = Math.floor(Math.random() * this.grid.rows);
this.rakeCol = 7;
} else {
this.economy.setDeckForLevel(level, this.wave.biome);
this.hasRakeArmed = this.economy.upgrades.hasRake;
this.rakeRow = Math.floor(Math.random() * this.grid.rows);
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(stage) {
this.plants = [];
this.izombieSetupDone = true;
const currentStage = stage || (this.zombieLab ? (this.zombieLab.selectedStage || this.zombieLab.stage || 1) : 1);
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 });
}
}
showDaveDialogue(level, mode, onContinue) {
const dialogue = window.CampaignSystem ? window.CampaignSystem.getDaveDialogue(level, mode) : null;
if (!dialogue) {
if (typeof onContinue === 'function') onContinue();
return;
}
const modal = document.getElementById('dave-modal');
const speakerEl = document.getElementById('dave-modal-speaker');
const subEl = document.getElementById('dave-modal-subtitle');
const textEl = document.getElementById('dave-modal-text');
const btnContinue = document.getElementById('btn-dave-continue');
if (speakerEl) speakerEl.textContent = dialogue.title || 'Безумный Дэйв';
if (subEl) subEl.textContent = 'Твой чудаковатый сосед';
if (textEl) textEl.textContent = `«${dialogue.text}»`;
if (btnContinue && dialogue.btnText) btnContinue.textContent = dialogue.btnText;
if (modal) modal.classList.remove('hidden');
const handleContinue = () => {
if (modal) modal.classList.add('hidden');
if (window.PvZAudio) window.PvZAudio.playButtonClick();
if (typeof onContinue === 'function') onContinue();
};
if (btnContinue) {
const newBtn = btnContinue.cloneNode(true);
btnContinue.parentNode.replaceChild(newBtn, btnContinue);
newBtn.addEventListener('click', handleContinue);
}
}
init() {
let startLvl = 1;
if (this.currentMode === 'adventure') {
startLvl = this.currentLevel || 1;
} else if (this.currentMode === 'roguelike') {
startLvl = this.roguelike ? this.roguelike.currentFloor : 1;
}
this.showDaveDialogue(startLvl, this.currentMode, () => {
// Fixed-conveyor mini-games launch immediately, standard modes open deck builder
if (this.currentMode === 'bowling' || this.currentMode === 'izombie' || this.currentMode === 'boss' || this.currentMode === 'bossrush') {
this.startLevel(startLvl, this.currentMode);
} else {
this.openSeedSelectionModal(startLvl, this.currentMode);
}
if (!this.loopStarted) {
this.loopStarted = true;
requestAnimationFrame(this.loop.bind(this));
}
});
}
reset() {
this.grid.reset();
this.economy.reset();
this.wave.reset();
this.combat.reset();
if (this.projectilePool) {
this.projectilePool.releaseAll(this.projectiles);
}
this.plants = [];
this.zombies = [];
this.projectiles = [];
this.score = 0;
this.zombiesKilled = 0;
this.isGameOver = false;
this.isVictory = false;
this.hoverCell = null;
this.izombieSetupDone = false; // ensure clean re-init for I, Zombie mode
document.getElementById('game-over-modal').classList.add('hidden');
document.getElementById('victory-modal').classList.add('hidden');
}
spawnProjectile(x, y, row, type = 'pea', vy = 0, damage = 20, isCrit = false, targetX = 750) {
const proj = this.projectilePool ? this.projectilePool.get(x, y, row, type, vy, damage, isCrit, targetX) : new window.Projectile(x, y, row, type, vy, damage, isCrit, targetX);
this.projectiles.push(proj);
return proj;
}
spawnSun(x, y, value = 25, isFalling = false, targetY = null) {
let finalValue = value;
if (this.roguelike && this.currentMode === 'roguelike' && this.roguelike.hasRelic('relic_sun_resonance') && !isFalling) {
finalValue += 15;
}
if (!isFalling) {
this.recordStat('sunProduced', finalValue);
}
this.economy.spawnSun(x, y, finalValue, isFalling, targetY);
}
spawnCoin(x, y, type = 'silver') {
this.economy.spawnCoin(x, y, type);
}
spawnZombie(row, type = 'normal') {
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);
}
explodeArea(centerCol, centerRow, radiusTiles = 3) {
const half = Math.floor(radiusTiles / 2);
const bounds = this.grid.getCellBounds(centerCol, centerRow);
this.combat.createExplosionParticles(bounds.centerX, bounds.centerY);
for (const z of this.zombies) {
if (z.toRemove || z.health <= 0) continue;
const rowDiff = Math.abs(z.row - centerRow);
const colEstimated = (z.x - this.grid.startX) / this.grid.cellW;
const colDiff = Math.abs(colEstimated - centerCol);
if (rowDiff <= half && colDiff <= half + 0.5) {
z.takeDamage(1800);
if (z.health <= 0) {
this.addScore(250);
this.zombiesKilled++;
this.recordStat('zombiesKilled', 1);
if (Math.random() < 0.4) this.spawnCoin(z.x, z.y, 'gold');
}
}
}
}
triggerGameOver(reason = 'Зомби прорвали оборону газона. Попробуйте снова!') {
if (this.isGameOver) return;
this.isGameOver = true;
this.isRunning = false;
if (window.PvZAudio) {
window.PvZAudio.stopMusic();
window.PvZAudio.playLose();
}
if (this.leaderboards) {
if (this.currentMode === 'survival') {
this.leaderboards.submitScore('survival', this.wave.survivalFlags || 0, `${this.wave.survivalFlags || 0} Флагов`);
} else if (this.currentMode === 'roguelike') {
const flr = this.roguelike ? this.roguelike.currentFloor : 1;
this.leaderboards.submitScore('roguelike', flr, `${flr} Этаж`);
} else {
this.leaderboards.submitScore(this.currentMode, this.score, `${this.score} очков`);
}
}
this.saveUserData();
if (window.PvZAuth && window.PvZAuth.flushSave) window.PvZAuth.flushSave();
const descEl = document.getElementById('game-over-desc');
if (descEl) descEl.textContent = reason;
const statsEl = document.getElementById('game-over-stats');
if (statsEl) {
statsEl.innerHTML = `Счёт: ${this.score} | Зомби: ${this.zombiesKilled}
Рекорд: ${this.highScore} | Монеты: ${this.economy.totalCoins}`;
}
document.getElementById('game-over-modal').classList.remove('hidden');
}
triggerVictory() {
if (this.isVictory) return;
this.isVictory = true;
this.isRunning = false;
this.recordStat('levelsCompleted', 1);
this.addScore(1000);
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.used && !lm.active)) {
if (this.achievements) this.achievements.trigger('clean_lawn');
}
if (this.currentMode === 'izombie' && this.achievements) {
this.achievements.trigger('izombie_win');
}
if (this.leaderboards) {
if (this.currentMode === 'bossrush') {
this.leaderboards.submitScore('bossrush', 3, 'Boss Rush пройден! (3 волны)');
this.economy.addCoins(400); // 100 + 400 = 500 bonus
if (this.workshop) {
this.workshop.materials.crystals += 2;
}
} else if (this.currentMode === 'boss') {
this.leaderboards.submitScore('boss', 1, 'Победа над Зомбоссом');
} else if (this.currentMode === 'adventure') {
this.leaderboards.submitScore('adventure', this.currentLevel, `Уровень ${this.currentLevel}`);
} else if (this.currentMode === 'roguelike') {
const flr = this.roguelike ? this.roguelike.currentFloor : 1;
this.leaderboards.submitScore('roguelike', flr, `${flr} Этажей`);
} else {
this.leaderboards.submitScore(this.currentMode, this.score, `${this.score} очков`);
}
}
// 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) {
levelReward = campLevel.reward;
if (levelReward > 100) {
this.economy.addCoins(levelReward - 100);
}
}
if (this.currentLevel >= this.maxUnlockedLevel && this.currentLevel < 25) {
this.maxUnlockedLevel = this.currentLevel + 1;
}
if (this.currentLevel < 25) {
this.currentLevel++;
}
}
this.saveUserData();
if (window.PvZAuth && window.PvZAuth.flushSave) window.PvZAuth.flushSave();
if (this.currentMode === 'roguelike') {
if (window.PvZAudio) {
window.PvZAudio.stopMusic();
window.PvZAudio.playWin();
}
this.showDraftModal();
return;
}
if (window.PvZAudio) {
window.PvZAudio.stopMusic();
window.PvZAudio.playWin();
}
const statsEl = document.getElementById('victory-stats');
if (statsEl) {
if (this.currentMode === 'izombie') {
statsEl.innerHTML = `Победа! Все цели уничтожены! Мозги захвачены!
Этап «Я, Зомби»: Этап ${izombieCurrentStage} из 5${izombieNextUnlocked ? ' (🎉 Открыт следующий этап!)' : ''}
Награда: +${izombieRewardCoins} 💰 монет, +6 ⚙️ деталей, +4 🧪 биогумуса (Всего: ${this.economy.totalCoins})`;
} else if (this.currentMode === 'bossrush') {
statsEl.innerHTML = `Триумф! Режим Boss Rush успешно завершён!
Награда: +500 💰 монет • +2 💎 кристалла (Всего: ${this.economy.totalCoins})`;
} else if (this.currentMode === 'adventure' && window.CampaignSystem) {
const nextInfo = window.CampaignSystem.formatLevel(this.currentLevel);
statsEl.innerHTML = `Победа! Финальный счёт: ${this.score} (+1000 очков)
Награда за этап: +${levelReward} 💰 монет (Всего: ${this.economy.totalCoins})
Следующий этап: ${nextInfo.fullLabel}`;
} else {
statsEl.innerHTML = `Победа! Финальный счёт: ${this.score} (+1000 очков)
Награда: +100 💰 монет (Всего: ${this.economy.totalCoins})
Уровень разблокирован: ${this.currentLevel}`;
}
}
document.getElementById('victory-modal').classList.remove('hidden');
}
triggerHaptic(ms = 15) {
if (typeof navigator !== 'undefined' && navigator.vibrate) {
try { navigator.vibrate(ms); } catch (_) {}
}
}
getEventPos(e) {
const rect = this.canvas.getBoundingClientRect();
let clientX, clientY;
if (e.touches && e.touches.length > 0) {
clientX = e.touches[0].clientX;
clientY = e.touches[0].clientY;
} else if (e.changedTouches && e.changedTouches.length > 0) {
clientX = e.changedTouches[0].clientX;
clientY = e.changedTouches[0].clientY;
} else {
clientX = e.clientX;
clientY = e.clientY;
}
const scaleX = this.logicalWidth / rect.width;
const scaleY = this.logicalHeight / rect.height;
return {
x: (clientX - rect.left) * scaleX,
y: (clientY - rect.top) * scaleY
};
}
checkTouchCollect(pos) {
if (!this.isRunning) return;
for (let i = this.economy.suns.length - 1; i >= 0; i--) {
const s = this.economy.suns[i];
if (!s.collecting && Math.hypot(pos.x - s.x, pos.y - s.y) <= s.radius + 28) {
s.collecting = true;
this.triggerHaptic(12);
if (window.PvZAudio) window.PvZAudio.playSun();
if (this.economy.workshop) {
this.economy.sunCount = (this.economy.sunCount || 0) + 1;
if (this.economy.sunCount >= 8) {
this.economy.sunCount = 0;
this.economy.workshop.addMaterial('sunEssence', 1);
}
}
}
}
for (let i = this.economy.coins.length - 1; i >= 0; i--) {
const c = this.economy.coins[i];
if (!c.collecting && Math.hypot(pos.x - c.x, pos.y - c.y) <= c.radius + 28) {
c.collecting = true;
this.triggerHaptic(15);
if (window.PvZAudio) window.PvZAudio.playCoin();
}
}
}
bindEvents() {
this.canvas.addEventListener('mousemove', (e) => {
const pos = this.getEventPos(e);
this.hoverCell = this.grid.getCellFromCoords(pos.x, pos.y);
if (this.economy) {
this.economy.handleMouseMove(pos.x, pos.y);
}
if (this.isRunning) {
this.checkTouchCollect(pos);
}
});
this.canvas.addEventListener('mouseleave', () => {
this.hoverCell = null;
if (this.economy) {
this.economy.handleMouseMove(-1, -1);
}
});
this.canvas.addEventListener('click', (e) => {
if (!this.isRunning) return;
const pos = this.getEventPos(e);
const econResult = this.economy.handleClick(pos.x, pos.y);
if (econResult.handled) {
this.triggerHaptic(15);
return;
}
const cell = this.grid.getCellFromCoords(pos.x, pos.y);
if (cell) {
this.handleCellClick(cell.col, cell.row);
this.triggerHaptic(25);
}
});
// Mobile touch with Drag & Drop and continuous swipe-collect
this.canvas.addEventListener('touchstart', (e) => {
if (!this.isRunning) return;
if (this.economy) {
this.economy.handleMouseMove(-1, -1);
}
const pos = this.getEventPos(e);
this.checkTouchCollect(pos);
const econResult = this.economy.handleClick(pos.x, pos.y);
if (econResult.handled) {
this.triggerHaptic(15);
this.dragCard = this.economy.selectedCard;
return;
}
const cell = this.grid.getCellFromCoords(pos.x, pos.y);
if (cell) {
this.hoverCell = cell;
this.handleCellClick(cell.col, cell.row);
this.triggerHaptic(25);
}
}, { passive: false });
this.canvas.addEventListener('touchmove', (e) => {
if (!this.isRunning) return;
e.preventDefault();
const pos = this.getEventPos(e);
this.checkTouchCollect(pos);
this.hoverCell = this.grid.getCellFromCoords(pos.x, pos.y);
}, { passive: false });
this.canvas.addEventListener('touchend', (e) => {
if (!this.isRunning) return;
const pos = this.getEventPos(e);
if (this.dragCard) {
const cell = this.grid.getCellFromCoords(pos.x, pos.y);
if (cell && this.economy.selectedCard === this.dragCard) {
this.handleCellClick(cell.col, cell.row);
this.triggerHaptic(30);
}
this.dragCard = null;
}
this.hoverCell = null;
});
}
bindUI() {
// Mobile / Main Drawer Handlers
const mobileDrawer = document.getElementById('mobile-drawer');
const drawerBackdrop = document.getElementById('drawer-backdrop');
const btnMainMenuDrawer = document.getElementById('btn-main-menu-drawer');
const btnDrawerOpen = document.getElementById('btn-mobile-drawer');
const btnDrawerClose = document.getElementById('btn-drawer-close');
const openDrawer = () => {
if (mobileDrawer) mobileDrawer.classList.remove('hidden');
if (drawerBackdrop) drawerBackdrop.classList.remove('hidden');
this.triggerHaptic(10);
};
const closeDrawer = () => {
if (mobileDrawer) mobileDrawer.classList.add('hidden');
if (drawerBackdrop) drawerBackdrop.classList.add('hidden');
};
if (btnMainMenuDrawer) btnMainMenuDrawer.addEventListener('click', openDrawer);
if (btnDrawerOpen) btnDrawerOpen.addEventListener('click', openDrawer);
if (btnDrawerClose) btnDrawerClose.addEventListener('click', closeDrawer);
if (drawerBackdrop) drawerBackdrop.addEventListener('click', closeDrawer);
// Main Screen action from Drawer
const btnDrawerMain = document.getElementById('btn-drawer-main-screen');
if (btnDrawerMain) {
btnDrawerMain.addEventListener('click', () => {
closeDrawer();
this.openMainMenu();
});
}
// Theme & Music actions from Drawer
const btnDrawerTheme = document.getElementById('btn-drawer-theme');
const drawerThemeLabel = document.getElementById('drawer-theme-label');
if (btnDrawerTheme) {
btnDrawerTheme.addEventListener('click', () => {
const themeNames = { classic: 'Классика', cyberpunk: 'Киберпанк', retro8bit: 'Ретро 8-bit', winter: 'Зима' };
const keys = Object.keys(themeNames);
const nextIndex = (keys.indexOf(this.currentTheme) + 1) % keys.length;
this.applyTheme(keys[nextIndex]);
if (drawerThemeLabel) drawerThemeLabel.textContent = themeNames[this.currentTheme];
this.triggerHaptic(15);
});
}
const btnDrawerMusic = document.getElementById('btn-drawer-music');
const drawerMusicLabel = document.getElementById('drawer-music-label');
if (btnDrawerMusic) {
btnDrawerMusic.addEventListener('click', () => {
const isPlaying = window.PvZAudio ? window.PvZAudio.toggleMusic() : false;
if (drawerMusicLabel) drawerMusicLabel.textContent = isPlaying ? 'Вкл' : 'Выкл';
this.triggerHaptic(15);
});
}
// Drawer Items delegation to corresponding modal open triggers
const drawerActions = [
{ id: 'btn-drawer-campaign', target: 'btn-campaign-open' },
{ id: 'btn-drawer-workshop', target: 'btn-workshop' },
{ id: 'btn-drawer-zengarden', target: 'btn-zengarden' },
{ id: 'btn-drawer-almanac', target: 'btn-almanac' },
{ id: 'btn-drawer-achievements', target: 'btn-achievements' },
{ id: 'btn-drawer-tree', target: 'btn-tree' },
{ id: 'btn-drawer-leaderboard', target: 'btn-leaderboard' },
{ id: 'btn-drawer-bugreport', target: 'btn-bugreport' }
];
drawerActions.forEach(action => {
const btn = document.getElementById(action.id);
if (btn) {
btn.addEventListener('click', () => {
closeDrawer();
const targetBtn = document.getElementById(action.target);
if (targetBtn) targetBtn.click();
});
}
});
// Menu Button (Top Nav)
const btnMenu = document.getElementById('btn-menu');
if (btnMenu) {
btnMenu.addEventListener('click', () => {
this.openMainMenu();
});
}
// Secondary Menu Buttons in Game Over / Victory
const btnMenuLose = document.getElementById('btn-menu-lose');
if (btnMenuLose) {
btnMenuLose.addEventListener('click', () => {
document.getElementById('game-over-modal').classList.add('hidden');
this.openMainMenu();
});
}
const btnMenuWin = document.getElementById('btn-menu-win');
if (btnMenuWin) {
btnMenuWin.addEventListener('click', () => {
document.getElementById('victory-modal').classList.add('hidden');
this.openMainMenu();
});
}
// Speed
const btnSpeed = document.getElementById('btn-speed');
if (btnSpeed) {
btnSpeed.addEventListener('click', () => {
this.gameSpeed = this.gameSpeed === 1.0 ? 2.0 : 1.0;
btnSpeed.textContent = this.gameSpeed === 1.0 ? '⚡ 1x' : '⚡ 2x';
});
}
// Pause Button
const btnPause = document.getElementById('btn-pause');
if (btnPause) {
btnPause.addEventListener('click', () => {
this.togglePause();
});
}
// Fullscreen Toggle
const btnFullscreen = document.getElementById('btn-fullscreen');
const toggleFullscreen = () => {
if (!document.fullscreenElement && !document.webkitFullscreenElement) {
const docEl = document.documentElement;
if (docEl.requestFullscreen) {
docEl.requestFullscreen().catch(() => {});
} else if (docEl.webkitRequestFullscreen) {
docEl.webkitRequestFullscreen();
}
} else {
if (document.exitFullscreen) {
document.exitFullscreen().catch(() => {});
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
}
};
if (btnFullscreen) {
btnFullscreen.addEventListener('click', toggleFullscreen);
}
const updateFullscreenBtn = () => {
if (btnFullscreen) {
const isFs = !!(document.fullscreenElement || document.webkitFullscreenElement);
btnFullscreen.textContent = isFs ? '⛶ Окно' : '⛶ Экран';
}
};
document.addEventListener('fullscreenchange', updateFullscreenBtn);
document.addEventListener('webkitfullscreenchange', updateFullscreenBtn);
// Global Hotkeys (Space/KeyP: Pause; KeyF: Fullscreen; Digit1-Digit6: Deck slots 1-6; KeyS: Shovel)
window.addEventListener('keydown', (e) => {
// Do not intercept hotkeys if user is typing in an input or textarea
if (['INPUT', 'TEXTAREA', 'SELECT'].includes(document.activeElement?.tagName)) return;
if (e.code === 'Space' || e.code === 'KeyP') {
e.preventDefault();
if (this.isRunning) {
this.togglePause();
}
} else if (e.code === 'KeyF') {
e.preventDefault();
toggleFullscreen();
} else if (e.code === 'KeyS' || e.key === 's' || e.key === 'S' || e.key === 'ы' || e.key === 'Ы') {
e.preventDefault();
if (this.isRunning && !this.isPaused) {
this.economy.selectedCard = (this.economy.selectedCard === 'shovel') ? null : 'shovel';
if (window.PvZAudio) window.PvZAudio.playButtonClick();
this.triggerHaptic(15);
}
} else if (['Digit1', 'Digit2', 'Digit3', 'Digit4', 'Digit5', 'Digit6', 'Numpad1', 'Numpad2', 'Numpad3', 'Numpad4', 'Numpad5', 'Numpad6'].includes(e.code) || (e.key >= '1' && e.key <= '6')) {
const num = parseInt(e.key, 10) || parseInt(e.code.replace(/\D/g, ''), 10);
const slotIdx = num - 1;
if (this.isRunning && !this.isPaused && this.economy.cards && slotIdx >= 0 && slotIdx < this.economy.cards.length) {
e.preventDefault();
const card = this.economy.cards[slotIdx];
if (card) {
if (this.economy.selectedCard === card.id) {
this.economy.selectedCard = null;
} else if (this.economy.canAfford(card.cost) && card.timer <= 0) {
this.economy.selectedCard = card.id;
if (window.PvZAudio) window.PvZAudio.playButtonClick();
this.triggerHaptic(15);
}
}
}
}
});
// Music
const btnMusic = document.getElementById('btn-music');
if (btnMusic) {
btnMusic.addEventListener('click', () => {
const isPlaying = window.PvZAudio.toggleMusic();
btnMusic.textContent = isPlaying ? '🎵 Музыка: Вкл' : '🎵 Музыка: Выкл';
});
}
// Audio SFX
const btnMute = document.getElementById('btn-mute');
if (btnMute) {
btnMute.addEventListener('click', () => {
const isMuted = window.PvZAudio.toggleMute();
btnMute.textContent = isMuted ? '🔇 Вкл звук' : '🔊 Звук';
});
}
// Restart Lose
const btnRestartLose = document.getElementById('btn-restart-lose');
if (btnRestartLose) {
btnRestartLose.addEventListener('click', () => {
this.startLevel(this.currentLevel, this.currentMode);
});
}
// Next Level / Win
const btnRestartWin = document.getElementById('btn-restart-win');
if (btnRestartWin) {
btnRestartWin.addEventListener('click', () => {
const victoryModal = document.getElementById('victory-modal');
if (victoryModal) victoryModal.classList.add('hidden');
this.showDaveDialogue(this.currentLevel, this.currentMode, () => {
if (this.currentMode === 'bowling' || this.currentMode === 'izombie' || this.currentMode === 'boss' || this.currentMode === 'bossrush' || (this.currentMode === 'adventure' && this.currentLevel === 25)) {
this.startLevel(this.currentLevel, this.currentMode);
} else {
this.openSeedSelectionModal(this.currentLevel, this.currentMode);
}
});
});
}
// Auth & Profile Modal Handlers
const authModal = document.getElementById('auth-modal');
const btnAuthOpen = document.getElementById('btn-auth-open');
const btnAuthClose = document.getElementById('btn-auth-close');
const btnAuthCloseProfile = document.getElementById('btn-auth-close-profile');
const btnAuthCloseReg = document.getElementById('btn-auth-close-reg');
const loginView = document.getElementById('auth-login-view') || document.getElementById('auth-guest-view');
const registerView = document.getElementById('auth-register-view');
const btnAuthToRegister = document.getElementById('btn-auth-to-register');
const btnAuthToLogin = document.getElementById('btn-auth-to-login');
const btnAuthSubmit = document.getElementById('btn-auth-submit'); // Войти
const btnAuthDoRegister = document.getElementById('btn-auth-do-register'); // Зарегистрироваться
const btnAuthGuest = document.getElementById('btn-auth-guest'); // Остаться гостем
const btnAuthLogout = document.getElementById('btn-auth-logout'); // Выйти
const btnAuthLogoutNav = document.getElementById('btn-auth-logout-nav');
const authUser = document.getElementById('auth-username');
const authPass = document.getElementById('auth-password');
const authError = document.getElementById('auth-error');
const authRegUser = document.getElementById('auth-reg-username');
const authRegPass = document.getElementById('auth-reg-password');
const authRegError = document.getElementById('auth-reg-error');
const resetAuthForms = () => {
if (authUser) authUser.value = '';
if (authPass) authPass.value = '';
if (authError) { authError.textContent = ''; authError.classList.add('hidden'); }
if (authRegUser) authRegUser.value = '';
if (authRegPass) authRegPass.value = '';
if (authRegError) { authRegError.textContent = ''; authRegError.classList.add('hidden'); }
};
const handleLogout = () => {
if (window.PvZAuth) {
window.PvZAuth.logout();
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
}
if (authModal) authModal.classList.add('hidden');
resetAuthForms();
if (window.PvZAudio) window.PvZAudio.playCoin();
};
if (btnAuthLogoutNav) {
btnAuthLogoutNav.addEventListener('click', handleLogout);
}
if (btnAuthLogout) {
btnAuthLogout.addEventListener('click', handleLogout);
}
if (btnAuthOpen && authModal) {
btnAuthOpen.addEventListener('click', () => {
resetAuthForms();
this.updateProfileUI();
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();
});
}
});
}
const startHeroProfile = document.getElementById('start-hero-profile');
if (startHeroProfile && btnAuthOpen) {
startHeroProfile.addEventListener('click', () => {
btnAuthOpen.click();
});
}
// Modal Close buttons
if (btnAuthClose && authModal) {
btnAuthClose.addEventListener('click', () => {
authModal.classList.add('hidden');
resetAuthForms();
});
}
if (btnAuthCloseProfile && authModal) {
btnAuthCloseProfile.addEventListener('click', () => {
authModal.classList.add('hidden');
});
}
if (btnAuthCloseReg && authModal) {
btnAuthCloseReg.addEventListener('click', () => {
authModal.classList.add('hidden');
resetAuthForms();
});
}
// Switch between Login and Register views
if (btnAuthToRegister && loginView && registerView) {
btnAuthToRegister.addEventListener('click', () => {
resetAuthForms();
loginView.classList.add('hidden');
registerView.classList.remove('hidden');
if (authRegUser) setTimeout(() => authRegUser.focus(), 50);
});
}
if (btnAuthToLogin && loginView && registerView) {
btnAuthToLogin.addEventListener('click', () => {
resetAuthForms();
registerView.classList.add('hidden');
loginView.classList.remove('hidden');
if (authUser) setTimeout(() => authUser.focus(), 50);
});
}
// Stay as Guest button
if (btnAuthGuest && authModal) {
btnAuthGuest.addEventListener('click', () => {
if (window.PvZAuth) window.PvZAuth.loginAsGuest();
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
authModal.classList.add('hidden');
resetAuthForms();
if (window.PvZAudio) window.PvZAudio.playButtonClick();
});
}
// Login Action (Войти)
if (btnAuthSubmit && authModal) {
btnAuthSubmit.addEventListener('click', async () => {
const u = authUser ? authUser.value.trim() : '';
const p = authPass ? authPass.value : '';
if (!u) {
if (authError) { authError.textContent = 'Введите логин'; authError.classList.remove('hidden'); }
return;
}
if (!p) {
if (authError) { authError.textContent = 'Введите пароль'; authError.classList.remove('hidden'); }
return;
}
if (authError) authError.classList.add('hidden');
if (window.PvZAuth) {
btnAuthSubmit.textContent = '⏳ Вход...';
btnAuthSubmit.disabled = true;
const res = await window.PvZAuth.login(u, p);
btnAuthSubmit.textContent = '🔑 Войти';
btnAuthSubmit.disabled = false;
if (res && res.success) {
authModal.classList.add('hidden');
resetAuthForms();
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
if (window.PvZAudio) window.PvZAudio.playCoin();
} else {
if (authError) {
authError.textContent = (res && res.message) ? res.message : 'Неверный логин или пароль';
authError.classList.remove('hidden');
}
}
}
});
}
// Register Action (Зарегистрироваться)
if (btnAuthDoRegister && authModal) {
btnAuthDoRegister.addEventListener('click', async () => {
const u = authRegUser ? authRegUser.value.trim() : '';
const p = authRegPass ? authRegPass.value : '';
if (!u) {
if (authRegError) { authRegError.textContent = 'Введите логин для регистрации'; authRegError.classList.remove('hidden'); }
return;
}
if (u.length < 3) {
if (authRegError) { authRegError.textContent = 'Логин должен содержать минимум 3 символа'; authRegError.classList.remove('hidden'); }
return;
}
if (!p || p.length < 4) {
if (authRegError) { authRegError.textContent = 'Пароль должен содержать минимум 4 символа'; authRegError.classList.remove('hidden'); }
return;
}
if (authRegError) authRegError.classList.add('hidden');
if (window.PvZAuth) {
btnAuthDoRegister.textContent = '⏳ Регистрация...';
btnAuthDoRegister.disabled = true;
const res = await window.PvZAuth.register(u, p);
btnAuthDoRegister.textContent = '✨ Зарегистрироваться';
btnAuthDoRegister.disabled = false;
if (res && res.success) {
authModal.classList.add('hidden');
resetAuthForms();
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
if (window.PvZAudio) window.PvZAudio.playCoin();
} else {
if (authRegError) {
authRegError.textContent = (res && res.message) ? res.message : 'Ошибка при регистрации';
authRegError.classList.remove('hidden');
}
}
}
});
}
// Export / Import Save File Handlers
const btnExportSave = document.getElementById('btn-export-save');
const btnExportSaveProfile = document.getElementById('btn-export-save-profile');
const btnImportSave = document.getElementById('btn-import-save');
const btnImportSaveProfile = document.getElementById('btn-import-save-profile');
const inputImportSave = document.getElementById('input-import-save');
const handleExportSave = () => {
this.saveUserData();
if (window.PvZAuth && window.PvZAuth.exportSaveFile) {
window.PvZAuth.exportSaveFile();
}
};
if (btnExportSave) btnExportSave.addEventListener('click', handleExportSave);
if (btnExportSaveProfile) btnExportSaveProfile.addEventListener('click', handleExportSave);
const handleTriggerImport = () => {
if (inputImportSave) {
inputImportSave.value = '';
inputImportSave.click();
}
};
if (btnImportSave) btnImportSave.addEventListener('click', handleTriggerImport);
if (btnImportSaveProfile) btnImportSaveProfile.addEventListener('click', handleTriggerImport);
if (inputImportSave) {
inputImportSave.addEventListener('change', async (e) => {
const file = e.target.files && e.target.files[0];
if (!file) return;
if (window.PvZAuth && window.PvZAuth.importSaveFile) {
const res = await window.PvZAuth.importSaveFile(file);
if (res && res.success) {
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
}
}
inputImportSave.value = '';
});
}
// 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') {
this.saveUserData();
if (window.PvZAuth && window.PvZAuth.flushSave) {
window.PvZAuth.flushSave();
}
}
});
window.addEventListener('beforeunload', () => {
this.saveUserData();
if (window.PvZAuth && window.PvZAuth.flushSave) {
window.PvZAuth.flushSave(true);
}
});
// Inventory / Deck Builder Buttons
const btnInvStart = document.getElementById('btn-inventory-start');
const btnDrawerInv = document.getElementById('btn-drawer-inventory');
if (btnInvStart) {
btnInvStart.addEventListener('click', () => {
this.openSeedSelectionModal(this.currentLevel || 1, this.currentMode || 'adventure');
});
}
if (btnDrawerInv) {
btnDrawerInv.addEventListener('click', () => {
const drawer = document.getElementById('mobile-drawer');
const backdrop = document.getElementById('drawer-backdrop');
if (drawer) drawer.classList.add('hidden');
if (backdrop) backdrop.classList.add('hidden');
this.openSeedSelectionModal(this.currentLevel || 1, this.currentMode || 'adventure');
});
}
// Shop Button & Tabs
const btnShop = document.getElementById('btn-shop');
const shopModal = document.getElementById('shop-modal');
const btnCloseShop = document.getElementById('btn-close-shop');
const tabShopPlants = document.getElementById('tab-shop-plants');
const tabShopUpgrades = document.getElementById('tab-shop-upgrades');
const panelShopPlants = document.getElementById('shop-plants-panel');
const panelShopUpgrades = document.getElementById('shop-upgrades-panel');
if (btnShop && shopModal) {
btnShop.addEventListener('click', () => {
this.updateShopUI();
shopModal.classList.remove('hidden');
});
}
if (btnCloseShop && shopModal) {
btnCloseShop.addEventListener('click', () => {
shopModal.classList.add('hidden');
this.updateRecordDisplay();
});
}
if (tabShopPlants && tabShopUpgrades && panelShopPlants && panelShopUpgrades) {
tabShopPlants.addEventListener('click', () => {
tabShopPlants.classList.add('active');
tabShopUpgrades.classList.remove('active');
panelShopPlants.classList.remove('hidden');
panelShopUpgrades.classList.add('hidden');
});
tabShopUpgrades.addEventListener('click', () => {
tabShopUpgrades.classList.add('active');
tabShopPlants.classList.remove('active');
panelShopUpgrades.classList.remove('hidden');
panelShopPlants.classList.add('hidden');
});
}
// Pre-Battle Seed Selection & Inventory Modal Handlers
const seedSelectModal = document.getElementById('seed-select-modal');
const btnSeedStart = document.getElementById('btn-seed-start-battle');
const btnSeedAuto = document.getElementById('btn-seed-auto-fill');
const btnSeedShop = document.getElementById('btn-seed-open-shop');
const btnSeedCancel = document.getElementById('btn-seed-cancel');
if (btnSeedStart) {
btnSeedStart.addEventListener('click', () => {
if (!this.economy.selectedDeck || this.economy.selectedDeck.length === 0) {
this.economy.autoFillDeck();
}
if (seedSelectModal) seedSelectModal.classList.add('hidden');
const startModal = document.getElementById('start-modal');
if (startModal) startModal.classList.add('hidden');
if (window.PvZAudio) window.PvZAudio.init();
const lvl = this.pendingLevel || this.currentLevel || 1;
const mode = this.pendingMode || this.currentMode || 'adventure';
this.startLevel(lvl, mode);
if (!this.loopStarted) {
this.loopStarted = true;
requestAnimationFrame(this.loop.bind(this));
}
});
}
if (btnSeedAuto) {
btnSeedAuto.addEventListener('click', () => {
this.economy.autoFillDeck();
this.renderSeedSelectionUI();
if (window.PvZAudio) window.PvZAudio.playButtonClick();
});
}
if (btnSeedShop) {
btnSeedShop.addEventListener('click', () => {
if (seedSelectModal) seedSelectModal.classList.add('hidden');
this.updateShopUI();
if (shopModal) shopModal.classList.remove('hidden');
});
}
if (btnSeedCancel) {
btnSeedCancel.addEventListener('click', () => {
if (seedSelectModal) seedSelectModal.classList.add('hidden');
const startModal = document.getElementById('start-modal');
if (startModal) startModal.classList.remove('hidden');
});
}
// Shop Item Purchases
const btnBuySlot = document.getElementById('btn-buy-slot');
if (btnBuySlot) {
btnBuySlot.addEventListener('click', () => {
if (this.economy.buyUpgrade('extraSlot')) {
if (window.PvZAudio) window.PvZAudio.playWin();
this.saveUserData();
this.updateShopUI();
this.renderSeedSelectionUI();
}
});
}
const btnBuyRake = document.getElementById('btn-buy-rake');
if (btnBuyRake) {
btnBuyRake.addEventListener('click', () => {
if (this.economy.buyUpgrade('rake')) {
if (window.PvZAudio) window.PvZAudio.playWin();
this.saveUserData();
this.updateShopUI();
}
});
}
// Workshop Modal Handlers
const workshopModal = document.getElementById('workshop-modal');
const btnWorkshopNav = document.getElementById('btn-workshop');
const btnWorkshopStart = document.getElementById('btn-workshop-start');
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();
if (workshopModal) workshopModal.classList.remove('hidden');
};
if (btnWorkshopNav) btnWorkshopNav.addEventListener('click', openWorkshop);
if (btnWorkshopStart) btnWorkshopStart.addEventListener('click', openWorkshop);
if (btnCloseWorkshop && workshopModal) {
btnCloseWorkshop.addEventListener('click', () => {
workshopModal.classList.add('hidden');
this.updateRecordDisplay();
});
}
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();
});
}
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 = `${res.icons[0]}`;
if (reel2) reel2.innerHTML = `${res.icons[1]}`;
if (reel3) reel3.innerHTML = `${res.icons[2]}`;
}
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();
});
}
// Zen Garden Modal Handlers
const zenModal = document.getElementById('zengarden-modal');
const btnZenNav = document.getElementById('btn-zengarden');
const btnZenStart = document.getElementById('btn-zengarden-start');
const btnCloseZen = document.getElementById('btn-close-zengarden');
const openZenGarden = () => {
if (this.zengarden) {
this.zengarden.calculateOfflineEarnings();
}
this.updateZenGardenUI();
if (zenModal) zenModal.classList.remove('hidden');
};
if (btnZenNav) btnZenNav.addEventListener('click', openZenGarden);
if (btnZenStart) btnZenStart.addEventListener('click', openZenGarden);
if (btnCloseZen && zenModal) {
btnCloseZen.addEventListener('click', () => {
zenModal.classList.add('hidden');
this.updateRecordDisplay();
});
}
// Suburban Almanac Modal Handlers
const almanacModal = document.getElementById('almanac-modal');
const btnAlmanacNav = document.getElementById('btn-almanac');
const btnAlmanacStart = document.getElementById('btn-almanac-start');
const btnCloseAlmanac = document.getElementById('btn-close-almanac');
const openAlmanac = () => {
this.renderAlmanacUI();
if (almanacModal) almanacModal.classList.remove('hidden');
};
if (btnAlmanacNav) btnAlmanacNav.addEventListener('click', openAlmanac);
if (btnAlmanacStart) btnAlmanacStart.addEventListener('click', openAlmanac);
if (btnCloseAlmanac && almanacModal) {
btnCloseAlmanac.addEventListener('click', () => {
almanacModal.classList.add('hidden');
});
}
// Achievements Modal Handlers
const achModal = document.getElementById('achievements-modal');
const btnAchNav = document.getElementById('btn-achievements');
const btnAchStart = document.getElementById('btn-achievements-start');
const btnCloseAch = document.getElementById('btn-close-achievements');
const openAchievements = () => {
this.renderAchievementsUI();
if (achModal) achModal.classList.remove('hidden');
};
if (btnAchNav) btnAchNav.addEventListener('click', openAchievements);
if (btnAchStart) btnAchStart.addEventListener('click', openAchievements);
if (btnCloseAch && achModal) {
btnCloseAch.addEventListener('click', () => {
achModal.classList.add('hidden');
});
}
// Bug Reporting Modal Handlers
const bugModal = document.getElementById('bugreport-modal');
const btnBugNav = document.getElementById('btn-bugreport');
const btnBugStart = document.getElementById('btn-bugreport-start');
const btnCloseBug = document.getElementById('btn-bug-close');
const openBugReport = () => {
if (this.authSystem && this.authSystem.currentUser) {
const nameInput = document.getElementById('bug-input-name');
if (nameInput && !nameInput.value) {
nameInput.value = this.authSystem.currentUser.username;
}
}
if (window.PvZFeedback) {
window.PvZFeedback.fetchReports();
}
if (bugModal) bugModal.classList.remove('hidden');
};
if (btnBugNav) btnBugNav.addEventListener('click', openBugReport);
if (btnBugStart) btnBugStart.addEventListener('click', openBugReport);
if (btnCloseBug && bugModal) {
btnCloseBug.addEventListener('click', () => {
bugModal.classList.add('hidden');
});
}
// Initialize Zen Garden & Almanac & Feedback DOM bindings
this.initZenGarden();
this.initAlmanac();
this.initFeedbackUI();
// Mode Buttons on Start Screen
const btnModeAdv = document.getElementById('btn-mode-adv');
if (btnModeAdv) {
btnModeAdv.addEventListener('click', () => {
this.currentMode = 'adventure';
this.highlightModeBtn('btn-mode-adv');
this.updateRecordDisplay();
});
}
const btnModeSurv = document.getElementById('btn-mode-surv');
if (btnModeSurv) {
btnModeSurv.addEventListener('click', () => {
this.currentMode = 'survival';
this.highlightModeBtn('btn-mode-surv');
this.updateRecordDisplay();
});
}
const btnModeBowl = document.getElementById('btn-mode-bowl');
if (btnModeBowl) {
btnModeBowl.addEventListener('click', () => {
this.currentMode = 'bowling';
this.highlightModeBtn('btn-mode-bowl');
this.updateRecordDisplay();
});
}
const btnModeIzombie = document.getElementById('btn-mode-izombie');
if (btnModeIzombie) {
btnModeIzombie.addEventListener('click', () => {
this.currentMode = 'izombie';
this.highlightModeBtn('btn-mode-izombie');
this.updateRecordDisplay();
});
}
const btnModeRogue = document.getElementById('btn-mode-rogue');
if (btnModeRogue) {
btnModeRogue.addEventListener('click', () => {
this.currentMode = 'roguelike';
if (this.roguelike) this.roguelike.reset();
this.highlightModeBtn('btn-mode-rogue');
this.updateRecordDisplay();
});
}
const btnModeBoss = document.getElementById('btn-mode-boss');
if (btnModeBoss) {
btnModeBoss.addEventListener('click', () => {
this.currentMode = 'boss';
this.highlightModeBtn('btn-mode-boss');
this.updateRecordDisplay();
});
}
const btnModeBossRush = document.getElementById('btn-mode-bossrush');
if (btnModeBossRush) {
btnModeBossRush.addEventListener('click', () => {
this.currentMode = 'bossrush';
this.highlightModeBtn('btn-mode-bossrush');
this.updateRecordDisplay();
});
}
// Theme Switcher Button
const btnTheme = document.getElementById('btn-theme');
if (btnTheme) {
btnTheme.addEventListener('click', () => {
this.cycleTheme();
});
}
// Tree of Wisdom Modal Handlers
const treeModal = document.getElementById('tree-modal');
const btnTreeNav = document.getElementById('btn-tree');
const btnTreeStart = document.getElementById('btn-tree-start');
const btnCloseTree = document.getElementById('btn-tree-close');
const btnTreeFeed = document.getElementById('btn-tree-feed');
const openTree = () => {
this.renderTreeUI();
if (treeModal) treeModal.classList.remove('hidden');
};
if (btnTreeNav) btnTreeNav.addEventListener('click', openTree);
if (btnTreeStart) btnTreeStart.addEventListener('click', openTree);
if (btnCloseTree && treeModal) {
btnCloseTree.addEventListener('click', () => {
treeModal.classList.add('hidden');
});
}
if (btnTreeFeed) {
btnTreeFeed.addEventListener('click', () => {
if (this.tree && this.tree.feedFertilizer()) {
this.renderTreeUI();
this.updateRecordDisplay();
this.updateShopUI();
}
});
}
// Leaderboards Modal Handlers
const lbModal = document.getElementById('leaderboard-modal');
const btnLbNav = document.getElementById('btn-leaderboard');
const btnLbStart = document.getElementById('btn-leaderboard-start');
const btnCloseLb = document.getElementById('btn-leaderboard-close');
const openLeaderboard = (mode = 'survival') => {
this.renderLeaderboardUI(mode);
if (lbModal) lbModal.classList.remove('hidden');
};
if (btnLbNav) btnLbNav.addEventListener('click', () => openLeaderboard('survival'));
if (btnLbStart) btnLbStart.addEventListener('click', () => openLeaderboard('survival'));
if (btnCloseLb && lbModal) {
btnCloseLb.addEventListener('click', () => {
lbModal.classList.add('hidden');
});
}
const lbTabs = [
{ id: 'tab-lb-survival', mode: 'survival' },
{ id: 'tab-lb-rogue', mode: 'roguelike' },
{ id: 'tab-lb-adv', mode: 'adventure' },
{ id: 'tab-lb-izombie', mode: 'izombie' },
{ id: 'tab-lb-bowl', mode: 'bowling' },
{ id: 'tab-lb-boss', mode: 'boss' },
{ id: 'tab-lb-bossrush', mode: 'bossrush' }
];
lbTabs.forEach(t => {
const el = document.getElementById(t.id);
if (el) {
el.addEventListener('click', () => {
lbTabs.forEach(other => {
const oEl = document.getElementById(other.id);
if (oEl) oEl.classList.remove('active');
});
el.classList.add('active');
this.renderLeaderboardUI(t.mode);
});
}
});
// Campaign Map Modal Handlers
const campaignModal = document.getElementById('campaign-modal');
const btnCampaignNav = document.getElementById('btn-campaign-open');
const btnCampaignStart = document.getElementById('btn-campaign-select');
const btnCloseCampaign = document.getElementById('btn-campaign-close');
if (btnCampaignNav) btnCampaignNav.addEventListener('click', () => this.openCampaignModal());
if (btnCampaignStart) btnCampaignStart.addEventListener('click', () => this.openCampaignModal());
if (btnCloseCampaign && campaignModal) {
btnCloseCampaign.addEventListener('click', () => {
campaignModal.classList.add('hidden');
this.updateRecordDisplay();
});
}
for (let w = 1; w <= 5; w++) {
const tab = document.getElementById(`tab-world-${w}`);
if (tab) {
tab.addEventListener('click', () => {
this.renderCampaignMapUI(w);
});
}
}
}
openCampaignModal(worldId = null) {
if (!worldId) {
if (window.CampaignSystem) {
const currentItem = window.CampaignSystem.getLevel(this.currentLevel);
worldId = currentItem ? currentItem.world : 1;
} else {
worldId = 1;
}
}
this.renderCampaignMapUI(worldId);
const modal = document.getElementById('campaign-modal');
if (modal) modal.classList.remove('hidden');
}
renderCampaignMapUI(activeWorldId = 1) {
const gridEl = document.getElementById('campaign-levels-grid');
if (!gridEl || !window.CampaignSystem) return;
// Update active tab styling
for (let w = 1; w <= 5; w++) {
const tab = document.getElementById(`tab-world-${w}`);
if (tab) {
if (w === activeWorldId) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
}
}
const levels = window.CampaignSystem.getLevelsByWorld(activeWorldId);
const world = window.CampaignSystem.getWorld(activeWorldId);
let html = '';
levels.forEach(lvl => {
const isPassed = lvl.level < this.currentLevel;
const isCurrent = lvl.level === this.currentLevel;
const isUnlocked = lvl.level <= (this.maxUnlockedLevel || 1);
let badgeClass = 'badge-locked';
let badgeText = '🔒 Заблокировано';
if (isCurrent) {
badgeClass = 'badge-current';
badgeText = '⚔️ Текущий';
} else if (isPassed) {
badgeClass = 'badge-done';
badgeText = '⭐ Пройдено';
} else if (isUnlocked) {
badgeClass = 'badge-unlocked';
badgeText = '🔓 Доступно';
}
const cardClass = `campaign-level-card${isCurrent ? ' current' : ''}${!isUnlocked ? ' locked' : ''}`;
const buttonHtml = isUnlocked
? ``
: ``;
const biomeNames = {
day: '☀️ День',
night: '🌙 Ночь',
pool: '🏊 Бассейн (6 линий)',
fog: '🌫️ Туман (6 линий)',
roof: '🏠 Крыша'
};
html += `
🌊 Волн: ${lvl.waves}
💰 Награда: +${lvl.reward}
🏞️ ${biomeNames[lvl.biome] || lvl.biome}
${buttonHtml}
`;
});
gridEl.innerHTML = html;
// Attach click listeners to level launch buttons
gridEl.querySelectorAll('.btn-play-level[data-level]').forEach(btn => {
btn.addEventListener('click', (e) => {
const targetLevel = parseInt(e.currentTarget.getAttribute('data-level'), 10);
if (targetLevel) {
const campModal = document.getElementById('campaign-modal');
if (campModal) campModal.classList.add('hidden');
const startModal = document.getElementById('start-modal');
if (startModal) startModal.classList.add('hidden');
if (window.PvZAudio) window.PvZAudio.init();
this.showDaveDialogue(targetLevel, 'adventure', () => {
if (targetLevel === 25) {
this.startLevel(targetLevel, 'adventure');
} else {
this.openSeedSelectionModal(targetLevel, 'adventure');
}
if (!this.loopStarted) {
this.loopStarted = true;
requestAnimationFrame(this.loop.bind(this));
}
});
}
});
});
}
highlightModeBtn(activeId) {
['btn-mode-adv', 'btn-mode-surv', 'btn-mode-bowl', 'btn-mode-izombie', 'btn-mode-rogue', 'btn-mode-boss', 'btn-mode-bossrush'].forEach(id => {
const btn = document.getElementById(id);
if (btn) {
const isActive = id === activeId;
btn.classList.toggle('active', isActive);
btn.style.borderColor = isActive ? '#ffeb3b' : '';
btn.style.opacity = isActive ? '1' : '0.7';
}
});
}
openSeedSelectionModal(targetLevel = null, targetMode = null) {
this.pendingLevel = targetLevel !== null ? targetLevel : (this.currentLevel || 1);
this.pendingMode = targetMode !== null ? targetMode : (this.currentMode || 'adventure');
this.renderSeedSelectionUI();
const modal = document.getElementById('seed-select-modal');
if (modal) modal.classList.remove('hidden');
const startModal = document.getElementById('start-modal');
if (startModal) startModal.classList.add('hidden');
const campaignModal = document.getElementById('campaign-modal');
if (campaignModal) campaignModal.classList.add('hidden');
}
renderSeedSelectionUI() {
const activeDeckContainer = document.getElementById('seed-active-deck');
const inventoryContainer = document.getElementById('seed-inventory-grid');
const deckCountEl = document.getElementById('seed-deck-count');
const deckMaxEl = document.getElementById('seed-deck-max');
const maxSlots = this.economy.getMaxSlots();
const selectedDeck = this.economy.selectedDeck || [];
if (deckCountEl) deckCountEl.textContent = selectedDeck.length;
if (deckMaxEl) deckMaxEl.textContent = maxSlots;
// 1. Render Active Deck Slots
if (activeDeckContainer) {
activeDeckContainer.innerHTML = '';
selectedDeck.forEach(plantId => {
const p = window.PLANT_CATALOGUE.find(item => item.id === plantId);
if (!p) return;
const slot = document.createElement('div');
slot.className = 'seed-slot-card';
slot.title = `Нажмите, чтобы убрать ${p.name} из колоды`;
slot.innerHTML = `
✕
${p.icon}
☀️ ${p.cost}
`;
slot.addEventListener('click', () => {
this.economy.removeFromDeck(plantId);
this.renderSeedSelectionUI();
if (window.PvZAudio) window.PvZAudio.playButtonClick();
});
activeDeckContainer.appendChild(slot);
});
// Render Empty Slots up to maxSlots
for (let i = selectedDeck.length; i < maxSlots; i++) {
const emptySlot = document.createElement('div');
emptySlot.className = 'seed-slot-empty';
emptySlot.textContent = `+ Слот ${i + 1}`;
activeDeckContainer.appendChild(emptySlot);
}
}
// 2. Render Inventory Grid
if (inventoryContainer && window.PLANT_CATALOGUE) {
inventoryContainer.innerHTML = '';
window.PLANT_CATALOGUE.forEach(p => {
const isUnlocked = this.economy.isPlantUnlocked(p.id);
const isInDeck = selectedDeck.includes(p.id);
const canAfford = this.economy.totalCoins >= (p.price || 0);
const item = document.createElement('div');
item.className = `seed-inv-item ${isUnlocked ? (isInDeck ? 'in-deck' : 'unlocked') : 'locked'}`;
let statusBadge = '';
if (isInDeck) {
statusBadge = '✓ В колоде';
} else if (isUnlocked) {
statusBadge = '+ Выбрать';
} else {
statusBadge = ``;
}
item.innerHTML = `
${p.icon}
${p.name}
☀️ ${p.cost}
${statusBadge}
`;
if (isUnlocked) {
item.addEventListener('click', () => {
if (isInDeck) {
this.economy.removeFromDeck(p.id);
} else {
this.economy.addToDeck(p.id);
}
this.renderSeedSelectionUI();
if (window.PvZAudio) window.PvZAudio.playButtonClick();
});
} else {
const buyBtn = item.querySelector('button');
if (buyBtn) {
buyBtn.addEventListener('click', (e) => {
e.stopPropagation();
const res = this.economy.buyPlant(p.id);
if (res.success) {
if (window.PvZAudio) window.PvZAudio.playCoin();
this.saveUserData();
this.updateShopUI();
this.renderSeedSelectionUI();
}
});
}
}
inventoryContainer.appendChild(item);
});
}
}
updateShopUI() {
const elCoins = document.getElementById('shop-coins');
if (elCoins) elCoins.textContent = `💰 Монеты: ${this.economy.totalCoins.toLocaleString('ru-RU')}`;
const btnBuySlot = document.getElementById('btn-buy-slot');
if (btnBuySlot) {
btnBuySlot.textContent = this.economy.upgrades.extraSlot ? 'Куплено ✔️' : 'Купить (500 💰)';
btnBuySlot.disabled = this.economy.upgrades.extraSlot || this.economy.totalCoins < 500;
}
const btnBuyRake = document.getElementById('btn-buy-rake');
if (btnBuyRake) {
btnBuyRake.textContent = this.economy.upgrades.hasRake ? 'Куплено ✔️' : 'Купить (150 💰)';
btnBuyRake.disabled = this.economy.upgrades.hasRake || this.economy.totalCoins < 150;
}
// Populate Shop Plants Grid
const shopPlantsGrid = document.getElementById('shop-plants-grid');
if (shopPlantsGrid && window.PLANT_CATALOGUE) {
shopPlantsGrid.innerHTML = '';
window.PLANT_CATALOGUE.forEach(p => {
const isBasic = this.economy.defaultUnlocked.includes(p.id);
const isUnlocked = this.economy.isPlantUnlocked(p.id);
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';
let actionHtml = '';
if (isBasic) {
actionHtml = 'Базовое ✅';
} else if (isUnlocked) {
actionHtml = 'Куплено ✅';
} else {
actionHtml = ``;
}
card.innerHTML = `
${p.icon}
${p.name}
☀️ ${sunCost} солнца
${p.desc}
${actionHtml}
`;
const buyBtn = card.querySelector('button.btn-buy');
if (buyBtn) {
buyBtn.addEventListener('click', () => {
const res = this.economy.buyPlant(p.id);
if (res.success) {
if (window.PvZAudio) window.PvZAudio.playCoin();
this.saveUserData();
this.updateShopUI();
this.renderSeedSelectionUI();
}
});
}
shopPlantsGrid.appendChild(card);
});
}
}
updateWorkshopUI() {
if (!this.workshop) return;
// 1. Materials bar
const matBio = document.getElementById('mat-bio');
const matScrap = document.getElementById('mat-scrap');
const matSun = document.getElementById('mat-sun');
const matCrystal = document.getElementById('mat-crystal');
const matCoins = document.getElementById('mat-coins');
if (matBio) matBio.textContent = this.workshop.materials.bioHumus;
if (matScrap) matScrap.textContent = this.workshop.materials.scrapMetal;
if (matSun) matSun.textContent = this.workshop.materials.sunEssence;
if (matCrystal) matCrystal.textContent = this.workshop.materials.crystals;
if (matCoins) matCoins.textContent = this.economy ? this.economy.totalCoins : 0;
// 2. Plants Upgrades List
const plantsContainer = document.getElementById('plants-upgrade-list');
if (plantsContainer) {
plantsContainer.innerHTML = '';
const pDefs = this.workshop.plantDefs;
for (const [id, def] of Object.entries(pDefs)) {
const lvl = this.workshop.getPlantLevel(id);
const curTier = def.tiers[lvl];
const nextTier = lvl < 3 ? def.tiers[lvl + 1] : null;
const card = document.createElement('div');
card.className = 'upgrade-card';
let costHtml = '';
let canAfford = false;
if (nextTier) {
canAfford = this.workshop.canAffordCost(nextTier.cost);
const parts = [];
if (nextTier.cost.coins) {
const hasCoins = this.economy && this.economy.totalCoins >= nextTier.cost.coins;
parts.push(`💰 ${nextTier.cost.coins}`);
}
if (nextTier.cost.bioHumus) {
const hasBio = this.workshop.materials.bioHumus >= nextTier.cost.bioHumus;
parts.push(`🧪 ${nextTier.cost.bioHumus}`);
}
if (nextTier.cost.scrapMetal) {
const hasScrap = this.workshop.materials.scrapMetal >= nextTier.cost.scrapMetal;
parts.push(`⚙️ ${nextTier.cost.scrapMetal}`);
}
if (nextTier.cost.sunEssence) {
const hasSun = this.workshop.materials.sunEssence >= nextTier.cost.sunEssence;
parts.push(`☀️ ${nextTier.cost.sunEssence}`);
}
if (nextTier.cost.crystals) {
const hasCryst = this.workshop.materials.crystals >= nextTier.cost.crystals;
parts.push(`💎 ${nextTier.cost.crystals}`);
}
costHtml = parts.join(' ');
}
const stars = lvl === 3 ? '★★★' : (lvl === 2 ? '★★☆' : '★☆☆');
const badgeClass = lvl === 3 ? 'upgrade-level-badge maxed' : 'upgrade-level-badge';
card.innerHTML = `
Текущий эффект: ${curTier.desc}
${nextTier ? `
След. ранг: ${nextTier.title} — ${nextTier.desc}
` : `
Максимальный 3-й уровень разблокирован!
`}
`;
plantsContainer.appendChild(card);
}
// Bind upgrade buttons for plants
plantsContainer.querySelectorAll('[data-upgrade-plant]').forEach(btn => {
btn.addEventListener('click', (e) => {
const plantId = e.currentTarget.getAttribute('data-upgrade-plant');
if (this.workshop.upgradePlant(plantId)) {
this.saveUserData();
this.updateWorkshopUI();
}
});
});
}
// 3. Perks Upgrades List
const perksContainer = document.getElementById('perks-upgrade-list');
if (perksContainer) {
perksContainer.innerHTML = '';
const perkDefs = this.workshop.perkDefs;
for (const [id, def] of Object.entries(perkDefs)) {
const lvl = this.workshop.getPerkLevel(id);
const curTier = lvl > 0 ? def.tiers[lvl] : null;
const nextTier = lvl < 3 ? def.tiers[lvl + 1] : null;
const card = document.createElement('div');
card.className = 'upgrade-card';
let costHtml = '';
let canAfford = false;
if (nextTier) {
canAfford = this.workshop.canAffordCost(nextTier.cost);
const parts = [];
if (nextTier.cost.coins) {
const hasCoins = this.economy && this.economy.totalCoins >= nextTier.cost.coins;
parts.push(`💰 ${nextTier.cost.coins}`);
}
if (nextTier.cost.bioHumus) {
const hasBio = this.workshop.materials.bioHumus >= nextTier.cost.bioHumus;
parts.push(`🧪 ${nextTier.cost.bioHumus}`);
}
if (nextTier.cost.scrapMetal) {
const hasScrap = this.workshop.materials.scrapMetal >= nextTier.cost.scrapMetal;
parts.push(`⚙️ ${nextTier.cost.scrapMetal}`);
}
if (nextTier.cost.sunEssence) {
const hasSun = this.workshop.materials.sunEssence >= nextTier.cost.sunEssence;
parts.push(`☀️ ${nextTier.cost.sunEssence}`);
}
if (nextTier.cost.crystals) {
const hasCryst = this.workshop.materials.crystals >= nextTier.cost.crystals;
parts.push(`💎 ${nextTier.cost.crystals}`);
}
costHtml = parts.join(' ');
}
const stars = lvl === 3 ? '★★★' : (lvl === 2 ? '★★☆' : (lvl === 1 ? '★☆☆' : '☆☆☆'));
const badgeClass = lvl === 3 ? 'upgrade-level-badge maxed' : 'upgrade-level-badge';
card.innerHTML = `
${def.desc}. ${curTier ? `(Сейчас: ${curTier.desc})` : '(Не изучен)'}
${nextTier ? `
След. уровень: ${nextTier.desc}
` : `
Перк полностью освоен!
`}
`;
perksContainer.appendChild(card);
}
// Bind upgrade buttons for perks
perksContainer.querySelectorAll('[data-upgrade-perk]').forEach(btn => {
btn.addEventListener('click', (e) => {
const perkId = e.currentTarget.getAttribute('data-upgrade-perk');
if (this.workshop.upgradePerk(perkId)) {
this.saveUserData();
this.updateWorkshopUI();
}
});
});
}
// 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 += `
`;
});
const curStageDef = this.zombieLab.stages.find(s => s.id === selStage) || this.zombieLab.stages[0];
stageBanner.innerHTML = `
🗺️ Кампания «Я, Зомби» (Этап ${selStage} из 5)
${curStageDef.title}: ${curStageDef.desc}
${stageBtnsHtml}
`;
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 = `
Текущий бонус: ${curData.title} — ${curData.desc}
${!isMax ? `
Мутация до Ур. ${curTier + 1}: ${nextData.title} — ${nextData.desc}
` : `
🧬 Максимальная мутация достигнута!
`}
`;
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 = `
🧅
«THIS IS MY SWAMP!»
ШРЕК СОТРЯС ЗЕМЛЮ! 2000 урона всем зомби! (${affected} поражено)
`;
toast.classList.add('visible');
setTimeout(() => toast.classList.remove('visible'), 4000);
}
initZenGarden() {
if (!this.zengarden) return;
this.zenTool = 'water'; // 'water', 'fertilizer', 'music'
const zenCanvas = document.getElementById('zenCanvas');
if (!zenCanvas) return;
const zenCtx = zenCanvas.getContext('2d');
// Tool selectors
const btnWater = document.getElementById('zen-tool-water');
const btnFert = document.getElementById('zen-tool-fert');
const btnMusic = document.getElementById('zen-tool-music');
const btnFeedSnail = document.getElementById('zen-feed-snail');
const btnPlantSprout = document.getElementById('zen-plant-sprout');
const updateToolBtns = () => {
[btnWater, btnFert, btnMusic].forEach(b => { if (b) b.classList.remove('active'); });
if (this.zenTool === 'water' && btnWater) btnWater.classList.add('active');
if (this.zenTool === 'fertilizer' && btnFert) btnFert.classList.add('active');
if (this.zenTool === 'music' && btnMusic) btnMusic.classList.add('active');
const fertCount = document.getElementById('zen-fert-count');
const chocCount = document.getElementById('zen-choc-count');
if (fertCount) fertCount.textContent = this.zengarden.fertilizerCount;
if (chocCount) chocCount.textContent = this.zengarden.chocolateCount;
};
if (btnWater) btnWater.addEventListener('click', () => { this.zenTool = 'water'; updateToolBtns(); });
if (btnFert) btnFert.addEventListener('click', () => { this.zenTool = 'fertilizer'; updateToolBtns(); });
if (btnMusic) btnMusic.addEventListener('click', () => { this.zenTool = 'music'; updateToolBtns(); });
if (btnFeedSnail) {
btnFeedSnail.addEventListener('click', () => {
if (this.zengarden.feedSnail()) {
this.saveUserData();
updateToolBtns();
}
});
}
if (btnPlantSprout) {
btnPlantSprout.addEventListener('click', () => {
if (this.economy && this.economy.totalCoins >= 100) {
const types = ['sunflower', 'peashooter', 'wallnut', 'snowpea', 'repeater', 'chomper'];
const picked = types[Math.floor(Math.random() * types.length)];
if (this.zengarden.plantSprout(picked)) {
this.economy.addCoins(-100);
if (window.PvZAudio) window.PvZAudio.playWin();
this.saveUserData();
}
}
});
}
// Canvas pot click
zenCanvas.addEventListener('click', (e) => {
const rect = zenCanvas.getBoundingClientRect();
const scaleX = zenCanvas.width / rect.width;
const scaleY = zenCanvas.height / rect.height;
const mx = (e.clientX - rect.left) * scaleX;
const my = (e.clientY - rect.top) * scaleY;
// Check coins click
for (const c of this.zengarden.gardenCoins) {
if (!c.collected && Math.hypot(mx - c.x, my - c.y) < 25) {
this.zengarden.collectCoin(c);
return;
}
}
// Check pots click (col: 0..3, row: 0..1)
for (let i = 0; i < 8; i++) {
const col = i % 4;
const row = Math.floor(i / 4);
const px = 120 + col * 150;
const py = row === 0 ? 150 : 275;
if (Math.abs(mx - px) < 45 && my >= py - 50 && my <= py + 30) {
if (this.zenTool === 'water') {
this.zengarden.waterPlant(i);
} else if (this.zenTool === 'fertilizer') {
this.zengarden.fertilizePlant(i);
} else if (this.zenTool === 'music') {
this.zengarden.playMusicToPlant(i);
}
this.saveUserData();
updateToolBtns();
return;
}
}
});
updateToolBtns();
// Zen Garden Animation Loop
let lastZenTime = performance.now();
const zenLoop = (now) => {
const modal = document.getElementById('zengarden-modal');
if (modal && !modal.classList.contains('hidden')) {
const dt = Math.min((now - lastZenTime) / 1000, 0.1);
this.zengarden.update(dt);
this.zengarden.draw(zenCtx);
}
lastZenTime = now;
requestAnimationFrame(zenLoop);
};
requestAnimationFrame(zenLoop);
}
updateZenGardenUI() {
if (!this.zengarden) return;
const fertCount = document.getElementById('zen-fert-count');
const chocCount = document.getElementById('zen-choc-count');
if (fertCount) fertCount.textContent = this.zengarden.fertilizerCount;
if (chocCount) chocCount.textContent = this.zengarden.chocolateCount;
const alertEl = document.getElementById('zen-offline-alert');
if (alertEl) {
if (this.zengarden.offlineEarned > 0) {
alertEl.textContent = `💰 Пассивный доход за время отсутствия: +${this.zengarden.offlineEarned} монет!`;
} else {
alertEl.textContent = `💰 Взрослые растения приносят монеты каждые несколько секунд.`;
}
}
}
initAlmanac() {
if (!this.almanac) return;
const tabPlants = document.getElementById('tab-almanac-plants');
const tabZombies = document.getElementById('tab-almanac-zombies');
if (tabPlants) {
tabPlants.addEventListener('click', () => {
this.almanac.selectedCategory = 'plants';
this.almanac.selectedId = 'peashooter';
tabPlants.classList.add('active');
if (tabZombies) tabZombies.classList.remove('active');
this.renderAlmanacUI();
});
}
if (tabZombies) {
tabZombies.addEventListener('click', () => {
this.almanac.selectedCategory = 'zombies';
this.almanac.selectedId = 'z_normal';
tabZombies.classList.add('active');
if (tabPlants) tabPlants.classList.remove('active');
this.renderAlmanacUI();
});
}
}
renderAlmanacUI() {
if (!this.almanac) return;
const listContainer = document.getElementById('almanac-list');
const detailsContainer = document.getElementById('almanac-details');
if (!listContainer || !detailsContainer) return;
const items = this.almanac.selectedCategory === 'plants' ? this.almanac.plants : this.almanac.zombies;
listContainer.innerHTML = '';
// Render grid items
for (const item of items) {
const slot = document.createElement('div');
const isSelected = item.id === this.almanac.selectedId;
slot.className = isSelected ? 'almanac-slot selected' : 'almanac-slot';
slot.innerHTML = `
${item.icon}
${item.name}
`;
slot.addEventListener('click', () => {
this.almanac.selectedId = item.id;
this.renderAlmanacUI();
if (window.PvZAudio) window.PvZAudio.playCoin();
});
listContainer.appendChild(slot);
}
// Render detailed view for selected item
const selectedItem = this.almanac.getItem(this.almanac.selectedId) || items[0];
if (selectedItem) {
let statsHtml = '';
if (this.almanac.selectedCategory === 'plants') {
statsHtml = `
Стоимость: ☀️ ${selectedItem.cost}
Перезарядка: ${selectedItem.cooldown}
Урон: ${selectedItem.damage}
Прочность: ${selectedItem.toughness}
`;
} else {
statsHtml = `
Скорость: ${selectedItem.speed}
Прочность: ${selectedItem.toughness}
`;
}
detailsContainer.innerHTML = `
${selectedItem.icon}
${selectedItem.name}
${statsHtml}
${selectedItem.desc}
${selectedItem.lore}
`;
}
}
initFeedbackUI() {
// Bug Filter Tabs
const bugTabs = [
{ id: 'tab-bug-all', filter: 'all' },
{ id: 'tab-bug-bugs', filter: 'bug' },
{ id: 'tab-bug-ideas', filter: 'feature' },
{ id: 'tab-bug-visual', filter: 'visual' },
{ id: 'tab-bug-fixed', filter: 'resolved' }
];
bugTabs.forEach(t => {
const el = document.getElementById(t.id);
if (el) {
el.addEventListener('click', () => {
bugTabs.forEach(other => {
const oEl = document.getElementById(other.id);
if (oEl) oEl.classList.remove('active');
});
el.classList.add('active');
if (window.PvZFeedback) {
window.PvZFeedback.setFilter(t.filter);
}
});
}
});
// Bug Form Submit
const btnBugSubmit = document.getElementById('btn-bug-submit');
if (btnBugSubmit) {
btnBugSubmit.addEventListener('click', () => {
const nameInput = document.getElementById('bug-input-name');
const catInput = document.getElementById('bug-input-category');
const textInput = document.getElementById('bug-input-text');
const statusEl = document.getElementById('bug-form-status');
const author = nameInput ? nameInput.value.trim() : '';
const cat = catInput ? catInput.value : 'bug';
const text = textInput ? textInput.value.trim() : '';
if (!text) {
if (statusEl) {
statusEl.textContent = 'Пожалуйста, опишите проблему перед отправкой!';
statusEl.className = 'bug-status-msg error';
statusEl.classList.remove('hidden');
}
if (textInput) textInput.focus();
return;
}
btnBugSubmit.disabled = true;
btnBugSubmit.textContent = 'Отправка... ⏳';
if (window.PvZFeedback) {
window.PvZFeedback.sendReport(author, cat, text, (res) => {
btnBugSubmit.disabled = false;
btnBugSubmit.textContent = 'Отправить 🚀';
if (res && res.success) {
if (textInput) textInput.value = '';
if (statusEl) {
statusEl.textContent = res.offline
? 'Сохранено локально! Сообщение появится в чате.'
: 'Спасибо! Баг-репорт успешно сохранен на сервере ✅';
statusEl.className = 'bug-status-msg success';
statusEl.classList.remove('hidden');
setTimeout(() => {
if (statusEl) statusEl.classList.add('hidden');
}, 4000);
}
} else {
if (statusEl) {
statusEl.textContent = (res && res.message) || 'Ошибка отправки репорта';
statusEl.className = 'bug-status-msg error';
statusEl.classList.remove('hidden');
}
}
});
}
});
}
}
renderAchievementsUI() {
if (!this.achievements) return;
const listEl = document.getElementById('achievements-list');
if (!listEl) return;
listEl.innerHTML = '';
for (const ach of this.achievements.achievements) {
const card = document.createElement('div');
const isDone = ach.completed;
const isClaimed = ach.claimed;
card.className = `achievement-item ${isDone ? 'completed' : ''} ${isClaimed ? 'claimed' : ''}`;
const pct = Math.min(100, Math.round((ach.progress / ach.max) * 100));
let rewardText = `💰 +${ach.reward.coins}`;
if (ach.reward.crystals) {
rewardText += ` • 💎 +${ach.reward.crystals}`;
}
let btnHtml = '';
if (isClaimed) {
btnHtml = ``;
} else if (isDone) {
btnHtml = ``;
} else {
btnHtml = ``;
}
card.innerHTML = `
${ach.icon}
${ach.title}
${ach.desc}
Награда: ${rewardText}
${btnHtml}
`;
const claimBtn = card.querySelector('.btn-claim-reward');
if (claimBtn) {
claimBtn.addEventListener('click', () => {
this.achievements.claimReward(ach.id);
this.renderAchievementsUI();
this.updateRecordDisplay();
this.updateShopUI();
});
}
listEl.appendChild(card);
}
}
renderTreeUI() {
if (!this.tree) return;
const heightEl = document.getElementById('tree-height-display');
const artEl = document.getElementById('tree-art');
const titleEl = document.getElementById('tree-wisdom-title');
const quoteEl = document.getElementById('tree-wisdom-quote');
const cheatsListEl = document.getElementById('tree-cheats-list');
const feedBtn = document.getElementById('btn-tree-feed');
const height = this.tree.height;
if (heightEl) heightEl.textContent = `Высота: ${height} футов | Древесное удобрение: 150 💰`;
if (artEl) {
if (height >= 100) artEl.textContent = '🌲✨';
else if (height >= 50) artEl.textContent = '🌳';
else if (height >= 20) artEl.textContent = '🪴';
else if (height >= 5) artEl.textContent = '🌿';
else artEl.textContent = '🌱';
}
const wisdom = this.tree.getCurrentWisdom();
if (titleEl) titleEl.textContent = wisdom.title || 'Первый росток';
if (quoteEl) quoteEl.textContent = wisdom.text || '';
if (feedBtn) {
const canAfford = this.economy && this.economy.totalCoins >= this.tree.fertilizerCost;
feedBtn.disabled = !canAfford;
feedBtn.textContent = `Подкормить Древо (${this.tree.fertilizerCost} 💰)`;
}
if (cheatsListEl) {
cheatsListEl.innerHTML = '';
const cheatMeta = [
{ id: 'mustache', name: 'Усы (Mustache)', reqHeight: 10, icon: '👨🏻' },
{ id: 'future', name: 'Киберочки (Future)', reqHeight: 30, icon: '🕶️' },
{ id: 'daisies', name: 'Цветы (Daisies)', reqHeight: 50, icon: '🌼' },
{ id: 'dance', name: 'Танцы (Dance)', reqHeight: 100, icon: '🕺' },
{ id: 'pinata', name: 'Конфетти (Pinata)', reqHeight: 100, icon: '🎉' }
];
cheatMeta.forEach(c => {
const isUnlocked = height >= c.reqHeight;
const isActive = !!this.tree.cheats[c.id];
const item = document.createElement('div');
item.className = `tree-cheat-item ${isUnlocked ? (isActive ? 'active' : '') : 'locked'}`;
item.innerHTML = `
${c.icon}
${c.name}
${isUnlocked ? (isActive ? 'Статус: ВКЛЮЧЕН ✅' : 'Статус: Выключен') : `Требуется: ${c.reqHeight} футов 🔒`}
`;
const toggleBtn = item.querySelector('button');
if (toggleBtn && isUnlocked) {
toggleBtn.addEventListener('click', () => {
this.tree.toggleCheat(c.id);
this.renderTreeUI();
});
}
cheatsListEl.appendChild(item);
});
}
}
renderLeaderboardUI(mode = 'survival') {
if (!this.leaderboards) return;
this.leaderboards.selectedMode = mode;
const contentEl = document.getElementById('leaderboard-content');
if (!contentEl) return;
const entries = this.leaderboards.getEntries(mode);
if (!entries || entries.length === 0) {
contentEl.innerHTML = `Пока нет записей. Сыграйте в этот режим, чтобы установить первый рекорд!
`;
return;
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&')
.replace(//g, '>')
.replace(/"/g, '"')
.replace(/'/g, ''');
}
let rowsHtml = '';
entries.forEach((item, index) => {
let medal = `#${index + 1}`;
if (index === 0) medal = '🥇 1';
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 += `
| ${medal} |
${safeName} |
${safeDetail} |
${safeDate} |
`;
});
contentEl.innerHTML = `
| Место |
Игрок |
Результат |
Дата |
${rowsHtml}
`;
}
showDraftModal() {
if (!this.roguelike) return;
const modal = document.getElementById('draft-modal');
const listEl = document.getElementById('draft-choices-list');
if (!modal || !listEl) return;
const choices = this.roguelike.generateDraftChoices();
listEl.innerHTML = '';
choices.forEach((choice, index) => {
const card = document.createElement('div');
card.className = `draft-card-item ${choice.type === 'relic' ? 'relic-choice' : 'seed-choice'}`;
card.innerHTML = `
${choice.type === 'relic' ? '✨ РЕЛИКВИЯ' : '🌱 СЕМЯ РАСТЕНИЯ'}
${choice.icon}
${choice.name}
${choice.desc}
`;
const pickBtn = card.querySelector('.draft-choice-btn');
if (pickBtn) {
pickBtn.addEventListener('click', () => {
this.roguelike.selectChoice(index);
modal.classList.add('hidden');
if (window.PvZAudio) window.PvZAudio.playCoin();
this.startLevel(this.roguelike.currentFloor, 'roguelike');
});
}
listEl.appendChild(card);
});
modal.classList.remove('hidden');
}
createPlant(type, col, row, bounds) {
const level = this.workshop ? this.workshop.getPlantLevel(type) : 1;
const plant = new window.Plant(type, col, row, bounds.x, bounds.y, bounds.w, bounds.h, level);
if (this.workshop) {
const hardyLvl = this.workshop.getPerkLevel('hardyRoots');
if (hardyLvl > 0) {
const mult = 1 + (hardyLvl === 1 ? 0.20 : (hardyLvl === 2 ? 0.35 : 0.50));
plant.maxHealth = Math.round(plant.maxHealth * mult);
plant.health = plant.maxHealth;
}
}
// Roguelike: relic_iron_bark (+350 HP to Wallnut)
if (this.roguelike && this.currentMode === 'roguelike' && this.roguelike.hasRelic('relic_iron_bark') && type === 'wallnut') {
plant.maxHealth += 350;
plant.health = plant.maxHealth;
}
// Roguelike: relic_fertilizer_boost (20% faster shootInterval)
if (this.roguelike && this.currentMode === 'roguelike' && this.roguelike.hasRelic('relic_fertilizer_boost')) {
if (plant.shootInterval) {
plant.shootInterval *= 0.8;
}
}
// Achievement: first_plant
if (this.achievements) {
this.achievements.trigger('first_plant');
}
return plant;
}
handleCellClick(col, row) {
// 0. I, Zombie Mechanic: Click row to deploy zombie!
if (this.currentMode === 'izombie') {
const selected = this.economy.selectedCard;
if (!selected) return;
const card = this.economy.cards.find(c => c.id === selected);
if (card && this.economy.spendSun(card.cost) && card.timer <= 0) {
this.economy.startCooldown(card.id);
const zType = selected.replace('z_', '');
this.spawnZombie(row, zType);
if (window.PvZAudio) window.PvZAudio.playPlant();
this.economy.selectedCard = null;
}
return;
}
// 1. Wall-nut Bowling Mechanic
if (this.currentMode === 'bowling') {
const selected = this.economy.selectedCard;
if (!selected) return;
const card = this.economy.cards.find(c => c.id === selected);
if (card && card.timer <= 0) {
this.economy.startCooldown(card.id);
const bounds = this.grid.getCellBounds(0, row);
if (selected === 'bowling_cherry') {
// Exploding bowling cherry
this.explodeArea(col, row, 3);
if (window.PvZAudio) window.PvZAudio.playExplosion();
} else {
// Rolling bowling nut
const ball = this.spawnProjectile(130, bounds.centerY, row, 'bowling', 0);
if (window.PvZAudio) window.PvZAudio.playPlant();
}
this.economy.selectedCard = null;
}
return;
}
// 2. Standard Planting Mechanic
const selected = this.economy.selectedCard;
if (!selected) return;
const existingPlant = this.grid.getPlant(col, row);
const isWater = this.grid.isWaterCell(row);
const hasGrave = this.grid.hasGrave(col, row);
// Shovel
if (selected === 'shovel') {
if (existingPlant) {
this.grid.removePlant(col, row);
existingPlant.toRemove = true;
this.economy.selectedCard = null;
if (window.PvZAudio) window.PvZAudio.playPlant();
}
return;
}
// Gravebuster
if (selected === 'gravebuster') {
if (hasGrave && !existingPlant) {
const card = this.economy.cards.find(c => c.id === selected);
if (card && this.economy.spendSun(card.cost)) {
this.economy.startCooldown(card.id);
const bounds = this.grid.getCellBounds(col, row);
const plant = this.createPlant(card.id, col, row, bounds);
this.grid.setPlant(col, row, plant);
this.plants.push(plant);
this.recordStat('plantsPlanted', 1);
this.economy.selectedCard = null;
if (window.PvZAudio) window.PvZAudio.playPlant();
}
}
return;
}
// Cannot plant on Gravestone with anything else
if (hasGrave) return;
// Water lane checks (Pool biome)
if (isWater) {
if (selected === 'lilypad' && !existingPlant) {
// Plant Lilypad
const card = this.economy.cards.find(c => c.id === selected);
if (card && this.economy.spendSun(card.cost)) {
this.economy.startCooldown(card.id);
const bounds = this.grid.getCellBounds(col, row);
const plant = this.createPlant('lilypad', col, row, bounds);
this.grid.setPlant(col, row, plant);
this.plants.push(plant);
this.recordStat('plantsPlanted', 1);
this.economy.selectedCard = null;
if (window.PvZAudio) window.PvZAudio.playPlant();
}
return;
} else if (existingPlant && existingPlant.type === 'lilypad') {
// Plant normal plant on top of Lilypad!
const card = this.economy.cards.find(c => c.id === selected);
if (card && selected !== 'lilypad' && this.economy.spendSun(card.cost)) {
this.economy.startCooldown(card.id);
const bounds = this.grid.getCellBounds(col, row);
const plant = this.createPlant(card.id, col, row, bounds);
this.grid.setPlant(col, row, plant);
this.plants.push(plant);
this.recordStat('plantsPlanted', 1);
this.economy.selectedCard = null;
if (window.PvZAudio) window.PvZAudio.playPlant();
}
return;
} else {
return; // Water requires lilypad first
}
}
// Roof checks (Roof biome requires flowerpot)
if (this.grid.isRoof()) {
if (selected === 'flowerpot' && !existingPlant) {
const card = this.economy.cards.find(c => c.id === selected);
if (card && this.economy.canAfford(card.cost) && card.timer <= 0) {
if (this.economy.spendSun(card.cost)) {
this.economy.startCooldown(card.id);
const bounds = this.grid.getCellBounds(col, row);
const plant = this.createPlant('flowerpot', col, row, bounds);
this.grid.setPlant(col, row, plant);
this.plants.push(plant);
this.recordStat('plantsPlanted', 1);
this.economy.selectedCard = null;
if (window.PvZAudio) window.PvZAudio.playPlant();
}
}
return;
} else if (existingPlant && existingPlant.type === 'flowerpot') {
const card = this.economy.cards.find(c => c.id === selected);
if (card && selected !== 'flowerpot' && this.economy.canAfford(card.cost) && card.timer <= 0) {
if (this.economy.spendSun(card.cost)) {
this.economy.startCooldown(card.id);
const bounds = this.grid.getCellBounds(col, row);
const plant = this.createPlant(card.id, col, row, bounds);
this.grid.setPlant(col, row, plant);
this.plants.push(plant);
this.recordStat('plantsPlanted', 1);
this.economy.selectedCard = null;
if (window.PvZAudio) window.PvZAudio.playPlant();
}
}
return;
} else {
return; // Roof requires flowerpot first
}
}
// Normal Grass Planting
if (!existingPlant) {
const card = this.economy.cards.find(c => c.id === selected);
if (card && this.economy.canAfford(card.cost) && card.timer <= 0) {
if (this.economy.spendSun(card.cost)) {
this.economy.startCooldown(card.id);
const bounds = this.grid.getCellBounds(col, row);
const plant = this.createPlant(card.id, col, row, bounds);
this.grid.setPlant(col, row, plant);
this.plants.push(plant);
this.recordStat('plantsPlanted', 1);
this.economy.selectedCard = null;
if (window.PvZAudio) window.PvZAudio.playPlant();
}
}
}
}
loop(timestamp) {
const rawDt = Math.min((timestamp - this.lastTime) / 1000, 0.1);
this.lastTime = timestamp;
const dt = rawDt * this.gameSpeed;
if (this.isRunning && !this.isPaused) {
this.update(dt);
}
this.render();
requestAnimationFrame(this.loop.bind(this));
}
update(dt) {
// 1. Grid & Lawnmowers
this.grid.updateLawnmowers(dt, this.zombies);
// 2. Economy & Workshop Loot & Zen Garden
this.economy.update(dt);
if (this.workshop) {
this.workshop.update(dt, this);
}
if (this.zengarden) {
this.zengarden.update(dt * 0.2);
}
// 3. Waves & Spawns
this.wave.update(dt, this);
// 4. Garden Rake Check
if (this.hasRakeArmed) {
const rakeBounds = this.grid.getCellBounds(this.rakeCol, this.rakeRow);
for (const z of this.zombies) {
if (z.row === this.rakeRow && Math.abs(z.x - rakeBounds.centerX) < 30 && z.health > 0) {
this.hasRakeArmed = false;
z.takeDamage(1000);
this.combat.createExplosionParticles(rakeBounds.centerX, rakeBounds.centerY);
if (window.PvZAudio) window.PvZAudio.playStrike();
break;
}
}
}
// 5. Plants
for (const plant of this.plants) {
plant.update(dt, this);
if (plant.toRemove) {
this.grid.removePlant(plant.col, plant.row);
if (this.currentMode === 'izombie' && plant.type === 'sunflower') {
this.economy.addSun(150);
if (window.PvZAudio) window.PvZAudio.playSun();
}
}
}
this.plants = this.plants.filter(p => !p.toRemove);
// 6. Zombies
for (const zombie of this.zombies) {
zombie.update(dt, this);
}
this.zombies = this.zombies.filter(z => !z.toRemove);
// 7. Projectiles (in-place update with pooling release, eliminating GC allocations)
let projWriteIdx = 0;
for (let i = 0; i < this.projectiles.length; i++) {
const proj = this.projectiles[i];
proj.update(dt);
if (!proj.toRemove) {
this.projectiles[projWriteIdx++] = proj;
} else {
if (this.projectilePool) this.projectilePool.release(proj);
}
}
this.projectiles.length = projWriteIdx;
// 8. Combat
this.combat.update(dt, this);
// 8.5 Roguelike Relics & Timers
if (this.roguelike && this.currentMode === 'roguelike') {
this.roguelike.update(dt, this);
}
// 8.6 Dr. Zomboss Boss Fight
if (this.boss && (this.boss.isActive || this.currentMode === 'boss' || this.currentMode === 'bossrush')) {
this.boss.update(dt, this);
}
// 9. I, Zombie Win & Defeat Check
if (this.currentMode === 'izombie' && this.izombieSetupDone && this.isRunning) {
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('Закончились солнце и зомби! Растения уцелели.');
}
}
}
}
render() {
this.ctx.clearRect(0, 0, this.logicalWidth, this.logicalHeight);
// 1. Grid & Biome
this.grid.draw(this.ctx, this.hoverCell, this.economy.selectedCard);
// 2. Garden Rake
if (this.hasRakeArmed) {
const rakeBounds = this.grid.getCellBounds(this.rakeCol, this.rakeRow);
this.drawRake(this.ctx, rakeBounds.centerX, rakeBounds.centerY);
}
// 3. Plants
for (const plant of this.plants) {
plant.draw(this.ctx);
}
// 4. Zombies (depth sort)
const sortedZombies = [...this.zombies].sort((a, b) => a.y - b.y);
for (const zombie of sortedZombies) {
zombie.draw(this.ctx, this);
}
// 5. Projectiles
for (const proj of this.projectiles) {
proj.draw(this.ctx);
}
// 5.5 Workshop Floating Loot
if (this.workshop) {
this.workshop.draw(this.ctx);
}
// 6. Combat Particles
this.combat.draw(this.ctx);
// 7. Top UI & Suns
this.economy.draw(this.ctx);
// 8. Wave status & Banners
this.wave.draw(this.ctx);
// 8.5 Roguelike Active Relics HUD
if (this.roguelike && this.currentMode === 'roguelike') {
this.roguelike.draw(this.ctx);
}
// 8.6 Dr. Zomboss Robot & Hazards
if (this.boss && (this.boss.isActive || this.currentMode === 'boss' || this.currentMode === 'bossrush')) {
this.boss.draw(this.ctx);
}
// 9. Ghost Preview
if (this.hoverCell && this.economy.selectedCard && this.economy.selectedCard !== 'shovel') {
const bounds = this.grid.getCellBounds(this.hoverCell.col, this.hoverCell.row);
this.ctx.save();
this.ctx.globalAlpha = 0.55;
this.economy.drawCardThumbnail(this.ctx, this.economy.selectedCard, bounds.centerX, bounds.centerY);
this.ctx.restore();
}
// 10. Level & Score indicator in top right
this.ctx.save();
this.ctx.fillStyle = '#ffeb3b';
this.ctx.font = 'bold 12px sans-serif';
this.ctx.textAlign = 'right';
const modeLabel = this.currentMode === 'survival'
? 'Выживание'
: (this.currentMode === 'bowling'
? 'Боулинг'
: (this.currentMode === 'boss'
? 'Босс: Д-р Зомбосс'
: (this.currentMode === 'roguelike' ? `Рогалик: Этаж ${this.roguelike ? this.roguelike.currentFloor : 1}` : `Уровень ${this.currentLevel}`)));
this.ctx.fillText(`${modeLabel} | Счёт: ${this.score}`, 880, 78);
this.ctx.restore();
// 11. Pause Banner Overlay
if (this.isPaused) {
this.ctx.save();
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.65)';
this.ctx.fillRect(0, 0, this.logicalWidth, this.logicalHeight);
// Center Modal Card
this.ctx.fillStyle = '#2a160c';
this.ctx.strokeStyle = '#e09f3e';
this.ctx.lineWidth = 4;
this.ctx.beginPath();
this.ctx.roundRect(260, 210, 380, 180, 16);
this.ctx.fill();
this.ctx.stroke();
this.ctx.fillStyle = '#ffeb3b';
this.ctx.font = 'bold 28px sans-serif';
this.ctx.textAlign = 'center';
this.ctx.fillText('⏸️ ИГРА НА ПАУЗЕ', this.logicalWidth / 2, 270);
this.ctx.fillStyle = '#e0e0e0';
this.ctx.font = '14px sans-serif';
this.ctx.fillText('Все растения и зомби остановлены.', this.logicalWidth / 2, 308);
this.ctx.fillStyle = '#81c784';
this.ctx.font = 'bold 15px sans-serif';
this.ctx.fillText('Нажмите Пробел или кнопку «▶️ Играть»', this.logicalWidth / 2, 345);
this.ctx.restore();
}
}
drawRake(ctx, x, y) {
ctx.save();
ctx.translate(x, y);
ctx.strokeStyle = '#8d6e63';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-10, 18);
ctx.lineTo(15, -12);
ctx.stroke();
ctx.strokeStyle = '#757575';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(8, -16);
ctx.lineTo(20, -6);
for (let i = 0; i < 4; i++) {
ctx.moveTo(8 + i * 3, -16 + i * 2.5);
ctx.lineTo(5 + i * 3, -22 + i * 2.5);
}
ctx.stroke();
ctx.restore();
}
};