// 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(); } this.wave = new window.WaveSystem(); this.combat = new window.CombatSystem(); // 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.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.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 // 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.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(); } 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'; this.applyTheme(this.currentTheme); } } saveUserData() { if (window.PvZAuth) { if (this.achievements) this.achievements.saveToAuth(); if (this.tree) this.tree.saveToAuth(); if (this.leaderboards) this.leaderboards.saveToAuth(); window.PvZAuth.saveUserData({ coins: this.economy ? this.economy.totalCoins : undefined, level: this.currentLevel, maxUnlockedLevel: this.maxUnlockedLevel, highScore: this.highScore, theme: this.currentTheme, 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); 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(); } updateProfileUI() { const badgeName = document.getElementById('badge-user-name'); const btnAuth = document.getElementById('btn-auth-open'); const btnLogoutNav = document.getElementById('btn-auth-logout-nav'); const authStatus = document.getElementById('auth-current-status'); const btnLogoutModal = document.getElementById('btn-auth-logout'); const btnAuthGuest = document.getElementById('btn-auth-guest'); if (window.PvZAuth) { const user = window.PvZAuth.getCurrentUser(); const isGuest = window.PvZAuth.isGuest(); if (badgeName) badgeName.textContent = isGuest ? '👤 Гость' : `👤 ${user}`; if (btnAuth) btnAuth.textContent = isGuest ? 'Войти' : 'Сменить'; if (btnLogoutNav) { if (isGuest) btnLogoutNav.classList.add('hidden'); else btnLogoutNav.classList.remove('hidden'); } if (authStatus) { authStatus.innerHTML = isGuest ? 'Текущий режим: Гость' : `Вы вошли как: ${user}`; } if (btnLogoutModal) { if (isGuest) btnLogoutModal.classList.add('hidden'); else btnLogoutModal.classList.remove('hidden'); } if (btnAuthGuest) { if (isGuest) btnAuthGuest.classList.remove('hidden'); else btnAuthGuest.classList.add('hidden'); } } } 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'].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; } 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() { this.plants = []; this.izombieSetupDone = true; // Col 0: Sunflowers in rows 0..4 (5 trophy targets) for (let r = 0; r < 5; r++) { const b0 = this.grid.getCellBounds(0, r); const sf = new window.Plant('sunflower', 0, r, b0.x, b0.y, b0.w, b0.h); this.grid.setPlant(0, r, sf); this.plants.push(sf); } // Row 0 defense: Peashooter at col 2 const b02 = this.grid.getCellBounds(2, 0); const p02 = new window.Plant('peashooter', 2, 0, b02.x, b02.y, b02.w, b02.h); this.grid.setPlant(2, 0, p02); this.plants.push(p02); // Row 1 defense: Repeater at col 2, Wallnut at col 4 const b12 = this.grid.getCellBounds(2, 1); const p12 = new window.Plant('repeater', 2, 1, b12.x, b12.y, b12.w, b12.h); this.grid.setPlant(2, 1, p12); this.plants.push(p12); const b14 = this.grid.getCellBounds(4, 1); const p14 = new window.Plant('wallnut', 4, 1, b14.x, b14.y, b14.w, b14.h); this.grid.setPlant(4, 1, p14); this.plants.push(p14); // Row 2 defense: Snowpea at col 1, Peashooter at col 3 const b21 = this.grid.getCellBounds(1, 2); const p21 = new window.Plant('snowpea', 1, 2, b21.x, b21.y, b21.w, b21.h); this.grid.setPlant(1, 2, p21); this.plants.push(p21); const b23 = this.grid.getCellBounds(3, 2); const p23 = new window.Plant('peashooter', 3, 2, b23.x, b23.y, b23.w, b23.h); this.grid.setPlant(3, 2, p23); this.plants.push(p23); // Row 3 defense: Peashooter at col 2, Wallnut at col 4 const b32 = this.grid.getCellBounds(2, 3); const p32 = new window.Plant('peashooter', 2, 3, b32.x, b32.y, b32.w, b32.h); this.grid.setPlant(2, 3, p32); this.plants.push(p32); const b34 = this.grid.getCellBounds(4, 3); const p34 = new window.Plant('wallnut', 4, 3, b34.x, b34.y, b34.w, b34.h); this.grid.setPlant(4, 3, p34); this.plants.push(p34); // Row 4 defense: Repeater at col 1, Potato Mine at col 3 (armed) const b41 = this.grid.getCellBounds(1, 4); const p41 = new window.Plant('repeater', 1, 4, b41.x, b41.y, b41.w, b41.h); this.grid.setPlant(1, 4, p41); this.plants.push(p41); const b43 = this.grid.getCellBounds(3, 4); const p43 = new window.Plant('potatomine', 3, 4, b43.x, b43.y, b43.w, b43.h); p43.isArmed = true; p43.armTimer = 15; this.grid.setPlant(3, 4, p43); this.plants.push(p43); } 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.startLevel(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(); 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'); } 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; } 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; 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++; 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.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.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; 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 = `Победа! Все подсолнухи съедены! Мозги захвачены!
Награда: +100 💰 монет (Всего: ${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'); } 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) { 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 + 24) { s.collecting = true; if (window.PvZAudio) window.PvZAudio.playSun(); } } 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 + 24) { c.collecting = true; 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); }); this.canvas.addEventListener('mouseleave', () => { this.hoverCell = null; }); 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) return; const cell = this.grid.getCellFromCoords(pos.x, pos.y); if (cell) { this.handleCellClick(cell.col, cell.row); } }); // Mobile touch this.canvas.addEventListener('touchstart', (e) => { if (!this.isRunning) return; e.preventDefault(); const pos = this.getEventPos(e); this.checkTouchCollect(pos); const econResult = this.economy.handleClick(pos.x, pos.y); if (econResult.handled) return; const cell = this.grid.getCellFromCoords(pos.x, pos.y); if (cell) { this.handleCellClick(cell.col, cell.row); } }, { 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', () => { this.hoverCell = null; }); } bindUI() { // 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'; }); } // 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', () => { this.startLevel(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 btnAuthSubmit = document.getElementById('btn-auth-submit'); 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 handleLogout = () => { if (window.PvZAuth) { window.PvZAuth.logout(); this.loadUserData(); this.updateProfileUI(); this.updateRecordDisplay(); this.updateShopUI(); } if (authModal) authModal.classList.add('hidden'); if (authUser) authUser.value = ''; if (authPass) authPass.value = ''; if (authError) authError.classList.add('hidden'); if (window.PvZAudio) window.PvZAudio.playCoin(); }; if (btnAuthLogoutNav) { btnAuthLogoutNav.addEventListener('click', handleLogout); } if (btnAuthLogout) { btnAuthLogout.addEventListener('click', handleLogout); } if (btnAuthOpen && authModal) { btnAuthOpen.addEventListener('click', () => { if (authError) authError.classList.add('hidden'); this.updateProfileUI(); authModal.classList.remove('hidden'); if (authUser) authUser.focus(); }); } if (btnAuthClose && authModal) { btnAuthClose.addEventListener('click', () => { authModal.classList.add('hidden'); }); } if (btnAuthGuest && authModal) { btnAuthGuest.addEventListener('click', () => { handleLogout(); }); } if (btnAuthSubmit && authModal) { btnAuthSubmit.addEventListener('click', () => { const u = authUser ? authUser.value.trim() : ''; const p = authPass ? authPass.value : ''; if (!u) { if (authError) { authError.textContent = 'Введите имя пользователя'; authError.classList.remove('hidden'); } return; } if (!p && u.toLowerCase() !== 'гость') { 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 = window.PvZAuth.login(u, p, (serverData, errMsg) => { // Server response callback (async) btnAuthSubmit.textContent = 'Войти / Регистрация'; btnAuthSubmit.disabled = false; if (errMsg) { // Wrong password from server if (authError) { authError.textContent = errMsg; authError.classList.remove('hidden'); } // Restore guest state in UI this.updateProfileUI(); return; } if (serverData) { this.loadUserData(); this.updateProfileUI(); this.updateRecordDisplay(); this.updateShopUI(); } }); if (res.success) { // Optimistic: close modal immediately (data loads async) authModal.classList.add('hidden'); if (authUser) authUser.value = ''; if (authPass) authPass.value = ''; this.loadUserData(); this.updateProfileUI(); this.updateRecordDisplay(); } else { btnAuthSubmit.textContent = 'Войти / Регистрация'; btnAuthSubmit.disabled = false; if (authError) { authError.textContent = res.message || 'Ошибка входа'; authError.classList.remove('hidden'); } } } }); } // Shop Button const btnShop = document.getElementById('btn-shop'); const shopModal = document.getElementById('shop-modal'); const btnCloseShop = document.getElementById('btn-close-shop'); if (btnShop && shopModal) { btnShop.addEventListener('click', () => { this.updateShopUI(); shopModal.classList.remove('hidden'); }); } if (btnCloseShop && shopModal) { btnCloseShop.addEventListener('click', () => { shopModal.classList.add('hidden'); this.updateRecordDisplay(); }); } // 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(); } }); } 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 contentPlants = document.getElementById('ws-content-plants'); const contentPerks = document.getElementById('ws-content-perks'); 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(); }); } if (tabPlants && tabPerks && contentPlants && contentPerks) { tabPlants.addEventListener('click', () => { tabPlants.classList.add('active'); tabPerks.classList.remove('active'); contentPlants.classList.remove('hidden'); contentPerks.classList.add('hidden'); }); tabPerks.addEventListener('click', () => { tabPerks.classList.add('active'); tabPlants.classList.remove('active'); contentPerks.classList.remove('hidden'); contentPlants.classList.add('hidden'); }); } // 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.startLevel(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'; } }); } updateShopUI() { const elCoins = document.getElementById('shop-coins'); if (elCoins) elCoins.textContent = `💰 Монеты: ${this.economy.totalCoins}`; 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; } } 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(); } }); }); } } 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; } 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'; rowsHtml += ` ${medal} ${item.name} ${item.detail || item.score} ${item.date || ''} `; }); 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 = new window.Projectile(130, bounds.centerY, row, 'bowling', 0); this.projectiles.push(ball); 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.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.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.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.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.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.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.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 for (const proj of this.projectiles) { proj.update(dt); } this.projectiles = this.projectiles.filter(p => !p.toRemove); // 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 sunflowers = this.plants.filter(p => p.type === 'sunflower'); if (sunflowers.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(); } 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(); } };