Files
ai-qpid-pvz/game/js/game.js
T

3263 lines
122 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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();
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
// 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();
} 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.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,
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');
}
}
}
}
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 = `🏆 Рекорд: <strong>${this.highScore}</strong> очков &nbsp;|&nbsp; 💰 Монеты: <strong>${this.economy.totalCoins}</strong> &nbsp;|&nbsp; Уровень: <strong>${lvlLabel}</strong>`;
}
}
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);
}
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;
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 = `Счёт: <strong>${this.score}</strong> | Зомби: <strong>${this.zombiesKilled}</strong><br>Рекорд: <strong>${this.highScore}</strong> | Монеты: <strong>${this.economy.totalCoins}</strong>`;
}
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.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 = `Победа! Все подсолнухи съедены! Мозги захвачены!<br>Награда: <strong>+100 💰 монет</strong> (Всего: ${this.economy.totalCoins})`;
} else if (this.currentMode === 'bossrush') {
statsEl.innerHTML = `Триумф! Режим Boss Rush успешно завершён!<br>Награда: <strong>+500 💰 монет</strong> &bull; <strong>+2 💎 кристалла</strong> (Всего: ${this.economy.totalCoins})`;
} else if (this.currentMode === 'adventure' && window.CampaignSystem) {
const nextInfo = window.CampaignSystem.formatLevel(this.currentLevel);
statsEl.innerHTML = `Победа! Финальный счёт: <strong>${this.score}</strong> (+1000 очков)<br>Награда за этап: <strong>+${levelReward} 💰 монет</strong> (Всего: ${this.economy.totalCoins})<br>Следующий этап: <strong>${nextInfo.fullLabel}</strong>`;
} else {
statsEl.innerHTML = `Победа! Финальный счёт: <strong>${this.score}</strong> (+1000 очков)<br>Награда: <strong>+100 💰 монет</strong> (Всего: ${this.economy.totalCoins})<br>Уровень разблокирован: <strong>${this.currentLevel}</strong>`;
}
}
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);
}
});
}
const startHeroProfile = document.getElementById('start-hero-profile');
if (startHeroProfile && btnAuthOpen) {
startHeroProfile.addEventListener('click', () => {
btnAuthOpen.click();
});
}
// Modal Close buttons
if (btnAuthClose && authModal) {
btnAuthClose.addEventListener('click', () => {
authModal.classList.add('hidden');
resetAuthForms();
});
}
if (btnAuthCloseProfile && authModal) {
btnAuthCloseProfile.addEventListener('click', () => {
authModal.classList.add('hidden');
});
}
if (btnAuthCloseReg && authModal) {
btnAuthCloseReg.addEventListener('click', () => {
authModal.classList.add('hidden');
resetAuthForms();
});
}
// Switch between Login and Register views
if (btnAuthToRegister && loginView && registerView) {
btnAuthToRegister.addEventListener('click', () => {
resetAuthForms();
loginView.classList.add('hidden');
registerView.classList.remove('hidden');
if (authRegUser) setTimeout(() => authRegUser.focus(), 50);
});
}
if (btnAuthToLogin && loginView && registerView) {
btnAuthToLogin.addEventListener('click', () => {
resetAuthForms();
registerView.classList.add('hidden');
loginView.classList.remove('hidden');
if (authUser) setTimeout(() => authUser.focus(), 50);
});
}
// Stay as Guest button
if (btnAuthGuest && authModal) {
btnAuthGuest.addEventListener('click', () => {
if (window.PvZAuth) window.PvZAuth.loginAsGuest();
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
authModal.classList.add('hidden');
resetAuthForms();
if (window.PvZAudio) window.PvZAudio.playButtonClick();
});
}
// Login Action (Войти)
if (btnAuthSubmit && authModal) {
btnAuthSubmit.addEventListener('click', async () => {
const u = authUser ? authUser.value.trim() : '';
const p = authPass ? authPass.value : '';
if (!u) {
if (authError) { authError.textContent = 'Введите логин'; authError.classList.remove('hidden'); }
return;
}
if (!p) {
if (authError) { authError.textContent = 'Введите пароль'; authError.classList.remove('hidden'); }
return;
}
if (authError) authError.classList.add('hidden');
if (window.PvZAuth) {
btnAuthSubmit.textContent = '⏳ Вход...';
btnAuthSubmit.disabled = true;
const res = await window.PvZAuth.login(u, p);
btnAuthSubmit.textContent = '🔑 Войти';
btnAuthSubmit.disabled = false;
if (res && res.success) {
authModal.classList.add('hidden');
resetAuthForms();
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
if (window.PvZAudio) window.PvZAudio.playCoin();
} else {
if (authError) {
authError.textContent = (res && res.message) ? res.message : 'Неверный логин или пароль';
authError.classList.remove('hidden');
}
}
}
});
}
// Register Action (Зарегистрироваться)
if (btnAuthDoRegister && authModal) {
btnAuthDoRegister.addEventListener('click', async () => {
const u = authRegUser ? authRegUser.value.trim() : '';
const p = authRegPass ? authRegPass.value : '';
if (!u) {
if (authRegError) { authRegError.textContent = 'Введите логин для регистрации'; authRegError.classList.remove('hidden'); }
return;
}
if (u.length < 3) {
if (authRegError) { authRegError.textContent = 'Логин должен содержать минимум 3 символа'; authRegError.classList.remove('hidden'); }
return;
}
if (!p || p.length < 4) {
if (authRegError) { authRegError.textContent = 'Пароль должен содержать минимум 4 символа'; authRegError.classList.remove('hidden'); }
return;
}
if (authRegError) authRegError.classList.add('hidden');
if (window.PvZAuth) {
btnAuthDoRegister.textContent = '⏳ Регистрация...';
btnAuthDoRegister.disabled = true;
const res = await window.PvZAuth.register(u, p);
btnAuthDoRegister.textContent = '✨ Зарегистрироваться';
btnAuthDoRegister.disabled = false;
if (res && res.success) {
authModal.classList.add('hidden');
resetAuthForms();
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
if (window.PvZAudio) window.PvZAudio.playCoin();
} else {
if (authRegError) {
authRegError.textContent = (res && res.message) ? res.message : 'Ошибка при регистрации';
authRegError.classList.remove('hidden');
}
}
}
});
}
// Export / Import Save File Handlers
const btnExportSave = document.getElementById('btn-export-save');
const btnExportSaveProfile = document.getElementById('btn-export-save-profile');
const btnImportSave = document.getElementById('btn-import-save');
const btnImportSaveProfile = document.getElementById('btn-import-save-profile');
const inputImportSave = document.getElementById('input-import-save');
const handleExportSave = () => {
this.saveUserData();
if (window.PvZAuth && window.PvZAuth.exportSaveFile) {
window.PvZAuth.exportSaveFile();
}
};
if (btnExportSave) btnExportSave.addEventListener('click', handleExportSave);
if (btnExportSaveProfile) btnExportSaveProfile.addEventListener('click', handleExportSave);
const handleTriggerImport = () => {
if (inputImportSave) {
inputImportSave.value = '';
inputImportSave.click();
}
};
if (btnImportSave) btnImportSave.addEventListener('click', handleTriggerImport);
if (btnImportSaveProfile) btnImportSaveProfile.addEventListener('click', handleTriggerImport);
if (inputImportSave) {
inputImportSave.addEventListener('change', async (e) => {
const file = e.target.files && e.target.files[0];
if (!file) return;
if (window.PvZAuth && window.PvZAuth.importSaveFile) {
const res = await window.PvZAuth.importSaveFile(file);
if (res && res.success) {
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
}
}
inputImportSave.value = '';
});
}
// 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 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
? `<button class="btn-play-level" data-level="${lvl.level}">ИГРАТЬ (ЭТАП ${lvl.code})</button>`
: `<button class="btn-play-level locked" disabled>🔒 ЗАКРЫТО</button>`;
const biomeNames = {
day: '☀️ День',
night: '🌙 Ночь',
pool: '🏊 Бассейн (6 линий)',
fog: '🌫️ Туман (6 линий)',
roof: '🏠 Крыша'
};
html += `
<div class="${cardClass}">
<div class="level-card-header">
<span class="level-code">${world.icon} Этап ${lvl.code}</span>
<span class="level-badge ${badgeClass}">${badgeText}</span>
</div>
<div class="level-card-body">
<div class="level-title">${lvl.title}</div>
<div class="level-desc">${lvl.desc}</div>
</div>
<div class="level-card-info">
<span>🌊 Волн: <strong>${lvl.waves}</strong></span>
<span>💰 Награда: <strong>+${lvl.reward}</strong></span>
<span>🏞️ ${biomeNames[lvl.biome] || lvl.biome}</span>
</div>
${buttonHtml}
</div>
`;
});
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 = `
<div class="slot-remove-badge">✕</div>
<div class="slot-icon">${p.icon}</div>
<div class="slot-cost">☀️ ${p.cost}</div>
`;
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 = '<span class="seed-inv-status status-deck">✓ В колоде</span>';
} else if (isUnlocked) {
statusBadge = '<span class="seed-inv-status status-ready">+ Выбрать</span>';
} else {
statusBadge = `<button type="button" class="btn btn-sm btn-shop" style="font-size: 10px; padding: 2px 6px; margin-top: 3px;" ${canAfford ? '' : 'disabled'}>🔒 ${p.price} 💰</button>`;
}
item.innerHTML = `
<div class="seed-inv-icon">${p.icon}</div>
<div class="seed-inv-name" title="${p.name}">${p.name}</div>
<div class="seed-inv-sun">☀️ ${p.cost}</div>
${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 canAfford = this.economy.totalCoins >= (p.price || 0);
const card = document.createElement('div');
card.className = 'shop-plant-card';
let actionHtml = '';
if (isBasic) {
actionHtml = '<span class="badge-purchased" style="background: rgba(33, 150, 243, 0.25); border-color: #64b5f6; color: #90caf9;">Базовое ✅</span>';
} else if (isUnlocked) {
actionHtml = '<span class="badge-purchased">Куплено ✅</span>';
} else {
actionHtml = `<button type="button" class="btn-buy" data-plant-id="${p.id}" ${canAfford ? '' : 'disabled'}>Купить (${p.price} 💰)</button>`;
}
card.innerHTML = `
<div class="shop-plant-icon">${p.icon}</div>
<div class="shop-plant-info">
<h4>${p.name}</h4>
<div class="shop-plant-cost">☀️ ${p.cost} солнца</div>
<p>${p.desc}</p>
</div>
<div>${actionHtml}</div>
`;
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(`<span class="cost-badge ${hasCoins ? '' : 'insufficient'}">💰 ${nextTier.cost.coins}</span>`);
}
if (nextTier.cost.bioHumus) {
const hasBio = this.workshop.materials.bioHumus >= nextTier.cost.bioHumus;
parts.push(`<span class="cost-badge ${hasBio ? '' : 'insufficient'}">🧪 ${nextTier.cost.bioHumus}</span>`);
}
if (nextTier.cost.scrapMetal) {
const hasScrap = this.workshop.materials.scrapMetal >= nextTier.cost.scrapMetal;
parts.push(`<span class="cost-badge ${hasScrap ? '' : 'insufficient'}">⚙️ ${nextTier.cost.scrapMetal}</span>`);
}
if (nextTier.cost.sunEssence) {
const hasSun = this.workshop.materials.sunEssence >= nextTier.cost.sunEssence;
parts.push(`<span class="cost-badge ${hasSun ? '' : 'insufficient'}">☀️ ${nextTier.cost.sunEssence}</span>`);
}
if (nextTier.cost.crystals) {
const hasCryst = this.workshop.materials.crystals >= nextTier.cost.crystals;
parts.push(`<span class="cost-badge ${hasCryst ? '' : 'insufficient'}">💎 ${nextTier.cost.crystals}</span>`);
}
costHtml = parts.join(' ');
}
const stars = lvl === 3 ? '★★★' : (lvl === 2 ? '★★☆' : '★☆☆');
const badgeClass = lvl === 3 ? 'upgrade-level-badge maxed' : 'upgrade-level-badge';
card.innerHTML = `
<div class="upgrade-card-header">
<div class="upgrade-title"><span>${def.icon}</span> <span>${def.name}</span></div>
<div class="${badgeClass}">${stars} Ур. ${lvl}</div>
</div>
<div class="upgrade-desc">
<strong>Текущий эффект:</strong> ${curTier.desc}
</div>
${nextTier ? `
<div class="upgrade-stats-next">
<strong>След. ранг:</strong> ${nextTier.title} — ${nextTier.desc}
</div>
<div class="upgrade-cost-row">
<div class="cost-items">${costHtml}</div>
<button class="btn-upgrade-action" ${canAfford ? '' : 'disabled'} data-upgrade-plant="${id}">
Улучшить
</button>
</div>
` : `
<div class="upgrade-stats-next" style="border-left-color: #ffd700; color: #ffe082;">
Максимальный 3-й уровень разблокирован!
</div>
`}
`;
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(`<span class="cost-badge ${hasCoins ? '' : 'insufficient'}">💰 ${nextTier.cost.coins}</span>`);
}
if (nextTier.cost.bioHumus) {
const hasBio = this.workshop.materials.bioHumus >= nextTier.cost.bioHumus;
parts.push(`<span class="cost-badge ${hasBio ? '' : 'insufficient'}">🧪 ${nextTier.cost.bioHumus}</span>`);
}
if (nextTier.cost.scrapMetal) {
const hasScrap = this.workshop.materials.scrapMetal >= nextTier.cost.scrapMetal;
parts.push(`<span class="cost-badge ${hasScrap ? '' : 'insufficient'}">⚙️ ${nextTier.cost.scrapMetal}</span>`);
}
if (nextTier.cost.sunEssence) {
const hasSun = this.workshop.materials.sunEssence >= nextTier.cost.sunEssence;
parts.push(`<span class="cost-badge ${hasSun ? '' : 'insufficient'}">☀️ ${nextTier.cost.sunEssence}</span>`);
}
if (nextTier.cost.crystals) {
const hasCryst = this.workshop.materials.crystals >= nextTier.cost.crystals;
parts.push(`<span class="cost-badge ${hasCryst ? '' : 'insufficient'}">💎 ${nextTier.cost.crystals}</span>`);
}
costHtml = parts.join(' ');
}
const stars = lvl === 3 ? '★★★' : (lvl === 2 ? '★★☆' : (lvl === 1 ? '★☆☆' : '☆☆☆'));
const badgeClass = lvl === 3 ? 'upgrade-level-badge maxed' : 'upgrade-level-badge';
card.innerHTML = `
<div class="upgrade-card-header">
<div class="upgrade-title"><span>${def.icon}</span> <span>${def.name}</span></div>
<div class="${badgeClass}">${stars} Ур. ${lvl}</div>
</div>
<div class="upgrade-desc">
${def.desc}. ${curTier ? `<strong>(Сейчас: ${curTier.desc})</strong>` : '<em>(Не изучен)</em>'}
</div>
${nextTier ? `
<div class="upgrade-stats-next">
<strong>След. уровень:</strong> ${nextTier.desc}
</div>
<div class="upgrade-cost-row">
<div class="cost-items">${costHtml}</div>
<button class="btn-upgrade-action" ${canAfford ? '' : 'disabled'} data-upgrade-perk="${id}">
Изучить
</button>
</div>
` : `
<div class="upgrade-stats-next" style="border-left-color: #ffd700; color: #ffe082;">
Перк полностью освоен!
</div>
`}
`;
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 = `
<div class="almanac-slot-icon">${item.icon}</div>
<div class="almanac-slot-name">${item.name}</div>
`;
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 = `
<div class="almanac-stat-row"><span>Стоимость:</span> <strong>☀️ ${selectedItem.cost}</strong></div>
<div class="almanac-stat-row"><span>Перезарядка:</span> <strong>${selectedItem.cooldown}</strong></div>
<div class="almanac-stat-row"><span>Урон:</span> <strong>${selectedItem.damage}</strong></div>
<div class="almanac-stat-row"><span>Прочность:</span> <strong>${selectedItem.toughness}</strong></div>
`;
} else {
statsHtml = `
<div class="almanac-stat-row"><span>Скорость:</span> <strong>${selectedItem.speed}</strong></div>
<div class="almanac-stat-row"><span>Прочность:</span> <strong>${selectedItem.toughness}</strong></div>
`;
}
detailsContainer.innerHTML = `
<div class="almanac-detail-title">
<div class="almanac-detail-icon">${selectedItem.icon}</div>
<div class="almanac-detail-name">${selectedItem.name}</div>
</div>
<div class="almanac-stats-box" style="display: flex; flex-direction: column; gap: 4px;">
${statsHtml}
</div>
<div class="almanac-desc-text">
${selectedItem.desc}
</div>
<div class="almanac-lore-quote">
${selectedItem.lore}
</div>
`;
}
}
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 += ` &bull; 💎 +${ach.reward.crystals}`;
}
let btnHtml = '';
if (isClaimed) {
btnHtml = `<button class="btn btn-secondary" style="font-size: 11px; padding: 4px 10px; cursor: default; opacity: 0.6;" disabled>Получено ✓</button>`;
} else if (isDone) {
btnHtml = `<button class="btn btn-claim-reward" data-ach="${ach.id}" style="font-size: 11px; padding: 4px 10px; background: #ffd54f; color: #1b5e20; font-weight: bold;">Забрать 🎁</button>`;
} else {
btnHtml = `<button class="btn btn-secondary" style="font-size: 11px; padding: 4px 8px; opacity: 0.5;" disabled>${ach.progress} / ${ach.max}</button>`;
}
card.innerHTML = `
<div class="ach-icon">${ach.icon}</div>
<div class="ach-info">
<div class="ach-title">${ach.title}</div>
<div class="ach-desc">${ach.desc}</div>
<div class="ach-progress-bar">
<div class="ach-progress-fill" style="width: ${pct}%;"></div>
</div>
<div class="ach-reward-tag">Награда: ${rewardText}</div>
</div>
<div class="ach-action">
${btnHtml}
</div>
`;
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 = `
<div class="tree-cheat-info">
<span style="font-size: 18px; margin-right: 6px;">${c.icon}</span>
<strong>${c.name}</strong>
<div style="font-size: 11px; opacity: 0.8;">
${isUnlocked ? (isActive ? 'Статус: ВКЛЮЧЕН ✅' : 'Статус: Выключен') : `Требуется: ${c.reqHeight} футов 🔒`}
</div>
</div>
<button class="btn" style="font-size: 11px; padding: 4px 10px; ${isActive ? 'background: #e91e63;' : ''}" ${isUnlocked ? '' : 'disabled'}>
${isUnlocked ? (isActive ? 'Выкл' : 'Вкл') : '🔒'}
</button>
`;
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 = `<div style="text-align: center; color: #ffe082; padding: 24px; font-size: 14px;">Пока нет записей. Сыграйте в этот режим, чтобы установить первый рекорд!</div>`;
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 += `
<tr class="${index < 3 ? 'top-rank' : ''}">
<td style="font-weight: bold; width: 60px; text-align: center;">${medal}</td>
<td style="font-weight: bold; color: #fff;">${item.name}</td>
<td style="color: #ffd54f; font-weight: bold;">${item.detail || item.score}</td>
<td style="color: #b0bec5; font-size: 11px; text-align: right;">${item.date || ''}</td>
</tr>
`;
});
contentEl.innerHTML = `
<table class="leaderboard-table">
<thead>
<tr>
<th style="width: 60px; text-align: center;">Место</th>
<th>Игрок</th>
<th>Результат</th>
<th style="text-align: right;">Дата</th>
</tr>
</thead>
<tbody>
${rowsHtml}
</tbody>
</table>
`;
}
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 = `
<div class="draft-choice-type">${choice.type === 'relic' ? '✨ РЕЛИКВИЯ' : '🌱 СЕМЯ РАСТЕНИЯ'}</div>
<div class="draft-choice-icon">${choice.icon}</div>
<div class="draft-choice-title">${choice.name}</div>
<div class="draft-choice-desc">${choice.desc}</div>
<button class="btn draft-choice-btn" style="margin-top: 10px; width: 100%;">Выбрать</button>
`;
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 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();
// 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();
}
};