// 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 (Safely bound with optional chaining) const handleExportSave = () => { this.saveUserData(); if (window.PvZAuth && window.PvZAuth.exportSaveFile) { window.PvZAuth.exportSaveFile(); } }; document.getElementById('btn-export-save')?.addEventListener('click', handleExportSave); document.getElementById('btn-export-save-profile')?.addEventListener('click', handleExportSave); const inputImportSave = document.getElementById('input-import-save'); const handleTriggerImport = () => { if (inputImportSave) { inputImportSave.value = ''; inputImportSave.click(); } }; document.getElementById('btn-import-save')?.addEventListener('click', handleTriggerImport); document.getElementById('btn-import-save-profile')?.addEventListener('click', handleTriggerImport); 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(); } } if (inputImportSave) 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 += `
${world.icon} Этап ${lvl.code} ${badgeText}
${lvl.title}
${lvl.desc}
🌊 Волн: ${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 = `
${def.icon} ${def.name}
${stars} Ур. ${lvl}
Текущий эффект: ${curTier.desc}
${nextTier ? `
След. ранг: ${nextTier.title} — ${nextTier.desc}
${costHtml}
` : `
Максимальный 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.icon} ${def.name}
${stars} Ур. ${lvl}
${def.desc}. ${curTier ? `(Сейчас: ${curTier.desc})` : '(Не изучен)'}
${nextTier ? `
След. уровень: ${nextTier.desc}
${costHtml}
` : `
Перк полностью освоен!
`} `; 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 = `
${zDef.icon}

${zDef.name}

${stars} (Ур. ${curTier}/3)
Ранг ${curTier}
Текущий бонус: ${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(); } };