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

3903 lines
147 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();
}
if (window.CasinoSystem) {
this.casino = new window.CasinoSystem(this.economy);
}
if (window.ZombieLabSystem) {
this.zombieLab = new window.ZombieLabSystem(this.economy, this.workshop);
}
this.wave = new window.WaveSystem();
this.combat = new window.CombatSystem();
if (window.ProjectilePool) {
this.projectilePool = new window.ProjectilePool(60);
}
// Entities
this.plants = [];
this.zombies = [];
this.projectiles = [];
// State
this.currentMode = 'adventure'; // 'adventure', 'survival', 'bowling', 'izombie', 'roguelike'
this.currentLevel = 1;
this.maxUnlockedLevel = 1;
this.isRunning = false;
this.isGameOver = false;
this.isVictory = false;
this.gameSpeed = 1.0;
this.isPaused = false;
this.lastTime = 0;
this.hoverCell = null;
this.loopStarted = false;
this.izombieSetupDone = false;
// Rake state
this.hasRakeArmed = false;
this.rakeCol = 7;
this.rakeRow = 2;
// Score & Persistence
this.score = 0;
this.zombiesKilled = 0;
this.stats = {
zombiesKilled: 0,
sunProduced: 0,
plantsPlanted: 0,
levelsCompleted: 0,
coinsEarned: 0
};
this.loadUserData();
this.updateRecordDisplay();
this.updateProfileUI();
// Register with Test API
if (window.PvZTestAPI) {
window.PvZTestAPI.setEngine(this);
}
window._pvzEngine = this; // allow auth.js to trigger profile reload
// Dynamic profile sync event listener
window.addEventListener('pvz:profile-updated', () => {
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
});
// Bind events
this.bindEvents();
this.bindUI();
}
setupHiDPI() {
this.dpr = Math.min(window.devicePixelRatio || 1, 2.5);
this.canvas.width = this.logicalWidth * this.dpr;
this.canvas.height = this.logicalHeight * this.dpr;
this.ctx.resetTransform();
this.ctx.scale(this.dpr, this.dpr);
}
loadUserData() {
if (window.PvZAuth) {
const uData = window.PvZAuth.getUserData();
this.currentLevel = uData.level || 1;
this.maxUnlockedLevel = uData.maxUnlockedLevel || Math.max(this.currentLevel, 1);
this.highScore = uData.highScore || 0;
this.currentTheme = uData.theme || 'classic';
this.stats = uData.stats || {
zombiesKilled: 0,
sunProduced: 0,
plantsPlanted: 0,
levelsCompleted: 0,
coinsEarned: 0
};
this.applyTheme(this.currentTheme);
if (this.economy) this.economy.reloadProfileData();
if (this.workshop) this.workshop.loadFromAuth();
if (this.zengarden) this.zengarden.loadFromAuth();
if (this.achievements) this.achievements.loadFromAuth();
if (this.tree) this.tree.loadFromAuth();
if (this.leaderboards) this.leaderboards.loadFromAuth();
if (this.casino) this.casino.loadFromAuth();
if (this.zombieLab) this.zombieLab.loadFromAuth();
} else {
this.currentLevel = parseInt(localStorage.getItem('pvz_level') || '1', 10);
this.maxUnlockedLevel = parseInt(localStorage.getItem('pvz_max_level') || this.currentLevel.toString(), 10);
this.highScore = parseInt(localStorage.getItem('pvz_highscore') || '0', 10);
this.currentTheme = localStorage.getItem('pvz_theme') || 'classic';
try {
const rawStats = localStorage.getItem('pvz_stats');
this.stats = rawStats ? JSON.parse(rawStats) : {
zombiesKilled: 0,
sunProduced: 0,
plantsPlanted: 0,
levelsCompleted: 0,
coinsEarned: 0
};
} catch (_) {
this.stats = { zombiesKilled: 0, sunProduced: 0, plantsPlanted: 0, levelsCompleted: 0, coinsEarned: 0 };
}
this.applyTheme(this.currentTheme);
}
}
recordStat(key, amount = 1) {
if (!this.stats) {
this.stats = { zombiesKilled: 0, sunProduced: 0, plantsPlanted: 0, levelsCompleted: 0, coinsEarned: 0 };
}
this.stats[key] = (this.stats[key] || 0) + amount;
this.saveUserData();
}
saveUserData() {
if (window.PvZAuth) {
if (this.workshop) this.workshop.saveToAuth();
if (this.achievements) this.achievements.saveToAuth();
if (this.tree) this.tree.saveToAuth();
if (this.leaderboards) this.leaderboards.saveToAuth();
if (this.casino) this.casino.saveToAuth();
if (this.zombieLab) this.zombieLab.saveToAuth();
window.PvZAuth.saveUserData({
coins: this.economy ? this.economy.totalCoins : undefined,
level: this.currentLevel,
maxUnlockedLevel: this.maxUnlockedLevel,
highScore: this.highScore,
theme: this.currentTheme,
stats: this.stats,
upgrades: this.economy ? this.economy.upgrades : undefined,
materials: this.workshop ? this.workshop.materials : undefined,
plantLevels: this.workshop ? this.workshop.plantLevels : undefined,
perks: this.workshop ? this.workshop.perks : undefined,
zenGarden: this.zengarden ? {
pots: this.zengarden.pots,
fertilizerCount: this.zengarden.fertilizerCount,
chocolateCount: this.zengarden.chocolateCount,
snail: this.zengarden.snail,
lastVisit: Date.now()
} : undefined
});
}
localStorage.setItem('pvz_level', this.currentLevel.toString());
localStorage.setItem('pvz_max_level', this.maxUnlockedLevel.toString());
localStorage.setItem('pvz_highscore', this.highScore.toString());
localStorage.setItem('pvz_theme', this.currentTheme);
localStorage.setItem('pvz_stats', JSON.stringify(this.stats));
if (this.economy) {
localStorage.setItem('pvz_coins', this.economy.totalCoins.toString());
}
}
applyTheme(theme) {
this.currentTheme = theme || 'classic';
const body = document.body;
body.classList.remove('theme-cyberpunk', 'theme-retro8bit', 'theme-winter');
if (this.currentTheme !== 'classic') {
body.classList.add(`theme-${this.currentTheme}`);
}
const btnTheme = document.getElementById('btn-theme');
if (btnTheme) {
const labels = {
classic: '🎨 Классика',
cyberpunk: '🎨 Киберпанк',
retro8bit: '🎨 Ретро 8-бит',
winter: '🎨 Зима'
};
btnTheme.textContent = labels[this.currentTheme] || '🎨 Тема';
}
if (this.grid) {
this.grid.theme = this.currentTheme;
}
}
cycleTheme() {
const themes = ['classic', 'cyberpunk', 'retro8bit', 'winter'];
const nextIdx = (themes.indexOf(this.currentTheme) + 1) % themes.length;
this.applyTheme(themes[nextIdx]);
this.saveUserData();
if (window.PvZAudio) window.PvZAudio.playCoin();
}
togglePause() {
if (!this.isRunning) return;
this.isPaused = !this.isPaused;
const btnPause = document.getElementById('btn-pause');
if (btnPause) {
btnPause.textContent = this.isPaused ? '▶️ Играть' : '⏸️ Пауза';
if (this.isPaused) btnPause.classList.add('active');
else btnPause.classList.remove('active');
}
this.triggerHaptic(15);
if (window.PvZAudio) window.PvZAudio.playButtonClick();
}
updateProfileUI() {
const badgeName = document.getElementById('badge-user-name');
const btnAuth = document.getElementById('btn-auth-open');
const btnLogoutNav = document.getElementById('btn-auth-logout-nav');
const loggedInView = document.getElementById('auth-logged-in-view');
const loginView = document.getElementById('auth-login-view') || document.getElementById('auth-guest-view');
const registerView = document.getElementById('auth-register-view');
const userTitle = document.getElementById('profile-user-title');
const statCoins = document.getElementById('profile-stat-coins');
const statLevel = document.getElementById('profile-stat-level');
const statScore = document.getElementById('profile-stat-score');
const statMaterials = document.getElementById('profile-stat-materials');
// Modern Dashboard Hero Profile elements
const heroName = document.getElementById('hero-profile-name');
const heroCloud = document.getElementById('hero-cloud-badge');
const heroCoins = document.getElementById('hero-stat-coins');
const heroLevel = document.getElementById('hero-stat-level');
const heroScore = document.getElementById('hero-stat-score');
if (window.PvZAuth) {
const user = window.PvZAuth.getCurrentUser();
const isGuest = window.PvZAuth.isGuest();
const uData = window.PvZAuth.getUserData() || {};
const currentCoins = uData.coins !== undefined ? uData.coins : (this.economy ? this.economy.totalCoins : 0);
const currentLevelNum = uData.level !== undefined ? uData.level : (this.currentLevel || 1);
const currentScoreNum = uData.highScore !== undefined ? uData.highScore : (this.highScore || 0);
if (badgeName) badgeName.textContent = isGuest ? '👤 Гость' : `👤 ${user}`;
if (btnAuth) btnAuth.textContent = isGuest ? 'Войти' : 'Профиль';
if (heroName) heroName.textContent = isGuest ? 'Гость' : user;
if (heroCloud) {
heroCloud.textContent = isGuest ? 'Локально' : 'Облако';
heroCloud.className = isGuest ? 'badge-offline' : 'badge-online';
}
if (heroCoins) heroCoins.textContent = currentCoins.toLocaleString('ru-RU');
if (heroLevel) heroLevel.textContent = `Ур. ${currentLevelNum}`;
if (heroScore) heroScore.textContent = currentScoreNum.toLocaleString('ru-RU');
if (btnLogoutNav) {
if (isGuest) btnLogoutNav.classList.add('hidden');
else btnLogoutNav.classList.remove('hidden');
}
if (loggedInView) {
if (isGuest) {
loggedInView.classList.add('hidden');
if (loginView) loginView.classList.remove('hidden');
if (registerView) registerView.classList.add('hidden');
} else {
loggedInView.classList.remove('hidden');
if (loginView) loginView.classList.add('hidden');
if (registerView) registerView.classList.add('hidden');
if (userTitle) userTitle.textContent = user;
if (statCoins) statCoins.textContent = currentCoins.toLocaleString('ru-RU');
if (statLevel) statLevel.textContent = `Уровень ${currentLevelNum}`;
if (statScore) statScore.textContent = currentScoreNum.toLocaleString('ru-RU');
if (statMaterials) {
const mats = uData.materials || (this.workshop ? this.workshop.materials : {});
const totalMats = (mats.bioHumus || 0) + (mats.scrapMetal || 0) + (mats.sunEssence || 0) + (mats.crystals || 0);
statMaterials.textContent = `${totalMats} шт.`;
}
// Extended lifetime player stats
const stats = uData.stats || this.stats || {};
const statZombies = document.getElementById('profile-stat-zombies');
const statSun = document.getElementById('profile-stat-sun');
const statPlants = document.getElementById('profile-stat-plants');
const statLevelsWon = document.getElementById('profile-stat-levels-won');
const statCoinsEarned = document.getElementById('profile-stat-coins-earned');
if (statZombies) statZombies.textContent = (stats.zombiesKilled || 0).toLocaleString('ru-RU');
if (statSun) statSun.textContent = (stats.sunProduced || 0).toLocaleString('ru-RU');
if (statPlants) statPlants.textContent = (stats.plantsPlanted || 0).toLocaleString('ru-RU');
if (statLevelsWon) statLevelsWon.textContent = (stats.levelsCompleted || 0).toLocaleString('ru-RU');
if (statCoinsEarned) statCoinsEarned.textContent = (stats.coinsEarned || 0).toLocaleString('ru-RU');
this.renderUserSessionsUI();
}
}
}
}
async renderUserSessionsUI() {
const listEl = document.getElementById('auth-sessions-list');
if (!listEl) return;
if (!window.PvZAuth || window.PvZAuth.isGuest()) {
listEl.innerHTML = '<div style="font-size: 12px; color: #b0bec5; text-align: center; padding: 8px;">Войдите в аккаунт для просмотра сессий</div>';
return;
}
const sessions = await window.PvZAuth.fetchSessions();
if (!sessions || sessions.length === 0) {
listEl.innerHTML = '<div style="font-size: 12px; color: #b0bec5; text-align: center; padding: 8px;">Нет активных сессий</div>';
return;
}
listEl.innerHTML = '';
sessions.forEach(s => {
const card = document.createElement('div');
card.className = `session-card ${s.isCurrent ? 'is-current' : ''}`;
// Choose icon based on device name
let icon = '🖥️';
const dName = (s.deviceName || '').toLowerCase();
if (dName.includes('iphone') || dName.includes('android') || dName.includes('phone') || dName.includes('mobile')) {
icon = '📱';
} else if (dName.includes('ipad') || dName.includes('tablet')) {
icon = '📟';
} else if (dName.includes('mac') || dName.includes('apple')) {
icon = '🍎';
} else if (dName.includes('windows') || dName.includes('linux') || dName.includes('chrome') || dName.includes('pc')) {
icon = '💻';
}
const iconDiv = document.createElement('div');
iconDiv.className = 'session-icon';
iconDiv.textContent = icon;
const detailsDiv = document.createElement('div');
detailsDiv.className = 'session-details';
const devNameDiv = document.createElement('div');
devNameDiv.className = 'session-dev-name';
devNameDiv.textContent = s.deviceName || 'Браузер / Компьютер';
const metaDiv = document.createElement('div');
metaDiv.className = 'session-meta';
const ipSpan = document.createElement('span');
ipSpan.textContent = `IP: ${s.ipAddress || '127.0.0.1'}`;
const activeSpan = document.createElement('span');
activeSpan.textContent = `Активность: ${s.lastActive || s.createdAt || 'Только что'}`;
metaDiv.appendChild(ipSpan);
metaDiv.appendChild(activeSpan);
detailsDiv.appendChild(devNameDiv);
detailsDiv.appendChild(metaDiv);
card.appendChild(iconDiv);
card.appendChild(detailsDiv);
if (s.isCurrent) {
const badge = document.createElement('span');
badge.className = 'session-current-badge';
badge.textContent = '🟢 Текущее устройство';
card.appendChild(badge);
} else {
const btnTerm = document.createElement('button');
btnTerm.type = 'button';
btnTerm.className = 'btn btn-danger btn-terminate-session';
btnTerm.textContent = 'Завершить';
btnTerm.addEventListener('click', async (e) => {
e.stopPropagation();
btnTerm.disabled = true;
btnTerm.textContent = '⏳';
const res = await window.PvZAuth.terminateSession(s.token);
if (res && res.success) {
if (window.PvZAuth && window.PvZAuth.showNotification) {
window.PvZAuth.showNotification('Сессия устройства завершена', s.deviceName, '🔒');
}
} else {
if (window.PvZAuth && window.PvZAuth.showNotification) {
window.PvZAuth.showNotification('Ошибка завершения сессии', res?.message || 'Сбой', '❌');
}
}
await this.renderUserSessionsUI();
});
card.appendChild(btnTerm);
}
listEl.appendChild(card);
});
}
openMainMenu() {
this.isRunning = false;
if (window.PvZAudio) window.PvZAudio.stopMusic();
const startModal = document.getElementById('start-modal');
if (startModal) startModal.classList.remove('hidden');
// Close any game-phase modals that might have been left open
['game-over-modal', 'victory-modal', 'draft-modal', 'zengarden-modal',
'workshop-modal', 'leaderboard-modal', 'achievement-modal', 'almanac-modal',
'bugreport-modal', 'campaign-modal', 'tree-modal', 'casino-modal',
'seed-select-modal', 'dave-modal', 'shop-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;
// Ensure all pre-battle and menu overlays are hidden
['seed-select-modal', 'dave-modal', 'start-modal', 'campaign-modal', 'victory-modal', 'game-over-modal', 'shop-modal'].forEach(id => {
const el = document.getElementById(id);
if (el) el.classList.add('hidden');
});
// Pre-set economy mode flags BEFORE reset() so economy.reset() computes
// the correct baseSun (avoids stale isConveyor/isIZombie from previous game)
this.economy.isConveyor = (mode === 'bowling' || mode === 'boss' || mode === 'bossrush');
this.economy.isIZombie = (mode === 'izombie');
this.economy.isNight = (mode === 'izombie');
this.reset();
this.wave.setLevel(level, mode);
this.grid.initBiome(this.wave.biome);
if (mode === 'bowling') {
this.economy.setConveyorDeck();
this.hasRakeArmed = false;
} else if (mode === 'izombie') {
this.economy.setIZombieDeck();
this.grid.lawnmowers = [];
this.hasRakeArmed = false;
this.setupIZombieLawn(); // place lawn plants for I, Zombie mode
} else if (mode === 'boss' || mode === 'bossrush') {
this.economy.setBossDeck();
if (this.boss) this.boss.reset();
this.hasRakeArmed = false;
} else if (mode === 'roguelike') {
this.economy.isConveyor = false;
this.economy.isIZombie = false;
if (!this.economy.cards || this.economy.cards.length === 0 || (this.roguelike && this.roguelike.currentFloor === 1) || this.economy.cards.some(c => c.id.startsWith('z_') || c.id.startsWith('bowling_'))) {
this.economy.cards = this.economy.allCards
.filter(c => ['sunflower', 'peashooter'].includes(c.id))
.map(c => ({ ...c, timer: 0 }));
}
this.hasRakeArmed = this.economy.upgrades.hasRake;
this.rakeRow = Math.floor(Math.random() * this.grid.rows);
this.rakeCol = 7;
} else {
this.economy.setDeckForLevel(level, this.wave.biome);
this.hasRakeArmed = this.economy.upgrades.hasRake;
this.rakeRow = Math.floor(Math.random() * this.grid.rows);
this.rakeCol = 7;
}
if (this.wave && this.wave.startingSun !== null && this.wave.startingSun !== undefined && this.wave.startingSun >= 0) {
this.economy.sun = this.wave.startingSun;
}
this.isRunning = true;
this.lastTime = performance.now();
const musicTheme = (mode === 'boss' || mode === 'bossrush') ? 'boss' : this.wave.biome;
if (window.PvZAudio) window.PvZAudio.startMusic(musicTheme);
}
setupIZombieLawn(stage) {
this.plants = [];
this.izombieSetupDone = true;
const currentStage = stage || (this.zombieLab ? (this.zombieLab.selectedStage || this.zombieLab.stage || 1) : 1);
const addPlant = (type, col, row, options = {}) => {
const b = this.grid.getCellBounds(col, row);
const p = new window.Plant(type, col, row, b.x, b.y, b.w, b.h);
if (options.isArmed) {
p.isArmed = true;
p.armTimer = 15;
}
this.grid.setPlant(col, row, p);
this.plants.push(p);
return p;
};
if (currentStage === 5) {
// Stage 5: RAID BOSS - Mega Sunflower (5000 HP) with Royal Defense!
for (let r = 0; r < 5; r++) {
if (r === 2) {
addPlant('mega_sunflower', 0, r); // Center lane Raid Boss!
} else {
addPlant('sunflower', 0, r);
}
}
addPlant('snowpea', 2, 0);
addPlant('tallnut', 4, 0);
addPlant('repeater', 1, 1);
addPlant('repeater', 3, 1);
addPlant('tallnut', 4, 1);
// Lane 2: The Boss Lane
addPlant('repeater', 1, 2);
addPlant('snowpea', 2, 2);
addPlant('tallnut', 4, 2);
addPlant('repeater', 1, 3);
addPlant('repeater', 3, 3);
addPlant('tallnut', 4, 3);
addPlant('snowpea', 2, 4);
addPlant('tallnut', 4, 4);
} else if (currentStage === 4) {
// Stage 4: The Great Wall (Tallnuts & repeaters)
for (let r = 0; r < 5; r++) {
addPlant('sunflower', 0, r);
addPlant('repeater', 1, r);
addPlant('tallnut', 3, r);
}
addPlant('potatomine', 4, 1, { isArmed: true });
addPlant('potatomine', 4, 3, { isArmed: true });
} else if (currentStage === 3) {
// Stage 3: Frost & Thorns (Snowpeas & armed mines)
for (let r = 0; r < 5; r++) {
addPlant('sunflower', 0, r);
}
addPlant('snowpea', 2, 0);
addPlant('wallnut', 4, 0);
addPlant('repeater', 1, 1);
addPlant('snowpea', 3, 1);
addPlant('potatomine', 4, 1, { isArmed: true });
addPlant('snowpea', 2, 2);
addPlant('tallnut', 4, 2);
addPlant('repeater', 1, 3);
addPlant('potatomine', 3, 3, { isArmed: true });
addPlant('wallnut', 4, 3);
addPlant('snowpea', 2, 4);
addPlant('chomper', 4, 4);
} else if (currentStage === 2) {
// Stage 2: Night Guard (Mushrooms & Peashooters)
for (let r = 0; r < 5; r++) {
addPlant('sunflower', 0, r);
}
addPlant('peashooter', 1, 0);
addPlant('fumeshroom', 3, 0);
addPlant('snowpea', 2, 1);
addPlant('wallnut', 4, 1);
addPlant('repeater', 1, 2);
addPlant('repeater', 3, 2);
addPlant('wallnut', 5, 2);
addPlant('fumeshroom', 2, 3);
addPlant('chomper', 4, 3);
addPlant('peashooter', 1, 4);
addPlant('potatomine', 3, 4, { isArmed: true });
} else {
// Stage 1: Classic Lawn
for (let r = 0; r < 5; r++) {
addPlant('sunflower', 0, r);
}
addPlant('peashooter', 2, 0);
addPlant('repeater', 2, 1);
addPlant('wallnut', 4, 1);
addPlant('snowpea', 1, 2);
addPlant('peashooter', 3, 2);
addPlant('peashooter', 2, 3);
addPlant('wallnut', 4, 3);
addPlant('repeater', 1, 4);
addPlant('potatomine', 3, 4, { isArmed: true });
}
}
showDaveDialogue(level, mode, onContinue) {
const dialogue = window.CampaignSystem ? window.CampaignSystem.getDaveDialogue(level, mode) : null;
if (!dialogue) {
if (typeof onContinue === 'function') onContinue();
return;
}
const modal = document.getElementById('dave-modal');
const speakerEl = document.getElementById('dave-modal-speaker');
const subEl = document.getElementById('dave-modal-subtitle');
const textEl = document.getElementById('dave-modal-text');
const btnContinue = document.getElementById('btn-dave-continue');
if (speakerEl) speakerEl.textContent = dialogue.title || 'Безумный Дэйв';
if (subEl) subEl.textContent = 'Твой чудаковатый сосед';
if (textEl) textEl.textContent = `«${dialogue.text}»`;
if (btnContinue && dialogue.btnText) btnContinue.textContent = dialogue.btnText;
if (modal) modal.classList.remove('hidden');
const handleContinue = () => {
if (modal) modal.classList.add('hidden');
if (window.PvZAudio) window.PvZAudio.playButtonClick();
if (typeof onContinue === 'function') onContinue();
};
if (btnContinue) {
const newBtn = btnContinue.cloneNode(true);
btnContinue.parentNode.replaceChild(newBtn, btnContinue);
newBtn.addEventListener('click', handleContinue);
}
}
init() {
let startLvl = 1;
if (this.currentMode === 'adventure') {
startLvl = this.currentLevel || 1;
} else if (this.currentMode === 'roguelike') {
startLvl = this.roguelike ? this.roguelike.currentFloor : 1;
}
this.showDaveDialogue(startLvl, this.currentMode, () => {
// Fixed-conveyor mini-games launch immediately, standard modes open deck builder
if (this.currentMode === 'bowling' || this.currentMode === 'izombie' || this.currentMode === 'boss' || this.currentMode === 'bossrush') {
this.startLevel(startLvl, this.currentMode);
} else {
this.openSeedSelectionModal(startLvl, this.currentMode);
}
if (!this.loopStarted) {
this.loopStarted = true;
requestAnimationFrame(this.loop.bind(this));
}
});
}
reset() {
this.grid.reset();
this.economy.reset();
this.wave.reset();
this.combat.reset();
if (this.projectilePool) {
this.projectilePool.releaseAll(this.projectiles);
}
this.plants = [];
this.zombies = [];
this.projectiles = [];
this.score = 0;
this.zombiesKilled = 0;
this.isGameOver = false;
this.isVictory = false;
this.hoverCell = null;
this.izombieSetupDone = false; // ensure clean re-init for I, Zombie mode
document.getElementById('game-over-modal').classList.add('hidden');
document.getElementById('victory-modal').classList.add('hidden');
}
spawnProjectile(x, y, row, type = 'pea', vy = 0, damage = 20, isCrit = false, targetX = 750) {
const proj = this.projectilePool ? this.projectilePool.get(x, y, row, type, vy, damage, isCrit, targetX) : new window.Projectile(x, y, row, type, vy, damage, isCrit, targetX);
this.projectiles.push(proj);
return proj;
}
spawnSun(x, y, value = 25, isFalling = false, targetY = null) {
let finalValue = value;
if (this.roguelike && this.currentMode === 'roguelike' && this.roguelike.hasRelic('relic_sun_resonance') && !isFalling) {
finalValue += 15;
}
if (!isFalling) {
this.recordStat('sunProduced', finalValue);
}
this.economy.spawnSun(x, y, finalValue, isFalling, targetY);
}
spawnCoin(x, y, type = 'silver') {
this.economy.spawnCoin(x, y, type);
}
spawnZombie(row, type = 'normal') {
const zombie = new window.Zombie(row, type);
const bounds = this.grid.getCellBounds(0, row);
zombie.y = bounds.centerY - 5;
if (this.wave) {
if (this.wave.hpMultiplier && this.wave.hpMultiplier !== 1.0) {
zombie.maxHp = Math.round(zombie.maxHp * this.wave.hpMultiplier);
zombie.hp = Math.round(zombie.hp * this.wave.hpMultiplier);
}
if (this.wave.speedMultiplier && this.wave.speedMultiplier !== 1.0) {
zombie.speed *= this.wave.speedMultiplier;
}
}
this.zombies.push(zombie);
}
explodeArea(centerCol, centerRow, radiusTiles = 3) {
const half = Math.floor(radiusTiles / 2);
const bounds = this.grid.getCellBounds(centerCol, centerRow);
this.combat.createExplosionParticles(bounds.centerX, bounds.centerY);
for (const z of this.zombies) {
if (z.toRemove || z.health <= 0) continue;
const rowDiff = Math.abs(z.row - centerRow);
const colEstimated = (z.x - this.grid.startX) / this.grid.cellW;
const colDiff = Math.abs(colEstimated - centerCol);
if (rowDiff <= half && colDiff <= half + 0.5) {
z.takeDamage(1800);
if (z.health <= 0) {
this.addScore(250);
this.zombiesKilled++;
this.recordStat('zombiesKilled', 1);
if (Math.random() < 0.4) this.spawnCoin(z.x, z.y, 'gold');
}
}
}
}
triggerGameOver(reason = 'Зомби прорвали оборону газона. Попробуйте снова!') {
if (this.isGameOver) return;
this.isGameOver = true;
this.isRunning = false;
if (window.PvZAudio) {
window.PvZAudio.stopMusic();
window.PvZAudio.playLose();
}
if (this.leaderboards) {
if (this.currentMode === 'survival') {
this.leaderboards.submitScore('survival', this.wave.survivalFlags || 0, `${this.wave.survivalFlags || 0} Флагов`);
} else if (this.currentMode === 'roguelike') {
const flr = this.roguelike ? this.roguelike.currentFloor : 1;
this.leaderboards.submitScore('roguelike', flr, `${flr} Этаж`);
} else {
this.leaderboards.submitScore(this.currentMode, this.score, `${this.score} очков`);
}
}
this.saveUserData();
if (window.PvZAuth && window.PvZAuth.flushSave) window.PvZAuth.flushSave();
const descEl = document.getElementById('game-over-desc');
if (descEl) descEl.textContent = reason;
const statsEl = document.getElementById('game-over-stats');
if (statsEl) {
statsEl.innerHTML = `Счёт: <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.used && !lm.active)) {
if (this.achievements) this.achievements.trigger('clean_lawn');
}
if (this.currentMode === 'izombie' && this.achievements) {
this.achievements.trigger('izombie_win');
}
if (this.leaderboards) {
if (this.currentMode === 'bossrush') {
this.leaderboards.submitScore('bossrush', 3, 'Boss Rush пройден! (3 волны)');
this.economy.addCoins(400); // 100 + 400 = 500 bonus
if (this.workshop) {
this.workshop.materials.crystals += 2;
}
} else if (this.currentMode === 'boss') {
this.leaderboards.submitScore('boss', 1, 'Победа над Зомбоссом');
} else if (this.currentMode === 'adventure') {
this.leaderboards.submitScore('adventure', this.currentLevel, `Уровень ${this.currentLevel}`);
} else if (this.currentMode === 'roguelike') {
const flr = this.roguelike ? this.roguelike.currentFloor : 1;
this.leaderboards.submitScore('roguelike', flr, `${flr} Этажей`);
} else {
this.leaderboards.submitScore(this.currentMode, this.score, `${this.score} очков`);
}
}
// Save level progression
let levelReward = 100;
let izombieNextUnlocked = false;
let izombieCurrentStage = 1;
let izombieRewardCoins = 150;
if (this.currentMode === 'izombie' && this.zombieLab) {
izombieCurrentStage = this.zombieLab.selectedStage || 1;
izombieNextUnlocked = this.zombieLab.unlockNextStage(izombieCurrentStage);
const stDef = this.zombieLab.stages.find(s => s.id === izombieCurrentStage);
if (stDef) {
izombieRewardCoins = stDef.reward || 150;
if (stDef.crystals && this.workshop) {
this.workshop.addMaterial('crystals', stDef.crystals);
}
}
if (this.workshop) {
this.workshop.addMaterial('scrapMetal', 6);
this.workshop.addMaterial('bioHumus', 4);
}
this.economy.addCoins(Math.max(0, izombieRewardCoins - 100));
}
if (this.currentMode === 'adventure') {
const campLevel = window.CampaignSystem ? window.CampaignSystem.getLevel(this.currentLevel) : null;
if (campLevel && campLevel.reward) {
levelReward = campLevel.reward;
if (levelReward > 100) {
this.economy.addCoins(levelReward - 100);
}
}
if (this.currentLevel >= this.maxUnlockedLevel && this.currentLevel < 25) {
this.maxUnlockedLevel = this.currentLevel + 1;
}
if (this.currentLevel < 25) {
this.currentLevel++;
}
}
this.saveUserData();
if (window.PvZAuth && window.PvZAuth.flushSave) window.PvZAuth.flushSave();
if (this.currentMode === 'roguelike') {
if (window.PvZAudio) {
window.PvZAudio.stopMusic();
window.PvZAudio.playWin();
}
this.showDraftModal();
return;
}
if (window.PvZAudio) {
window.PvZAudio.stopMusic();
window.PvZAudio.playWin();
}
const statsEl = document.getElementById('victory-stats');
if (statsEl) {
if (this.currentMode === 'izombie') {
statsEl.innerHTML = `Победа! Все цели уничтожены! Мозги захвачены!<br>Этап «Я, Зомби»: <strong>Этап ${izombieCurrentStage} из 5</strong>${izombieNextUnlocked ? ' (🎉 Открыт следующий этап!)' : ''}<br>Награда: <strong>+${izombieRewardCoins} 💰 монет</strong>, <strong>+6 ⚙️ деталей</strong>, <strong>+4 🧪 биогумуса</strong> (Всего: ${this.economy.totalCoins})`;
} else if (this.currentMode === 'bossrush') {
statsEl.innerHTML = `Триумф! Режим Boss Rush успешно завершён!<br>Награда: <strong>+500 💰 монет</strong> &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);
} else if (window.PvZAuth && !window.PvZAuth.isGuest()) {
window.PvZAuth.fetchProfile().then(() => {
this.loadUserData();
this.updateProfileUI();
});
}
});
}
const startHeroProfile = document.getElementById('start-hero-profile');
if (startHeroProfile && btnAuthOpen) {
startHeroProfile.addEventListener('click', () => {
btnAuthOpen.click();
});
}
// Modal Close buttons
if (btnAuthClose && authModal) {
btnAuthClose.addEventListener('click', () => {
authModal.classList.add('hidden');
resetAuthForms();
});
}
if (btnAuthCloseProfile && authModal) {
btnAuthCloseProfile.addEventListener('click', () => {
authModal.classList.add('hidden');
});
}
if (btnAuthCloseReg && authModal) {
btnAuthCloseReg.addEventListener('click', () => {
authModal.classList.add('hidden');
resetAuthForms();
});
}
// Switch between Login and Register views
if (btnAuthToRegister && loginView && registerView) {
btnAuthToRegister.addEventListener('click', () => {
resetAuthForms();
loginView.classList.add('hidden');
registerView.classList.remove('hidden');
if (authRegUser) setTimeout(() => authRegUser.focus(), 50);
});
}
if (btnAuthToLogin && loginView && registerView) {
btnAuthToLogin.addEventListener('click', () => {
resetAuthForms();
registerView.classList.add('hidden');
loginView.classList.remove('hidden');
if (authUser) setTimeout(() => authUser.focus(), 50);
});
}
// Stay as Guest button
if (btnAuthGuest && authModal) {
btnAuthGuest.addEventListener('click', () => {
if (window.PvZAuth) window.PvZAuth.loginAsGuest();
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
authModal.classList.add('hidden');
resetAuthForms();
if (window.PvZAudio) window.PvZAudio.playButtonClick();
});
}
// Login Action (Войти)
if (btnAuthSubmit && authModal) {
btnAuthSubmit.addEventListener('click', async () => {
const u = authUser ? authUser.value.trim() : '';
const p = authPass ? authPass.value : '';
if (!u) {
if (authError) { authError.textContent = 'Введите логин'; authError.classList.remove('hidden'); }
return;
}
if (!p) {
if (authError) { authError.textContent = 'Введите пароль'; authError.classList.remove('hidden'); }
return;
}
if (authError) authError.classList.add('hidden');
if (window.PvZAuth) {
btnAuthSubmit.textContent = '⏳ Вход...';
btnAuthSubmit.disabled = true;
const res = await window.PvZAuth.login(u, p);
btnAuthSubmit.textContent = '🔑 Войти';
btnAuthSubmit.disabled = false;
if (res && res.success) {
authModal.classList.add('hidden');
resetAuthForms();
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
if (window.PvZAudio) window.PvZAudio.playCoin();
} else {
if (authError) {
authError.textContent = (res && res.message) ? res.message : 'Неверный логин или пароль';
authError.classList.remove('hidden');
}
}
}
});
}
// Register Action (Зарегистрироваться)
if (btnAuthDoRegister && authModal) {
btnAuthDoRegister.addEventListener('click', async () => {
const u = authRegUser ? authRegUser.value.trim() : '';
const p = authRegPass ? authRegPass.value : '';
if (!u) {
if (authRegError) { authRegError.textContent = 'Введите логин для регистрации'; authRegError.classList.remove('hidden'); }
return;
}
if (u.length < 3) {
if (authRegError) { authRegError.textContent = 'Логин должен содержать минимум 3 символа'; authRegError.classList.remove('hidden'); }
return;
}
if (!p || p.length < 4) {
if (authRegError) { authRegError.textContent = 'Пароль должен содержать минимум 4 символа'; authRegError.classList.remove('hidden'); }
return;
}
if (authRegError) authRegError.classList.add('hidden');
if (window.PvZAuth) {
btnAuthDoRegister.textContent = '⏳ Регистрация...';
btnAuthDoRegister.disabled = true;
const res = await window.PvZAuth.register(u, p);
btnAuthDoRegister.textContent = '✨ Зарегистрироваться';
btnAuthDoRegister.disabled = false;
if (res && res.success) {
authModal.classList.add('hidden');
resetAuthForms();
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
if (window.PvZAudio) window.PvZAudio.playCoin();
} else {
if (authRegError) {
authRegError.textContent = (res && res.message) ? res.message : 'Ошибка при регистрации';
authRegError.classList.remove('hidden');
}
}
}
});
}
// Export / Import Save File Handlers (Safely bound with optional chaining)
const handleExportSave = () => {
this.saveUserData();
if (window.PvZAuth && window.PvZAuth.exportSaveFile) {
window.PvZAuth.exportSaveFile();
}
};
document.getElementById('btn-export-save')?.addEventListener('click', handleExportSave);
document.getElementById('btn-export-save-profile')?.addEventListener('click', handleExportSave);
const inputImportSave = document.getElementById('input-import-save');
const handleTriggerImport = () => {
if (inputImportSave) {
inputImportSave.value = '';
inputImportSave.click();
}
};
document.getElementById('btn-import-save')?.addEventListener('click', handleTriggerImport);
document.getElementById('btn-import-save-profile')?.addEventListener('click', handleTriggerImport);
inputImportSave?.addEventListener('change', async (e) => {
const file = e.target.files && e.target.files[0];
if (!file) return;
if (window.PvZAuth && window.PvZAuth.importSaveFile) {
const res = await window.PvZAuth.importSaveFile(file);
if (res && res.success) {
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
}
}
if (inputImportSave) inputImportSave.value = '';
});
// Active Sessions event listeners
const btnRefreshSessions = document.getElementById('btn-refresh-sessions');
if (btnRefreshSessions) {
btnRefreshSessions.addEventListener('click', async () => {
btnRefreshSessions.classList.add('rotating');
await this.renderUserSessionsUI();
setTimeout(() => btnRefreshSessions.classList.remove('rotating'), 500);
});
}
const btnTermOthers = document.getElementById('btn-terminate-other-sessions');
if (btnTermOthers) {
btnTermOthers.addEventListener('click', async () => {
if (confirm('Завершить сессии на всех других устройствах?')) {
btnTermOthers.disabled = true;
btnTermOthers.textContent = '⏳ Завершение...';
const res = await window.PvZAuth.terminateOtherSessions();
btnTermOthers.disabled = false;
btnTermOthers.textContent = '🔒 Завершить сессии на всех других устройствах';
if (res && res.success) {
if (window.PvZAuth && window.PvZAuth.showNotification) {
window.PvZAuth.showNotification('Все другие сессии завершены!', 'Осталось только текущее устройство', '🔒');
}
} else {
if (window.PvZAuth && window.PvZAuth.showNotification) {
window.PvZAuth.showNotification('Ошибка', res?.message || 'Не удалось завершить сессии', '❌');
}
}
await this.renderUserSessionsUI();
}
});
}
// Real-time Profile change event subscription
if (window.PvZAuth && typeof window.PvZAuth.onProfileChange === 'function') {
window.PvZAuth.onProfileChange(() => {
this.loadUserData();
this.updateProfileUI();
this.updateRecordDisplay();
this.updateShopUI();
});
}
// Auto-save on page hide / tab switch / page unload
document.addEventListener('visibilitychange', () => {
if (document.visibilityState === 'hidden') {
this.saveUserData();
if (window.PvZAuth && window.PvZAuth.flushSave) {
window.PvZAuth.flushSave();
}
}
});
window.addEventListener('beforeunload', () => {
this.saveUserData();
if (window.PvZAuth && window.PvZAuth.flushSave) {
window.PvZAuth.flushSave(true);
}
});
// Inventory / Deck Builder Buttons
const btnInvStart = document.getElementById('btn-inventory-start');
const btnDrawerInv = document.getElementById('btn-drawer-inventory');
if (btnInvStart) {
btnInvStart.addEventListener('click', () => {
this.openSeedSelectionModal(this.currentLevel || 1, this.currentMode || 'adventure');
});
}
if (btnDrawerInv) {
btnDrawerInv.addEventListener('click', () => {
const drawer = document.getElementById('mobile-drawer');
const backdrop = document.getElementById('drawer-backdrop');
if (drawer) drawer.classList.add('hidden');
if (backdrop) backdrop.classList.add('hidden');
this.openSeedSelectionModal(this.currentLevel || 1, this.currentMode || 'adventure');
});
}
// Shop Button & Tabs
const btnShop = document.getElementById('btn-shop');
const shopModal = document.getElementById('shop-modal');
const btnCloseShop = document.getElementById('btn-close-shop');
const tabShopPlants = document.getElementById('tab-shop-plants');
const tabShopUpgrades = document.getElementById('tab-shop-upgrades');
const panelShopPlants = document.getElementById('shop-plants-panel');
const panelShopUpgrades = document.getElementById('shop-upgrades-panel');
if (btnShop && shopModal) {
btnShop.addEventListener('click', () => {
this.updateShopUI();
shopModal.classList.remove('hidden');
});
}
if (btnCloseShop && shopModal) {
btnCloseShop.addEventListener('click', () => {
shopModal.classList.add('hidden');
this.updateRecordDisplay();
});
}
if (tabShopPlants && tabShopUpgrades && panelShopPlants && panelShopUpgrades) {
tabShopPlants.addEventListener('click', () => {
tabShopPlants.classList.add('active');
tabShopUpgrades.classList.remove('active');
panelShopPlants.classList.remove('hidden');
panelShopUpgrades.classList.add('hidden');
});
tabShopUpgrades.addEventListener('click', () => {
tabShopUpgrades.classList.add('active');
tabShopPlants.classList.remove('active');
panelShopUpgrades.classList.remove('hidden');
panelShopPlants.classList.add('hidden');
});
}
// Pre-Battle Seed Selection & Inventory Modal Handlers
const seedSelectModal = document.getElementById('seed-select-modal');
const btnSeedStart = document.getElementById('btn-seed-start-battle');
const btnSeedAuto = document.getElementById('btn-seed-auto-fill');
const btnSeedShop = document.getElementById('btn-seed-open-shop');
const btnSeedCancel = document.getElementById('btn-seed-cancel');
if (btnSeedStart) {
btnSeedStart.addEventListener('click', () => {
if (!this.economy.selectedDeck || this.economy.selectedDeck.length === 0) {
this.economy.autoFillDeck();
}
if (seedSelectModal) seedSelectModal.classList.add('hidden');
const startModal = document.getElementById('start-modal');
if (startModal) startModal.classList.add('hidden');
if (window.PvZAudio) window.PvZAudio.init();
const lvl = this.pendingLevel || this.currentLevel || 1;
const mode = this.pendingMode || this.currentMode || 'adventure';
this.startLevel(lvl, mode);
if (!this.loopStarted) {
this.loopStarted = true;
requestAnimationFrame(this.loop.bind(this));
}
});
}
if (btnSeedAuto) {
btnSeedAuto.addEventListener('click', () => {
this.economy.autoFillDeck();
this.renderSeedSelectionUI();
if (window.PvZAudio) window.PvZAudio.playButtonClick();
});
}
if (btnSeedShop) {
btnSeedShop.addEventListener('click', () => {
if (seedSelectModal) seedSelectModal.classList.add('hidden');
this.updateShopUI();
if (shopModal) shopModal.classList.remove('hidden');
});
}
if (btnSeedCancel) {
btnSeedCancel.addEventListener('click', () => {
if (seedSelectModal) seedSelectModal.classList.add('hidden');
const startModal = document.getElementById('start-modal');
if (startModal) startModal.classList.remove('hidden');
});
}
// Shop Item Purchases
const btnBuySlot = document.getElementById('btn-buy-slot');
if (btnBuySlot) {
btnBuySlot.addEventListener('click', () => {
if (this.economy.buyUpgrade('extraSlot')) {
if (window.PvZAudio) window.PvZAudio.playWin();
this.saveUserData();
this.updateShopUI();
this.renderSeedSelectionUI();
}
});
}
const btnBuyRake = document.getElementById('btn-buy-rake');
if (btnBuyRake) {
btnBuyRake.addEventListener('click', () => {
if (this.economy.buyUpgrade('rake')) {
if (window.PvZAudio) window.PvZAudio.playWin();
this.saveUserData();
this.updateShopUI();
}
});
}
// Workshop Modal Handlers
const workshopModal = document.getElementById('workshop-modal');
const btnWorkshopNav = document.getElementById('btn-workshop');
const btnWorkshopStart = document.getElementById('btn-workshop-start');
const btnCloseWorkshop = document.getElementById('btn-close-workshop');
const tabPlants = document.getElementById('tab-plants');
const tabPerks = document.getElementById('tab-perks');
const tabZombies = document.getElementById('tab-zombies');
const contentPlants = document.getElementById('ws-content-plants');
const contentPerks = document.getElementById('ws-content-perks');
const contentZombies = document.getElementById('ws-content-zombies');
const openWorkshop = () => {
this.updateWorkshopUI();
if (workshopModal) workshopModal.classList.remove('hidden');
};
if (btnWorkshopNav) btnWorkshopNav.addEventListener('click', openWorkshop);
if (btnWorkshopStart) btnWorkshopStart.addEventListener('click', openWorkshop);
if (btnCloseWorkshop && workshopModal) {
btnCloseWorkshop.addEventListener('click', () => {
workshopModal.classList.add('hidden');
this.updateRecordDisplay();
});
}
const switchWorkshopTab = (tab) => {
if (tabPlants) tabPlants.classList.toggle('active', tab === 'plants');
if (tabPerks) tabPerks.classList.toggle('active', tab === 'perks');
if (tabZombies) tabZombies.classList.toggle('active', tab === 'zombies');
if (contentPlants) contentPlants.classList.toggle('hidden', tab !== 'plants');
if (contentPerks) contentPerks.classList.toggle('hidden', tab !== 'perks');
if (contentZombies) contentZombies.classList.toggle('hidden', tab !== 'zombies');
};
if (tabPlants) tabPlants.addEventListener('click', () => switchWorkshopTab('plants'));
if (tabPerks) tabPerks.addEventListener('click', () => switchWorkshopTab('perks'));
if (tabZombies) tabZombies.addEventListener('click', () => switchWorkshopTab('zombies'));
// Casino Modal Handlers
const casinoModal = document.getElementById('casino-modal');
const btnCasinoStart = document.getElementById('btn-casino-start');
const btnCasinoDrawer = document.getElementById('btn-drawer-casino');
const btnCloseCasino = document.getElementById('btn-close-casino');
const tabCasinoSlots = document.getElementById('tab-casino-slots');
const tabCasinoBank = document.getElementById('tab-casino-bank');
const contentCasinoSlots = document.getElementById('casino-content-slots');
const contentCasinoBank = document.getElementById('casino-content-bank');
const openCasino = () => {
this.updateCasinoUI();
if (casinoModal) casinoModal.classList.remove('hidden');
};
if (btnCasinoStart) btnCasinoStart.addEventListener('click', openCasino);
if (btnCasinoDrawer) btnCasinoDrawer.addEventListener('click', () => {
openCasino();
const drawer = document.getElementById('mobile-drawer');
if (drawer) drawer.classList.add('hidden');
});
if (btnCloseCasino && casinoModal) {
btnCloseCasino.addEventListener('click', () => {
casinoModal.classList.add('hidden');
this.updateRecordDisplay();
});
}
if (tabCasinoSlots && tabCasinoBank) {
tabCasinoSlots.addEventListener('click', () => {
tabCasinoSlots.classList.add('active');
tabCasinoBank.classList.remove('active');
if (contentCasinoSlots) contentCasinoSlots.classList.remove('hidden');
if (contentCasinoBank) contentCasinoBank.classList.add('hidden');
});
tabCasinoBank.addEventListener('click', () => {
tabCasinoBank.classList.add('active');
tabCasinoSlots.classList.remove('active');
if (contentCasinoBank) contentCasinoBank.classList.remove('hidden');
if (contentCasinoSlots) contentCasinoSlots.classList.add('hidden');
this.updateCasinoUI();
});
}
// Casino Bet Buttons
const betBtns = document.querySelectorAll('.btn-bet');
betBtns.forEach(btn => {
btn.addEventListener('click', () => {
betBtns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
const betVal = parseInt(btn.getAttribute('data-bet') || '50', 10);
if (this.casino) this.casino.currentBet = betVal;
});
});
// Casino Spin Button
const btnSpin = document.getElementById('btn-spin-slots');
const reel1 = document.getElementById('reel-1');
const reel2 = document.getElementById('reel-2');
const reel3 = document.getElementById('reel-3');
const resultBanner = document.getElementById('slot-result-banner');
if (btnSpin) {
btnSpin.addEventListener('click', () => {
if (!this.casino) return;
if (this.casino.isSpinning) return;
const bet = this.casino.currentBet || 50;
if (this.economy.totalCoins < bet) {
if (resultBanner) {
resultBanner.textContent = '❌ Недостаточно монет для ставки! Возьмите займ в банке.';
resultBanner.style.color = '#ef5350';
}
return;
}
// Animate spin
this.casino.isSpinning = true;
[reel1, reel2, reel3].forEach(r => { if (r) r.classList.add('spinning'); });
if (btnSpin) btnSpin.disabled = true;
if (resultBanner) {
resultBanner.textContent = '🎰 Барабаны вращаются...';
resultBanner.style.color = '#ffca28';
}
setTimeout(() => {
try {
if (this.casino) {
this.casino.isSpinning = false;
const res = this.casino.spin(bet);
this.casino.isSpinning = false;
if (res && res.icons) {
if (reel1) reel1.innerHTML = `<span class="reel-symbol">${res.icons[0]}</span>`;
if (reel2) reel2.innerHTML = `<span class="reel-symbol">${res.icons[1]}</span>`;
if (reel3) reel3.innerHTML = `<span class="reel-symbol">${res.icons[2]}</span>`;
}
if (resultBanner && res) {
resultBanner.textContent = res.message || '';
if (res.isJackpot) resultBanner.style.color = '#ffd700';
else if (res.payout > 0) resultBanner.style.color = '#69f0ae';
else resultBanner.style.color = '#b0bec5';
}
this.updateCasinoUI();
this.updateRecordDisplay();
}
} catch (e) {
console.error('Casino spin error:', e);
} finally {
if (this.casino) this.casino.isSpinning = false;
[reel1, reel2, reel3].forEach(r => { if (r) r.classList.remove('spinning'); });
if (btnSpin) btnSpin.disabled = false;
}
}, 500);
});
}
// Bank Deposit
const btnDeposit = document.getElementById('btn-bank-deposit');
const inputDeposit = document.getElementById('input-deposit-amount');
if (btnDeposit) {
btnDeposit.addEventListener('click', () => {
if (!this.casino) return;
const amt = parseInt(inputDeposit ? inputDeposit.value : '0', 10);
const res = this.casino.depositCoins(amt);
if (resultBanner) {
resultBanner.textContent = res.message;
resultBanner.style.color = res.success ? '#69f0ae' : '#ef5350';
}
if (inputDeposit && res.success) inputDeposit.value = '';
this.updateCasinoUI();
this.updateRecordDisplay();
});
}
// Bank Withdraw
const btnWithdraw = document.getElementById('btn-bank-withdraw');
if (btnWithdraw) {
btnWithdraw.addEventListener('click', () => {
if (!this.casino) return;
const res = this.casino.withdrawDeposit();
if (resultBanner) {
resultBanner.textContent = res.message;
resultBanner.style.color = res.success ? '#69f0ae' : '#ef5350';
}
this.updateCasinoUI();
this.updateRecordDisplay();
});
}
// Bank Take Loan
const btnTakeLoan = document.getElementById('btn-bank-take-loan');
const inputLoan = document.getElementById('input-loan-amount');
if (btnTakeLoan) {
btnTakeLoan.addEventListener('click', () => {
if (!this.casino) return;
const amt = parseInt(inputLoan ? inputLoan.value : '0', 10);
const res = this.casino.takeLoan(amt);
if (resultBanner) {
resultBanner.textContent = res.message;
resultBanner.style.color = res.success ? '#69f0ae' : '#ef5350';
}
this.updateCasinoUI();
this.updateRecordDisplay();
});
}
// Bank Repay Loan
const btnRepayLoan = document.getElementById('btn-bank-repay-loan');
if (btnRepayLoan) {
btnRepayLoan.addEventListener('click', () => {
if (!this.casino) return;
const amt = parseInt(inputLoan && inputLoan.value ? inputLoan.value : String(this.casino.loan), 10);
const res = this.casino.repayLoan(amt);
if (resultBanner) {
resultBanner.textContent = res.message;
resultBanner.style.color = res.success ? '#69f0ae' : '#ef5350';
}
if (inputLoan && res.success) inputLoan.value = '';
this.updateCasinoUI();
this.updateRecordDisplay();
});
}
// Zen Garden Modal Handlers
const zenModal = document.getElementById('zengarden-modal');
const btnZenNav = document.getElementById('btn-zengarden');
const btnZenStart = document.getElementById('btn-zengarden-start');
const btnCloseZen = document.getElementById('btn-close-zengarden');
const openZenGarden = () => {
if (this.zengarden) {
this.zengarden.calculateOfflineEarnings();
}
this.updateZenGardenUI();
if (zenModal) zenModal.classList.remove('hidden');
};
if (btnZenNav) btnZenNav.addEventListener('click', openZenGarden);
if (btnZenStart) btnZenStart.addEventListener('click', openZenGarden);
if (btnCloseZen && zenModal) {
btnCloseZen.addEventListener('click', () => {
zenModal.classList.add('hidden');
this.updateRecordDisplay();
});
}
// Suburban Almanac Modal Handlers
const almanacModal = document.getElementById('almanac-modal');
const btnAlmanacNav = document.getElementById('btn-almanac');
const btnAlmanacStart = document.getElementById('btn-almanac-start');
const btnCloseAlmanac = document.getElementById('btn-close-almanac');
const openAlmanac = () => {
this.renderAlmanacUI();
if (almanacModal) almanacModal.classList.remove('hidden');
};
if (btnAlmanacNav) btnAlmanacNav.addEventListener('click', openAlmanac);
if (btnAlmanacStart) btnAlmanacStart.addEventListener('click', openAlmanac);
if (btnCloseAlmanac && almanacModal) {
btnCloseAlmanac.addEventListener('click', () => {
almanacModal.classList.add('hidden');
});
}
// Achievements Modal Handlers
const achModal = document.getElementById('achievements-modal');
const btnAchNav = document.getElementById('btn-achievements');
const btnAchStart = document.getElementById('btn-achievements-start');
const btnCloseAch = document.getElementById('btn-close-achievements');
const openAchievements = () => {
this.renderAchievementsUI();
if (achModal) achModal.classList.remove('hidden');
};
if (btnAchNav) btnAchNav.addEventListener('click', openAchievements);
if (btnAchStart) btnAchStart.addEventListener('click', openAchievements);
if (btnCloseAch && achModal) {
btnCloseAch.addEventListener('click', () => {
achModal.classList.add('hidden');
});
}
// Bug Reporting Modal Handlers
const bugModal = document.getElementById('bugreport-modal');
const btnBugNav = document.getElementById('btn-bugreport');
const btnBugStart = document.getElementById('btn-bugreport-start');
const btnCloseBug = document.getElementById('btn-bug-close');
const openBugReport = () => {
if (this.authSystem && this.authSystem.currentUser) {
const nameInput = document.getElementById('bug-input-name');
if (nameInput && !nameInput.value) {
nameInput.value = this.authSystem.currentUser.username;
}
}
if (window.PvZFeedback) {
window.PvZFeedback.fetchReports();
}
if (bugModal) bugModal.classList.remove('hidden');
};
if (btnBugNav) btnBugNav.addEventListener('click', openBugReport);
if (btnBugStart) btnBugStart.addEventListener('click', openBugReport);
if (btnCloseBug && bugModal) {
btnCloseBug.addEventListener('click', () => {
bugModal.classList.add('hidden');
});
}
// Initialize Zen Garden & Almanac & Feedback DOM bindings
this.initZenGarden();
this.initAlmanac();
this.initFeedbackUI();
// Mode Buttons on Start Screen
const btnModeAdv = document.getElementById('btn-mode-adv');
if (btnModeAdv) {
btnModeAdv.addEventListener('click', () => {
this.currentMode = 'adventure';
this.highlightModeBtn('btn-mode-adv');
this.updateRecordDisplay();
});
}
const btnModeSurv = document.getElementById('btn-mode-surv');
if (btnModeSurv) {
btnModeSurv.addEventListener('click', () => {
this.currentMode = 'survival';
this.highlightModeBtn('btn-mode-surv');
this.updateRecordDisplay();
});
}
const btnModeBowl = document.getElementById('btn-mode-bowl');
if (btnModeBowl) {
btnModeBowl.addEventListener('click', () => {
this.currentMode = 'bowling';
this.highlightModeBtn('btn-mode-bowl');
this.updateRecordDisplay();
});
}
const btnModeIzombie = document.getElementById('btn-mode-izombie');
if (btnModeIzombie) {
btnModeIzombie.addEventListener('click', () => {
this.currentMode = 'izombie';
this.highlightModeBtn('btn-mode-izombie');
this.updateRecordDisplay();
});
}
const btnModeRogue = document.getElementById('btn-mode-rogue');
if (btnModeRogue) {
btnModeRogue.addEventListener('click', () => {
this.currentMode = 'roguelike';
if (this.roguelike) this.roguelike.reset();
this.highlightModeBtn('btn-mode-rogue');
this.updateRecordDisplay();
});
}
const btnModeBoss = document.getElementById('btn-mode-boss');
if (btnModeBoss) {
btnModeBoss.addEventListener('click', () => {
this.currentMode = 'boss';
this.highlightModeBtn('btn-mode-boss');
this.updateRecordDisplay();
});
}
const btnModeBossRush = document.getElementById('btn-mode-bossrush');
if (btnModeBossRush) {
btnModeBossRush.addEventListener('click', () => {
this.currentMode = 'bossrush';
this.highlightModeBtn('btn-mode-bossrush');
this.updateRecordDisplay();
});
}
// Theme Switcher Button
const btnTheme = document.getElementById('btn-theme');
if (btnTheme) {
btnTheme.addEventListener('click', () => {
this.cycleTheme();
});
}
// Tree of Wisdom Modal Handlers
const treeModal = document.getElementById('tree-modal');
const btnTreeNav = document.getElementById('btn-tree');
const btnTreeStart = document.getElementById('btn-tree-start');
const btnCloseTree = document.getElementById('btn-tree-close');
const btnTreeFeed = document.getElementById('btn-tree-feed');
const openTree = () => {
this.renderTreeUI();
if (treeModal) treeModal.classList.remove('hidden');
};
if (btnTreeNav) btnTreeNav.addEventListener('click', openTree);
if (btnTreeStart) btnTreeStart.addEventListener('click', openTree);
if (btnCloseTree && treeModal) {
btnCloseTree.addEventListener('click', () => {
treeModal.classList.add('hidden');
});
}
if (btnTreeFeed) {
btnTreeFeed.addEventListener('click', () => {
if (this.tree && this.tree.feedFertilizer()) {
this.renderTreeUI();
this.updateRecordDisplay();
this.updateShopUI();
}
});
}
// Leaderboards Modal Handlers
const lbModal = document.getElementById('leaderboard-modal');
const btnLbNav = document.getElementById('btn-leaderboard');
const btnLbStart = document.getElementById('btn-leaderboard-start');
const btnCloseLb = document.getElementById('btn-leaderboard-close');
const openLeaderboard = (mode = 'survival') => {
this.renderLeaderboardUI(mode);
if (lbModal) lbModal.classList.remove('hidden');
};
if (btnLbNav) btnLbNav.addEventListener('click', () => openLeaderboard('survival'));
if (btnLbStart) btnLbStart.addEventListener('click', () => openLeaderboard('survival'));
if (btnCloseLb && lbModal) {
btnCloseLb.addEventListener('click', () => {
lbModal.classList.add('hidden');
});
}
const lbTabs = [
{ id: 'tab-lb-survival', mode: 'survival' },
{ id: 'tab-lb-rogue', mode: 'roguelike' },
{ id: 'tab-lb-adv', mode: 'adventure' },
{ id: 'tab-lb-izombie', mode: 'izombie' },
{ id: 'tab-lb-bowl', mode: 'bowling' },
{ id: 'tab-lb-boss', mode: 'boss' },
{ id: 'tab-lb-bossrush', mode: 'bossrush' }
];
lbTabs.forEach(t => {
const el = document.getElementById(t.id);
if (el) {
el.addEventListener('click', () => {
lbTabs.forEach(other => {
const oEl = document.getElementById(other.id);
if (oEl) oEl.classList.remove('active');
});
el.classList.add('active');
this.renderLeaderboardUI(t.mode);
});
}
});
// Campaign Map Modal Handlers
const campaignModal = document.getElementById('campaign-modal');
const btnCampaignNav = document.getElementById('btn-campaign-open');
const btnCampaignStart = document.getElementById('btn-campaign-select');
const btnCloseCampaign = document.getElementById('btn-campaign-close');
if (btnCampaignNav) btnCampaignNav.addEventListener('click', () => this.openCampaignModal());
if (btnCampaignStart) btnCampaignStart.addEventListener('click', () => this.openCampaignModal());
if (btnCloseCampaign && campaignModal) {
btnCloseCampaign.addEventListener('click', () => {
campaignModal.classList.add('hidden');
this.updateRecordDisplay();
});
}
for (let w = 1; w <= 5; w++) {
const tab = document.getElementById(`tab-world-${w}`);
if (tab) {
tab.addEventListener('click', () => {
this.renderCampaignMapUI(w);
});
}
}
}
openCampaignModal(worldId = null) {
if (!worldId) {
if (window.CampaignSystem) {
const currentItem = window.CampaignSystem.getLevel(this.currentLevel);
worldId = currentItem ? currentItem.world : 1;
} else {
worldId = 1;
}
}
this.renderCampaignMapUI(worldId);
const modal = document.getElementById('campaign-modal');
if (modal) modal.classList.remove('hidden');
}
renderCampaignMapUI(activeWorldId = 1) {
const gridEl = document.getElementById('campaign-levels-grid');
if (!gridEl || !window.CampaignSystem) return;
// Update active tab styling
for (let w = 1; w <= 5; w++) {
const tab = document.getElementById(`tab-world-${w}`);
if (tab) {
if (w === activeWorldId) {
tab.classList.add('active');
} else {
tab.classList.remove('active');
}
}
}
const levels = window.CampaignSystem.getLevelsByWorld(activeWorldId);
const world = window.CampaignSystem.getWorld(activeWorldId);
let html = '';
levels.forEach(lvl => {
const isPassed = lvl.level < this.currentLevel;
const isCurrent = lvl.level === this.currentLevel;
const isUnlocked = lvl.level <= (this.maxUnlockedLevel || 1);
let badgeClass = 'badge-locked';
let badgeText = '🔒 Заблокировано';
if (isCurrent) {
badgeClass = 'badge-current';
badgeText = '⚔️ Текущий';
} else if (isPassed) {
badgeClass = 'badge-done';
badgeText = '⭐ Пройдено';
} else if (isUnlocked) {
badgeClass = 'badge-unlocked';
badgeText = '🔓 Доступно';
}
const cardClass = `campaign-level-card${isCurrent ? ' current' : ''}${!isUnlocked ? ' locked' : ''}`;
const buttonHtml = isUnlocked
? `<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 price = window.PvZConfig ? window.PvZConfig.getPlantShopPrice(p.id) : (p.price || 0);
const sunCost = window.PvZConfig ? window.PvZConfig.getPlantSunCost(p.id, p.cost) : p.cost;
const canAfford = this.economy.totalCoins >= price;
const card = document.createElement('div');
card.className = 'shop-plant-card';
let actionHtml = '';
if (isBasic) {
actionHtml = '<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'}>Купить (${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">☀️ ${sunCost} солнца</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();
}
});
});
}
// 4. Zombie Lab mutations & campaign stages
this.renderZombieLabUI();
}
renderZombieLabUI() {
const container = document.getElementById('zombies-upgrade-list');
if (!container || !this.zombieLab) return;
container.innerHTML = '';
// Campaign Stage Selector Banner
const stageBanner = document.createElement('div');
stageBanner.className = 'upgrade-card';
stageBanner.style.gridColumn = '1 / -1';
stageBanner.style.background = 'linear-gradient(135deg, rgba(76, 175, 80, 0.15), rgba(38, 50, 56, 0.95))';
stageBanner.style.border = '1px solid rgba(76, 175, 80, 0.5)';
const selStage = this.zombieLab.selectedStage || 1;
const unlStage = this.zombieLab.unlockedStage || 1;
let stageBtnsHtml = '';
this.zombieLab.stages.forEach(st => {
const isUnlocked = st.id <= unlStage;
const isSelected = st.id === selStage;
const activeStyle = isSelected ? 'border: 2px solid #ffeb3b; background: rgba(255, 235, 59, 0.25); color: #fff;' : '';
const lockIcon = isUnlocked ? (st.isBoss ? '👑' : '🧠') : '🔒';
stageBtnsHtml += `
<button class="btn btn-small btn-stage-select ${isSelected ? 'active' : ''}" data-stage-id="${st.id}" ${isUnlocked ? '' : 'disabled'} style="margin: 3px; font-size: 11px; padding: 5px 9px; ${activeStyle}">
${lockIcon} Этап ${st.id} ${st.isBoss ? '(БОСС)' : ''}
</button>
`;
});
const curStageDef = this.zombieLab.stages.find(s => s.id === selStage) || this.zombieLab.stages[0];
stageBanner.innerHTML = `
<div style="display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 8px;">
<div>
<h3 style="margin: 0; color: #a5d6a7; font-size: 15px;">🗺️ Кампания «Я, Зомби» (Этап ${selStage} из 5)</h3>
<p style="margin: 3px 0 0 0; font-size: 12px; color: #cfd8dc;">${curStageDef.title}: ${curStageDef.desc}</p>
</div>
<div style="display: flex; flex-wrap: wrap; gap: 4px;">
${stageBtnsHtml}
</div>
</div>
`;
container.appendChild(stageBanner);
// Bind stage select buttons
stageBanner.querySelectorAll('.btn-stage-select').forEach(btn => {
btn.addEventListener('click', (e) => {
const stId = parseInt(e.currentTarget.getAttribute('data-stage-id'), 10);
this.zombieLab.selectedStage = stId;
this.renderZombieLabUI();
});
});
// Zombie Mutation Cards
for (const [key, zDef] of Object.entries(this.zombieLab.zombies)) {
const curTier = this.zombieLab.getZombieLevel(key);
const isMax = curTier >= 3;
const curData = zDef.tiers[curTier] || zDef.tiers[1];
const nextData = isMax ? null : zDef.tiers[curTier + 1];
const card = document.createElement('div');
card.className = 'upgrade-card zombie-tier-card';
let costHtml = '';
if (nextData && nextData.cost) {
const parts = [];
if (nextData.cost.coins) parts.push(`💰 ${nextData.cost.coins}`);
if (nextData.cost.scrapMetal) parts.push(`⚙️ ${nextData.cost.scrapMetal}`);
if (nextData.cost.bioHumus) parts.push(`🧪 ${nextData.cost.bioHumus}`);
if (nextData.cost.crystals) parts.push(`💎 ${nextData.cost.crystals}`);
costHtml = parts.join(' &bull; ');
}
const stars = '★'.repeat(curTier) + '☆'.repeat(3 - curTier);
const canAfford = this.zombieLab.canUpgrade(key);
card.innerHTML = `
<div class="upgrade-card-header">
<div class="upgrade-plant-icon">${zDef.icon}</div>
<div class="upgrade-title-box">
<h4>${zDef.name}</h4>
<div class="upgrade-stars" style="color: #69f0ae;">${stars} (Ур. ${curTier}/3)</div>
</div>
<div class="zombie-tier-badge">Ранг ${curTier}</div>
</div>
<div class="upgrade-stats-current" style="font-size: 12px; color: #cfd8dc; margin-bottom: 6px;">
<strong>Текущий бонус:</strong> ${curData.title} &mdash; ${curData.desc}
</div>
${!isMax ? `
<div class="upgrade-stats-next" style="font-size: 12px; color: #a5d6a7; margin-bottom: 8px; border-left-color: #69f0ae;">
<strong>Мутация до Ур. ${curTier + 1}:</strong> ${nextData.title} &mdash; ${nextData.desc}
</div>
<button class="btn btn-upgrade" data-upgrade-zombie="${key}" ${canAfford ? '' : 'disabled'} style="width: 100%; font-size: 12px; padding: 7px;">
Мутировать (${costHtml})
</button>
` : `
<div class="upgrade-stats-next" style="border-left-color: #ffd700; color: #ffe082; font-size: 12px;">
🧬 Максимальная мутация достигнута!
</div>
`}
`;
container.appendChild(card);
}
// Bind upgrade buttons for zombies
container.querySelectorAll('[data-upgrade-zombie]').forEach(btn => {
btn.addEventListener('click', (e) => {
const zType = e.currentTarget.getAttribute('data-upgrade-zombie');
const res = this.zombieLab.upgradeZombie(zType);
if (res.success) {
this.saveUserData();
this.updateWorkshopUI();
}
});
});
}
updateCasinoUI() {
if (!this.casino) return;
const coinsEl = document.getElementById('casino-coins');
if (coinsEl) coinsEl.textContent = (this.economy ? this.economy.totalCoins : 0).toLocaleString();
const depStat = document.getElementById('bank-deposit-stat');
if (depStat) depStat.textContent = this.casino.deposit.toLocaleString();
const intStat = document.getElementById('bank-interest-stat');
if (intStat) intStat.textContent = `+${this.casino.getAccruedInterest().toLocaleString()}`;
const debtStat = document.getElementById('bank-debt-stat');
if (debtStat) debtStat.textContent = this.casino.loan.toLocaleString();
const depVal = document.getElementById('bank-deposit-val');
if (depVal) depVal.textContent = this.casino.deposit.toLocaleString();
const intVal = document.getElementById('bank-interest-val');
if (intVal) intVal.textContent = this.casino.getAccruedInterest().toLocaleString();
const debtVal = document.getElementById('bank-debt-val');
if (debtVal) debtVal.textContent = this.casino.loan.toLocaleString();
}
triggerShrekEasterEgg() {
if (window.PvZAudio && window.PvZAudio.playExplosion) {
window.PvZAudio.playExplosion();
}
const canvas = document.getElementById('game-canvas');
if (canvas) {
canvas.classList.add('shake');
setTimeout(() => canvas.classList.remove('shake'), 1200);
}
let affected = 0;
if (this.zombies && this.zombies.length > 0) {
for (const z of this.zombies) {
if (z && z.health > 0) {
z.takeDamage(2000);
affected++;
if (this.combat && this.combat.createSplash) {
this.combat.createSplash(z.x, z.y, '#7cb342', 12);
}
}
}
}
let toast = document.getElementById('achievement-toast');
if (!toast) {
toast = document.createElement('div');
toast.id = 'achievement-toast';
toast.className = 'achievement-toast';
document.body.appendChild(toast);
}
toast.innerHTML = `
<div class="ach-toast-icon">🧅</div>
<div class="ach-toast-text">
<div class="ach-toast-subtitle">«THIS IS MY SWAMP!»</div>
<div class="ach-toast-title">ШРЕК СОТРЯС ЗЕМЛЮ! 2000 урона всем зомби! (${affected} поражено)</div>
</div>
`;
toast.classList.add('visible');
setTimeout(() => toast.classList.remove('visible'), 4000);
}
initZenGarden() {
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;
}
function escapeHtml(str) {
if (!str) return '';
return String(str)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
let rowsHtml = '';
entries.forEach((item, index) => {
let medal = `#${index + 1}`;
if (index === 0) medal = '🥇 1';
else if (index === 1) medal = '🥈 2';
else if (index === 2) medal = '🥉 3';
const safeName = escapeHtml(item.name || 'Игрок');
const safeDetail = escapeHtml(item.detail || item.score || '');
const safeDate = escapeHtml(item.date || '');
rowsHtml += `
<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;">${safeName}</td>
<td style="color: #ffd54f; font-weight: bold;">${safeDetail}</td>
<td style="color: #b0bec5; font-size: 11px; text-align: right;">${safeDate}</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 targets = this.plants.filter(p => p.type === 'sunflower' || p.type === 'mega_sunflower');
if (targets.length === 0) {
this.triggerVictory();
} else {
const minZombieCost = 50;
const hasAliveZombies = this.zombies.some(z => z.health > 0 && !z.toRemove);
if (!hasAliveZombies && this.economy.sun < minZombieCost) {
this.triggerGameOver('Закончились солнце и зомби! Растения уцелели.');
}
}
}
}
render() {
this.ctx.clearRect(0, 0, this.logicalWidth, this.logicalHeight);
// 1. Grid & Biome
this.grid.draw(this.ctx, this.hoverCell, this.economy.selectedCard);
// 2. Garden Rake
if (this.hasRakeArmed) {
const rakeBounds = this.grid.getCellBounds(this.rakeCol, this.rakeRow);
this.drawRake(this.ctx, rakeBounds.centerX, rakeBounds.centerY);
}
// 3. Plants
for (const plant of this.plants) {
plant.draw(this.ctx);
}
// 4. Zombies (depth sort)
const sortedZombies = [...this.zombies].sort((a, b) => a.y - b.y);
for (const zombie of sortedZombies) {
zombie.draw(this.ctx, this);
}
// 5. Projectiles
for (const proj of this.projectiles) {
proj.draw(this.ctx);
}
// 5.5 Workshop Floating Loot
if (this.workshop) {
this.workshop.draw(this.ctx);
}
// 6. Combat Particles
this.combat.draw(this.ctx);
// 7. Top UI & Suns
this.economy.draw(this.ctx);
// 8. Wave status & Banners
this.wave.draw(this.ctx);
// 8.5 Roguelike Active Relics HUD
if (this.roguelike && this.currentMode === 'roguelike') {
this.roguelike.draw(this.ctx);
}
// 8.6 Dr. Zomboss Robot & Hazards
if (this.boss && (this.boss.isActive || this.currentMode === 'boss' || this.currentMode === 'bossrush')) {
this.boss.draw(this.ctx);
}
// 9. Ghost Preview
if (this.hoverCell && this.economy.selectedCard && this.economy.selectedCard !== 'shovel') {
const bounds = this.grid.getCellBounds(this.hoverCell.col, this.hoverCell.row);
this.ctx.save();
this.ctx.globalAlpha = 0.55;
this.economy.drawCardThumbnail(this.ctx, this.economy.selectedCard, bounds.centerX, bounds.centerY);
this.ctx.restore();
}
// 10. Level & Score indicator in top right
this.ctx.save();
this.ctx.fillStyle = '#ffeb3b';
this.ctx.font = 'bold 12px sans-serif';
this.ctx.textAlign = 'right';
const modeLabel = this.currentMode === 'survival'
? 'Выживание'
: (this.currentMode === 'bowling'
? 'Боулинг'
: (this.currentMode === 'boss'
? 'Босс: Д-р Зомбосс'
: (this.currentMode === 'roguelike' ? `Рогалик: Этаж ${this.roguelike ? this.roguelike.currentFloor : 1}` : `Уровень ${this.currentLevel}`)));
this.ctx.fillText(`${modeLabel} | Счёт: ${this.score}`, 880, 78);
this.ctx.restore();
// 11. Pause Banner Overlay
if (this.isPaused) {
this.ctx.save();
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.65)';
this.ctx.fillRect(0, 0, this.logicalWidth, this.logicalHeight);
// Center Modal Card
this.ctx.fillStyle = '#2a160c';
this.ctx.strokeStyle = '#e09f3e';
this.ctx.lineWidth = 4;
this.ctx.beginPath();
this.ctx.roundRect(260, 210, 380, 180, 16);
this.ctx.fill();
this.ctx.stroke();
this.ctx.fillStyle = '#ffeb3b';
this.ctx.font = 'bold 28px sans-serif';
this.ctx.textAlign = 'center';
this.ctx.fillText('⏸️ ИГРА НА ПАУЗЕ', this.logicalWidth / 2, 270);
this.ctx.fillStyle = '#e0e0e0';
this.ctx.font = '14px sans-serif';
this.ctx.fillText('Все растения и зомби остановлены.', this.logicalWidth / 2, 308);
this.ctx.fillStyle = '#81c784';
this.ctx.font = 'bold 15px sans-serif';
this.ctx.fillText('Нажмите Пробел или кнопку «▶️ Играть»', this.logicalWidth / 2, 345);
this.ctx.restore();
}
}
drawRake(ctx, x, y) {
ctx.save();
ctx.translate(x, y);
ctx.strokeStyle = '#8d6e63';
ctx.lineWidth = 3;
ctx.beginPath();
ctx.moveTo(-10, 18);
ctx.lineTo(15, -12);
ctx.stroke();
ctx.strokeStyle = '#757575';
ctx.lineWidth = 2.5;
ctx.beginPath();
ctx.moveTo(8, -16);
ctx.lineTo(20, -6);
for (let i = 0; i < 4; i++) {
ctx.moveTo(8 + i * 3, -16 + i * 2.5);
ctx.lineTo(5 + i * 3, -22 + i * 2.5);
}
ctx.stroke();
ctx.restore();
}
};