3148 lines
117 KiB
JavaScript
3148 lines
117 KiB
JavaScript
// Main Game Engine Controller (Sprint v1.2 - v1.5 Complete PvZ Platform)
|
||
window.GameEngine = class GameEngine {
|
||
constructor(canvasId) {
|
||
this.canvas = document.getElementById(canvasId);
|
||
this.ctx = this.canvas.getContext('2d');
|
||
|
||
// Logical dimensions
|
||
this.logicalWidth = 900;
|
||
this.logicalHeight = 600;
|
||
this.width = this.logicalWidth;
|
||
this.height = this.logicalHeight;
|
||
|
||
// HiDPI / Retina setup
|
||
this.setupHiDPI();
|
||
window.addEventListener('resize', () => this.setupHiDPI());
|
||
|
||
// Systems
|
||
this.grid = new window.GridSystem();
|
||
this.economy = new window.EconomySystem();
|
||
this.economy.engine = this;
|
||
if (window.WorkshopSystem) {
|
||
this.workshop = new window.WorkshopSystem(this.economy);
|
||
this.economy.workshop = this.workshop;
|
||
}
|
||
if (window.ZenGardenSystem) {
|
||
this.zengarden = new window.ZenGardenSystem(this.economy);
|
||
}
|
||
if (window.AlmanacSystem) {
|
||
this.almanac = new window.AlmanacSystem();
|
||
}
|
||
if (window.AchievementSystem) {
|
||
this.achievements = new window.AchievementSystem(this);
|
||
}
|
||
if (window.RoguelikeSystem) {
|
||
this.roguelike = new window.RoguelikeSystem(this);
|
||
}
|
||
if (window.TreeSystem) {
|
||
this.tree = new window.TreeSystem(this.economy);
|
||
}
|
||
if (window.BossSystem) {
|
||
this.boss = new window.BossSystem(this);
|
||
}
|
||
if (window.LeaderboardSystem) {
|
||
this.leaderboards = new window.LeaderboardSystem();
|
||
}
|
||
if (window.PvZFeedback) {
|
||
this.feedback = window.PvZFeedback;
|
||
this.feedback.init();
|
||
}
|
||
this.wave = new window.WaveSystem();
|
||
this.combat = new window.CombatSystem();
|
||
if (window.ProjectilePool) {
|
||
this.projectilePool = new window.ProjectilePool(60);
|
||
}
|
||
|
||
// Entities
|
||
this.plants = [];
|
||
this.zombies = [];
|
||
this.projectiles = [];
|
||
|
||
// State
|
||
this.currentMode = 'adventure'; // 'adventure', 'survival', 'bowling', 'izombie', 'roguelike'
|
||
this.currentLevel = 1;
|
||
this.maxUnlockedLevel = 1;
|
||
this.isRunning = false;
|
||
this.isGameOver = false;
|
||
this.isVictory = false;
|
||
this.gameSpeed = 1.0;
|
||
this.isPaused = false;
|
||
this.lastTime = 0;
|
||
this.hoverCell = null;
|
||
this.loopStarted = false;
|
||
this.izombieSetupDone = false;
|
||
|
||
// Rake state
|
||
this.hasRakeArmed = false;
|
||
this.rakeCol = 7;
|
||
this.rakeRow = 2;
|
||
|
||
// Score & Persistence
|
||
this.score = 0;
|
||
this.zombiesKilled = 0;
|
||
this.loadUserData();
|
||
this.updateRecordDisplay();
|
||
this.updateProfileUI();
|
||
|
||
// Register with Test API
|
||
if (window.PvZTestAPI) {
|
||
window.PvZTestAPI.setEngine(this);
|
||
}
|
||
window._pvzEngine = this; // allow auth.js to trigger profile reload
|
||
|
||
// Bind events
|
||
this.bindEvents();
|
||
this.bindUI();
|
||
}
|
||
|
||
setupHiDPI() {
|
||
this.dpr = Math.min(window.devicePixelRatio || 1, 2.5);
|
||
this.canvas.width = this.logicalWidth * this.dpr;
|
||
this.canvas.height = this.logicalHeight * this.dpr;
|
||
this.ctx.resetTransform();
|
||
this.ctx.scale(this.dpr, this.dpr);
|
||
}
|
||
|
||
loadUserData() {
|
||
if (window.PvZAuth) {
|
||
const uData = window.PvZAuth.getUserData();
|
||
this.currentLevel = uData.level || 1;
|
||
this.maxUnlockedLevel = uData.maxUnlockedLevel || Math.max(this.currentLevel, 1);
|
||
this.highScore = uData.highScore || 0;
|
||
this.currentTheme = uData.theme || 'classic';
|
||
this.applyTheme(this.currentTheme);
|
||
if (this.economy) this.economy.reloadProfileData();
|
||
if (this.workshop) this.workshop.loadFromAuth();
|
||
if (this.zengarden) this.zengarden.loadFromAuth();
|
||
if (this.achievements) this.achievements.loadFromAuth();
|
||
if (this.tree) this.tree.loadFromAuth();
|
||
if (this.leaderboards) this.leaderboards.loadFromAuth();
|
||
} else {
|
||
this.currentLevel = parseInt(localStorage.getItem('pvz_level') || '1', 10);
|
||
this.maxUnlockedLevel = parseInt(localStorage.getItem('pvz_max_level') || this.currentLevel.toString(), 10);
|
||
this.highScore = parseInt(localStorage.getItem('pvz_highscore') || '0', 10);
|
||
this.currentTheme = localStorage.getItem('pvz_theme') || 'classic';
|
||
this.applyTheme(this.currentTheme);
|
||
}
|
||
}
|
||
|
||
saveUserData() {
|
||
if (window.PvZAuth) {
|
||
if (this.achievements) this.achievements.saveToAuth();
|
||
if (this.tree) this.tree.saveToAuth();
|
||
if (this.leaderboards) this.leaderboards.saveToAuth();
|
||
window.PvZAuth.saveUserData({
|
||
coins: this.economy ? this.economy.totalCoins : undefined,
|
||
level: this.currentLevel,
|
||
maxUnlockedLevel: this.maxUnlockedLevel,
|
||
highScore: this.highScore,
|
||
theme: this.currentTheme,
|
||
upgrades: this.economy ? this.economy.upgrades : undefined,
|
||
materials: this.workshop ? this.workshop.materials : undefined,
|
||
plantLevels: this.workshop ? this.workshop.plantLevels : undefined,
|
||
perks: this.workshop ? this.workshop.perks : undefined,
|
||
zenGarden: this.zengarden ? {
|
||
pots: this.zengarden.pots,
|
||
fertilizerCount: this.zengarden.fertilizerCount,
|
||
chocolateCount: this.zengarden.chocolateCount,
|
||
snail: this.zengarden.snail,
|
||
lastVisit: Date.now()
|
||
} : undefined
|
||
});
|
||
}
|
||
localStorage.setItem('pvz_level', this.currentLevel.toString());
|
||
localStorage.setItem('pvz_max_level', this.maxUnlockedLevel.toString());
|
||
localStorage.setItem('pvz_highscore', this.highScore.toString());
|
||
localStorage.setItem('pvz_theme', this.currentTheme);
|
||
if (this.economy) {
|
||
localStorage.setItem('pvz_coins', this.economy.totalCoins.toString());
|
||
}
|
||
}
|
||
|
||
applyTheme(theme) {
|
||
this.currentTheme = theme || 'classic';
|
||
const body = document.body;
|
||
body.classList.remove('theme-cyberpunk', 'theme-retro8bit', 'theme-winter');
|
||
if (this.currentTheme !== 'classic') {
|
||
body.classList.add(`theme-${this.currentTheme}`);
|
||
}
|
||
|
||
const btnTheme = document.getElementById('btn-theme');
|
||
if (btnTheme) {
|
||
const labels = {
|
||
classic: '🎨 Классика',
|
||
cyberpunk: '🎨 Киберпанк',
|
||
retro8bit: '🎨 Ретро 8-бит',
|
||
winter: '🎨 Зима'
|
||
};
|
||
btnTheme.textContent = labels[this.currentTheme] || '🎨 Тема';
|
||
}
|
||
|
||
if (this.grid) {
|
||
this.grid.theme = this.currentTheme;
|
||
}
|
||
}
|
||
|
||
cycleTheme() {
|
||
const themes = ['classic', 'cyberpunk', 'retro8bit', 'winter'];
|
||
const nextIdx = (themes.indexOf(this.currentTheme) + 1) % themes.length;
|
||
this.applyTheme(themes[nextIdx]);
|
||
this.saveUserData();
|
||
if (window.PvZAudio) window.PvZAudio.playCoin();
|
||
}
|
||
|
||
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} шт.`;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
openMainMenu() {
|
||
this.isRunning = false;
|
||
if (window.PvZAudio) window.PvZAudio.stopMusic();
|
||
const startModal = document.getElementById('start-modal');
|
||
if (startModal) startModal.classList.remove('hidden');
|
||
|
||
// Close any game-phase modals that might have been left open
|
||
['game-over-modal', 'victory-modal', 'draft-modal', 'zengarden-modal',
|
||
'workshop-modal', 'leaderboard-modal', 'achievement-modal', 'almanac-modal',
|
||
'bugreport-modal', 'campaign-modal', 'tree-modal'].forEach(id => {
|
||
const el = document.getElementById(id);
|
||
if (el) el.classList.add('hidden');
|
||
});
|
||
|
||
// Highlight the mode button that matches currentMode
|
||
const modeMap = {
|
||
adventure: 'btn-mode-adv',
|
||
survival: 'btn-mode-surv',
|
||
bowling: 'btn-mode-bowl',
|
||
izombie: 'btn-mode-izombie',
|
||
roguelike: 'btn-mode-rogue',
|
||
boss: 'btn-mode-boss',
|
||
bossrush: 'btn-mode-bossrush'
|
||
};
|
||
const activeBtnId = modeMap[this.currentMode] || 'btn-mode-adv';
|
||
this.highlightModeBtn(activeBtnId);
|
||
|
||
this.updateRecordDisplay();
|
||
this.updateProfileUI();
|
||
}
|
||
|
||
updateRecordDisplay() {
|
||
const elRecord = document.getElementById('start-record');
|
||
if (elRecord) {
|
||
const lvlLabel = (window.CampaignSystem && this.currentMode === 'adventure')
|
||
? window.CampaignSystem.formatLevel(this.currentLevel).fullLabel
|
||
: this.currentLevel;
|
||
elRecord.innerHTML = `🏆 Рекорд: <strong>${this.highScore}</strong> очков | 💰 Монеты: <strong>${this.economy.totalCoins}</strong> | Уровень: <strong>${lvlLabel}</strong>`;
|
||
}
|
||
}
|
||
|
||
addScore(pts) {
|
||
this.score += pts;
|
||
if (this.score > this.highScore) {
|
||
this.highScore = this.score;
|
||
this.saveUserData();
|
||
}
|
||
}
|
||
|
||
startLevel(level = 1, mode = 'adventure') {
|
||
this.currentLevel = level;
|
||
this.currentMode = mode;
|
||
|
||
// Pre-set economy mode flags BEFORE reset() so economy.reset() computes
|
||
// the correct baseSun (avoids stale isConveyor/isIZombie from previous game)
|
||
this.economy.isConveyor = (mode === 'bowling' || mode === 'boss' || mode === 'bossrush');
|
||
this.economy.isIZombie = (mode === 'izombie');
|
||
this.economy.isNight = (mode === 'izombie');
|
||
|
||
this.reset();
|
||
this.wave.setLevel(level, mode);
|
||
this.grid.initBiome(this.wave.biome);
|
||
|
||
if (mode === 'bowling') {
|
||
this.economy.setConveyorDeck();
|
||
this.hasRakeArmed = false;
|
||
} else if (mode === 'izombie') {
|
||
this.economy.setIZombieDeck();
|
||
this.grid.lawnmowers = [];
|
||
this.hasRakeArmed = false;
|
||
this.setupIZombieLawn(); // place lawn plants for I, Zombie mode
|
||
} else if (mode === 'boss' || mode === 'bossrush') {
|
||
this.economy.setBossDeck();
|
||
if (this.boss) this.boss.reset();
|
||
this.hasRakeArmed = false;
|
||
} else if (mode === 'roguelike') {
|
||
this.economy.isConveyor = false;
|
||
this.economy.isIZombie = false;
|
||
if (!this.economy.cards || this.economy.cards.length === 0 || (this.roguelike && this.roguelike.currentFloor === 1) || this.economy.cards.some(c => c.id.startsWith('z_') || c.id.startsWith('bowling_'))) {
|
||
this.economy.cards = this.economy.allCards
|
||
.filter(c => ['sunflower', 'peashooter'].includes(c.id))
|
||
.map(c => ({ ...c, timer: 0 }));
|
||
}
|
||
this.hasRakeArmed = this.economy.upgrades.hasRake;
|
||
this.rakeRow = Math.floor(Math.random() * this.grid.rows);
|
||
this.rakeCol = 7;
|
||
} else {
|
||
this.economy.setDeckForLevel(level, this.wave.biome);
|
||
this.hasRakeArmed = this.economy.upgrades.hasRake;
|
||
this.rakeRow = Math.floor(Math.random() * this.grid.rows);
|
||
this.rakeCol = 7;
|
||
}
|
||
|
||
this.isRunning = true;
|
||
this.lastTime = performance.now();
|
||
const musicTheme = (mode === 'boss' || mode === 'bossrush') ? 'boss' : this.wave.biome;
|
||
if (window.PvZAudio) window.PvZAudio.startMusic(musicTheme);
|
||
}
|
||
|
||
setupIZombieLawn() {
|
||
this.plants = [];
|
||
this.izombieSetupDone = true;
|
||
|
||
// Col 0: Sunflowers in rows 0..4 (5 trophy targets)
|
||
for (let r = 0; r < 5; r++) {
|
||
const b0 = this.grid.getCellBounds(0, r);
|
||
const sf = new window.Plant('sunflower', 0, r, b0.x, b0.y, b0.w, b0.h);
|
||
this.grid.setPlant(0, r, sf);
|
||
this.plants.push(sf);
|
||
}
|
||
|
||
// Row 0 defense: Peashooter at col 2
|
||
const b02 = this.grid.getCellBounds(2, 0);
|
||
const p02 = new window.Plant('peashooter', 2, 0, b02.x, b02.y, b02.w, b02.h);
|
||
this.grid.setPlant(2, 0, p02);
|
||
this.plants.push(p02);
|
||
|
||
// Row 1 defense: Repeater at col 2, Wallnut at col 4
|
||
const b12 = this.grid.getCellBounds(2, 1);
|
||
const p12 = new window.Plant('repeater', 2, 1, b12.x, b12.y, b12.w, b12.h);
|
||
this.grid.setPlant(2, 1, p12);
|
||
this.plants.push(p12);
|
||
|
||
const b14 = this.grid.getCellBounds(4, 1);
|
||
const p14 = new window.Plant('wallnut', 4, 1, b14.x, b14.y, b14.w, b14.h);
|
||
this.grid.setPlant(4, 1, p14);
|
||
this.plants.push(p14);
|
||
|
||
// Row 2 defense: Snowpea at col 1, Peashooter at col 3
|
||
const b21 = this.grid.getCellBounds(1, 2);
|
||
const p21 = new window.Plant('snowpea', 1, 2, b21.x, b21.y, b21.w, b21.h);
|
||
this.grid.setPlant(1, 2, p21);
|
||
this.plants.push(p21);
|
||
|
||
const b23 = this.grid.getCellBounds(3, 2);
|
||
const p23 = new window.Plant('peashooter', 3, 2, b23.x, b23.y, b23.w, b23.h);
|
||
this.grid.setPlant(3, 2, p23);
|
||
this.plants.push(p23);
|
||
|
||
// Row 3 defense: Peashooter at col 2, Wallnut at col 4
|
||
const b32 = this.grid.getCellBounds(2, 3);
|
||
const p32 = new window.Plant('peashooter', 2, 3, b32.x, b32.y, b32.w, b32.h);
|
||
this.grid.setPlant(2, 3, p32);
|
||
this.plants.push(p32);
|
||
|
||
const b34 = this.grid.getCellBounds(4, 3);
|
||
const p34 = new window.Plant('wallnut', 4, 3, b34.x, b34.y, b34.w, b34.h);
|
||
this.grid.setPlant(4, 3, p34);
|
||
this.plants.push(p34);
|
||
|
||
// Row 4 defense: Repeater at col 1, Potato Mine at col 3 (armed)
|
||
const b41 = this.grid.getCellBounds(1, 4);
|
||
const p41 = new window.Plant('repeater', 1, 4, b41.x, b41.y, b41.w, b41.h);
|
||
this.grid.setPlant(1, 4, p41);
|
||
this.plants.push(p41);
|
||
|
||
const b43 = this.grid.getCellBounds(3, 4);
|
||
const p43 = new window.Plant('potatomine', 3, 4, b43.x, b43.y, b43.w, b43.h);
|
||
p43.isArmed = true;
|
||
p43.armTimer = 15;
|
||
this.grid.setPlant(3, 4, p43);
|
||
this.plants.push(p43);
|
||
}
|
||
|
||
init() {
|
||
let startLvl = 1;
|
||
if (this.currentMode === 'adventure') {
|
||
startLvl = this.currentLevel || 1;
|
||
} else if (this.currentMode === 'roguelike') {
|
||
startLvl = this.roguelike ? this.roguelike.currentFloor : 1;
|
||
}
|
||
|
||
// 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;
|
||
}
|
||
this.economy.spawnSun(x, y, finalValue, isFalling, targetY);
|
||
}
|
||
|
||
spawnCoin(x, y, type = 'silver') {
|
||
this.economy.spawnCoin(x, y, type);
|
||
}
|
||
|
||
spawnZombie(row, type = 'normal') {
|
||
const zombie = new window.Zombie(row, type);
|
||
const bounds = this.grid.getCellBounds(0, row);
|
||
zombie.y = bounds.centerY - 5;
|
||
this.zombies.push(zombie);
|
||
}
|
||
|
||
explodeArea(centerCol, centerRow, radiusTiles = 3) {
|
||
const half = Math.floor(radiusTiles / 2);
|
||
const bounds = this.grid.getCellBounds(centerCol, centerRow);
|
||
this.combat.createExplosionParticles(bounds.centerX, bounds.centerY);
|
||
|
||
for (const z of this.zombies) {
|
||
if (z.toRemove || z.health <= 0) continue;
|
||
const rowDiff = Math.abs(z.row - centerRow);
|
||
const colEstimated = (z.x - this.grid.startX) / this.grid.cellW;
|
||
const colDiff = Math.abs(colEstimated - centerCol);
|
||
|
||
if (rowDiff <= half && colDiff <= half + 0.5) {
|
||
z.takeDamage(1800);
|
||
if (z.health <= 0) {
|
||
this.addScore(250);
|
||
this.zombiesKilled++;
|
||
if (Math.random() < 0.4) this.spawnCoin(z.x, z.y, 'gold');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
triggerGameOver(reason = 'Зомби прорвали оборону газона. Попробуйте снова!') {
|
||
if (this.isGameOver) return;
|
||
this.isGameOver = true;
|
||
this.isRunning = false;
|
||
if (window.PvZAudio) {
|
||
window.PvZAudio.stopMusic();
|
||
window.PvZAudio.playLose();
|
||
}
|
||
|
||
if (this.leaderboards) {
|
||
if (this.currentMode === 'survival') {
|
||
this.leaderboards.submitScore('survival', this.wave.survivalFlags || 0, `${this.wave.survivalFlags || 0} Флагов`);
|
||
} else if (this.currentMode === 'roguelike') {
|
||
const flr = this.roguelike ? this.roguelike.currentFloor : 1;
|
||
this.leaderboards.submitScore('roguelike', flr, `${flr} Этаж`);
|
||
} else {
|
||
this.leaderboards.submitScore(this.currentMode, this.score, `${this.score} очков`);
|
||
}
|
||
}
|
||
|
||
this.saveUserData();
|
||
if (window.PvZAuth && window.PvZAuth.flushSave) window.PvZAuth.flushSave();
|
||
|
||
const descEl = document.getElementById('game-over-desc');
|
||
if (descEl) descEl.textContent = reason;
|
||
|
||
const statsEl = document.getElementById('game-over-stats');
|
||
if (statsEl) {
|
||
statsEl.innerHTML = `Счёт: <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.addScore(1000);
|
||
this.economy.addCoins(100); // 100 coin bonus for level completion!
|
||
|
||
// Check achievement: clean_lawn (all lawnmowers intact)
|
||
if (this.grid.lawnmowers && this.grid.lawnmowers.length > 0 && this.grid.lawnmowers.every(lm => lm && lm.active)) {
|
||
if (this.achievements) this.achievements.trigger('clean_lawn');
|
||
}
|
||
|
||
if (this.currentMode === 'izombie' && this.achievements) {
|
||
this.achievements.trigger('izombie_win');
|
||
}
|
||
|
||
if (this.leaderboards) {
|
||
if (this.currentMode === 'bossrush') {
|
||
this.leaderboards.submitScore('bossrush', 3, 'Boss Rush пройден! (3 волны)');
|
||
this.economy.addCoins(400); // 100 + 400 = 500 bonus
|
||
if (this.workshop) {
|
||
this.workshop.materials.crystals += 2;
|
||
}
|
||
} else if (this.currentMode === 'boss') {
|
||
this.leaderboards.submitScore('boss', 1, 'Победа над Зомбоссом');
|
||
} else if (this.currentMode === 'adventure') {
|
||
this.leaderboards.submitScore('adventure', this.currentLevel, `Уровень ${this.currentLevel}`);
|
||
} else if (this.currentMode === 'roguelike') {
|
||
const flr = this.roguelike ? this.roguelike.currentFloor : 1;
|
||
this.leaderboards.submitScore('roguelike', flr, `${flr} Этажей`);
|
||
} else {
|
||
this.leaderboards.submitScore(this.currentMode, this.score, `${this.score} очков`);
|
||
}
|
||
}
|
||
|
||
// Save level progression
|
||
let levelReward = 100;
|
||
if (this.currentMode === 'adventure') {
|
||
const campLevel = window.CampaignSystem ? window.CampaignSystem.getLevel(this.currentLevel) : null;
|
||
if (campLevel && campLevel.reward) {
|
||
levelReward = campLevel.reward;
|
||
if (levelReward > 100) {
|
||
this.economy.addCoins(levelReward - 100);
|
||
}
|
||
}
|
||
if (this.currentLevel >= this.maxUnlockedLevel && this.currentLevel < 25) {
|
||
this.maxUnlockedLevel = this.currentLevel + 1;
|
||
}
|
||
if (this.currentLevel < 25) {
|
||
this.currentLevel++;
|
||
}
|
||
}
|
||
|
||
this.saveUserData();
|
||
if (window.PvZAuth && window.PvZAuth.flushSave) window.PvZAuth.flushSave();
|
||
|
||
if (this.currentMode === 'roguelike') {
|
||
if (window.PvZAudio) {
|
||
window.PvZAudio.stopMusic();
|
||
window.PvZAudio.playWin();
|
||
}
|
||
this.showDraftModal();
|
||
return;
|
||
}
|
||
|
||
if (window.PvZAudio) {
|
||
window.PvZAudio.stopMusic();
|
||
window.PvZAudio.playWin();
|
||
}
|
||
|
||
const statsEl = document.getElementById('victory-stats');
|
||
if (statsEl) {
|
||
if (this.currentMode === 'izombie') {
|
||
statsEl.innerHTML = `Победа! Все подсолнухи съедены! Мозги захвачены!<br>Награда: <strong>+100 💰 монет</strong> (Всего: ${this.economy.totalCoins})`;
|
||
} else if (this.currentMode === 'bossrush') {
|
||
statsEl.innerHTML = `Триумф! Режим Boss Rush успешно завершён!<br>Награда: <strong>+500 💰 монет</strong> • <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.isRunning) {
|
||
this.checkTouchCollect(pos);
|
||
}
|
||
});
|
||
|
||
this.canvas.addEventListener('mouseleave', () => {
|
||
this.hoverCell = null;
|
||
});
|
||
|
||
this.canvas.addEventListener('click', (e) => {
|
||
if (!this.isRunning) return;
|
||
const pos = this.getEventPos(e);
|
||
|
||
const econResult = this.economy.handleClick(pos.x, pos.y);
|
||
if (econResult.handled) {
|
||
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;
|
||
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', () => {
|
||
this.startLevel(this.currentLevel, this.currentMode);
|
||
});
|
||
}
|
||
|
||
// Auth & Profile Modal Handlers
|
||
const authModal = document.getElementById('auth-modal');
|
||
const btnAuthOpen = document.getElementById('btn-auth-open');
|
||
const btnAuthClose = document.getElementById('btn-auth-close');
|
||
const btnAuthCloseProfile = document.getElementById('btn-auth-close-profile');
|
||
const btnAuthCloseReg = document.getElementById('btn-auth-close-reg');
|
||
|
||
const loginView = document.getElementById('auth-login-view') || document.getElementById('auth-guest-view');
|
||
const registerView = document.getElementById('auth-register-view');
|
||
|
||
const btnAuthToRegister = document.getElementById('btn-auth-to-register');
|
||
const btnAuthToLogin = document.getElementById('btn-auth-to-login');
|
||
|
||
const btnAuthSubmit = document.getElementById('btn-auth-submit'); // Войти
|
||
const btnAuthDoRegister = document.getElementById('btn-auth-do-register'); // Зарегистрироваться
|
||
const btnAuthGuest = document.getElementById('btn-auth-guest'); // Остаться гостем
|
||
const btnAuthLogout = document.getElementById('btn-auth-logout'); // Выйти
|
||
const btnAuthLogoutNav = document.getElementById('btn-auth-logout-nav');
|
||
|
||
const authUser = document.getElementById('auth-username');
|
||
const authPass = document.getElementById('auth-password');
|
||
const authError = document.getElementById('auth-error');
|
||
|
||
const authRegUser = document.getElementById('auth-reg-username');
|
||
const authRegPass = document.getElementById('auth-reg-password');
|
||
const authRegError = document.getElementById('auth-reg-error');
|
||
|
||
const resetAuthForms = () => {
|
||
if (authUser) authUser.value = '';
|
||
if (authPass) authPass.value = '';
|
||
if (authError) { authError.textContent = ''; authError.classList.add('hidden'); }
|
||
if (authRegUser) authRegUser.value = '';
|
||
if (authRegPass) authRegPass.value = '';
|
||
if (authRegError) { authRegError.textContent = ''; authRegError.classList.add('hidden'); }
|
||
};
|
||
|
||
const handleLogout = () => {
|
||
if (window.PvZAuth) {
|
||
window.PvZAuth.logout();
|
||
this.loadUserData();
|
||
this.updateProfileUI();
|
||
this.updateRecordDisplay();
|
||
this.updateShopUI();
|
||
}
|
||
if (authModal) authModal.classList.add('hidden');
|
||
resetAuthForms();
|
||
if (window.PvZAudio) window.PvZAudio.playCoin();
|
||
};
|
||
|
||
if (btnAuthLogoutNav) {
|
||
btnAuthLogoutNav.addEventListener('click', handleLogout);
|
||
}
|
||
if (btnAuthLogout) {
|
||
btnAuthLogout.addEventListener('click', handleLogout);
|
||
}
|
||
|
||
if (btnAuthOpen && authModal) {
|
||
btnAuthOpen.addEventListener('click', () => {
|
||
resetAuthForms();
|
||
this.updateProfileUI();
|
||
authModal.classList.remove('hidden');
|
||
if (authUser && window.PvZAuth && window.PvZAuth.isGuest()) {
|
||
setTimeout(() => authUser.focus(), 50);
|
||
}
|
||
});
|
||
}
|
||
|
||
const startHeroProfile = document.getElementById('start-hero-profile');
|
||
if (startHeroProfile && btnAuthOpen) {
|
||
startHeroProfile.addEventListener('click', () => {
|
||
btnAuthOpen.click();
|
||
});
|
||
}
|
||
|
||
// Modal Close buttons
|
||
if (btnAuthClose && authModal) {
|
||
btnAuthClose.addEventListener('click', () => {
|
||
authModal.classList.add('hidden');
|
||
resetAuthForms();
|
||
});
|
||
}
|
||
if (btnAuthCloseProfile && authModal) {
|
||
btnAuthCloseProfile.addEventListener('click', () => {
|
||
authModal.classList.add('hidden');
|
||
});
|
||
}
|
||
if (btnAuthCloseReg && authModal) {
|
||
btnAuthCloseReg.addEventListener('click', () => {
|
||
authModal.classList.add('hidden');
|
||
resetAuthForms();
|
||
});
|
||
}
|
||
|
||
// Switch between Login and Register views
|
||
if (btnAuthToRegister && loginView && registerView) {
|
||
btnAuthToRegister.addEventListener('click', () => {
|
||
resetAuthForms();
|
||
loginView.classList.add('hidden');
|
||
registerView.classList.remove('hidden');
|
||
if (authRegUser) setTimeout(() => authRegUser.focus(), 50);
|
||
});
|
||
}
|
||
|
||
if (btnAuthToLogin && loginView && registerView) {
|
||
btnAuthToLogin.addEventListener('click', () => {
|
||
resetAuthForms();
|
||
registerView.classList.add('hidden');
|
||
loginView.classList.remove('hidden');
|
||
if (authUser) setTimeout(() => authUser.focus(), 50);
|
||
});
|
||
}
|
||
|
||
// Stay as Guest button
|
||
if (btnAuthGuest && authModal) {
|
||
btnAuthGuest.addEventListener('click', () => {
|
||
if (window.PvZAuth) window.PvZAuth.loginAsGuest();
|
||
this.loadUserData();
|
||
this.updateProfileUI();
|
||
this.updateRecordDisplay();
|
||
authModal.classList.add('hidden');
|
||
resetAuthForms();
|
||
if (window.PvZAudio) window.PvZAudio.playButtonClick();
|
||
});
|
||
}
|
||
|
||
// Login Action (Войти)
|
||
if (btnAuthSubmit && authModal) {
|
||
btnAuthSubmit.addEventListener('click', async () => {
|
||
const u = authUser ? authUser.value.trim() : '';
|
||
const p = authPass ? authPass.value : '';
|
||
|
||
if (!u) {
|
||
if (authError) { authError.textContent = 'Введите логин'; authError.classList.remove('hidden'); }
|
||
return;
|
||
}
|
||
if (!p) {
|
||
if (authError) { authError.textContent = 'Введите пароль'; authError.classList.remove('hidden'); }
|
||
return;
|
||
}
|
||
if (authError) authError.classList.add('hidden');
|
||
|
||
if (window.PvZAuth) {
|
||
btnAuthSubmit.textContent = '⏳ Вход...';
|
||
btnAuthSubmit.disabled = true;
|
||
|
||
const res = await window.PvZAuth.login(u, p);
|
||
btnAuthSubmit.textContent = '🔑 Войти';
|
||
btnAuthSubmit.disabled = false;
|
||
|
||
if (res && res.success) {
|
||
authModal.classList.add('hidden');
|
||
resetAuthForms();
|
||
this.loadUserData();
|
||
this.updateProfileUI();
|
||
this.updateRecordDisplay();
|
||
this.updateShopUI();
|
||
if (window.PvZAudio) window.PvZAudio.playCoin();
|
||
} else {
|
||
if (authError) {
|
||
authError.textContent = (res && res.message) ? res.message : 'Неверный логин или пароль';
|
||
authError.classList.remove('hidden');
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Register Action (Зарегистрироваться)
|
||
if (btnAuthDoRegister && authModal) {
|
||
btnAuthDoRegister.addEventListener('click', async () => {
|
||
const u = authRegUser ? authRegUser.value.trim() : '';
|
||
const p = authRegPass ? authRegPass.value : '';
|
||
|
||
if (!u) {
|
||
if (authRegError) { authRegError.textContent = 'Введите логин для регистрации'; authRegError.classList.remove('hidden'); }
|
||
return;
|
||
}
|
||
if (u.length < 3) {
|
||
if (authRegError) { authRegError.textContent = 'Логин должен содержать минимум 3 символа'; authRegError.classList.remove('hidden'); }
|
||
return;
|
||
}
|
||
if (!p || p.length < 4) {
|
||
if (authRegError) { authRegError.textContent = 'Пароль должен содержать минимум 4 символа'; authRegError.classList.remove('hidden'); }
|
||
return;
|
||
}
|
||
if (authRegError) authRegError.classList.add('hidden');
|
||
|
||
if (window.PvZAuth) {
|
||
btnAuthDoRegister.textContent = '⏳ Регистрация...';
|
||
btnAuthDoRegister.disabled = true;
|
||
|
||
const res = await window.PvZAuth.register(u, p);
|
||
btnAuthDoRegister.textContent = '✨ Зарегистрироваться';
|
||
btnAuthDoRegister.disabled = false;
|
||
|
||
if (res && res.success) {
|
||
authModal.classList.add('hidden');
|
||
resetAuthForms();
|
||
this.loadUserData();
|
||
this.updateProfileUI();
|
||
this.updateRecordDisplay();
|
||
this.updateShopUI();
|
||
if (window.PvZAudio) window.PvZAudio.playCoin();
|
||
} else {
|
||
if (authRegError) {
|
||
authRegError.textContent = (res && res.message) ? res.message : 'Ошибка при регистрации';
|
||
authRegError.classList.remove('hidden');
|
||
}
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Export / Import Save File Handlers
|
||
const btnExportSave = document.getElementById('btn-export-save');
|
||
const btnExportSaveProfile = document.getElementById('btn-export-save-profile');
|
||
const btnImportSave = document.getElementById('btn-import-save');
|
||
const btnImportSaveProfile = document.getElementById('btn-import-save-profile');
|
||
const inputImportSave = document.getElementById('input-import-save');
|
||
|
||
const handleExportSave = () => {
|
||
this.saveUserData();
|
||
if (window.PvZAuth && window.PvZAuth.exportSaveFile) {
|
||
window.PvZAuth.exportSaveFile();
|
||
}
|
||
};
|
||
|
||
if (btnExportSave) btnExportSave.addEventListener('click', handleExportSave);
|
||
if (btnExportSaveProfile) btnExportSaveProfile.addEventListener('click', handleExportSave);
|
||
|
||
const handleTriggerImport = () => {
|
||
if (inputImportSave) {
|
||
inputImportSave.value = '';
|
||
inputImportSave.click();
|
||
}
|
||
};
|
||
|
||
if (btnImportSave) btnImportSave.addEventListener('click', handleTriggerImport);
|
||
if (btnImportSaveProfile) btnImportSaveProfile.addEventListener('click', handleTriggerImport);
|
||
|
||
if (inputImportSave) {
|
||
inputImportSave.addEventListener('change', async (e) => {
|
||
const file = e.target.files && e.target.files[0];
|
||
if (!file) return;
|
||
if (window.PvZAuth && window.PvZAuth.importSaveFile) {
|
||
const res = await window.PvZAuth.importSaveFile(file);
|
||
if (res && res.success) {
|
||
this.loadUserData();
|
||
this.updateProfileUI();
|
||
this.updateRecordDisplay();
|
||
this.updateShopUI();
|
||
}
|
||
}
|
||
inputImportSave.value = '';
|
||
});
|
||
}
|
||
|
||
// Auto-save on page hide / tab switch / page unload
|
||
document.addEventListener('visibilitychange', () => {
|
||
if (document.visibilityState === 'hidden') {
|
||
this.saveUserData();
|
||
if (window.PvZAuth && window.PvZAuth.flushSave) {
|
||
window.PvZAuth.flushSave();
|
||
}
|
||
}
|
||
});
|
||
|
||
window.addEventListener('beforeunload', () => {
|
||
this.saveUserData();
|
||
if (window.PvZAuth && window.PvZAuth.flushSave) {
|
||
window.PvZAuth.flushSave(true);
|
||
}
|
||
});
|
||
|
||
// Inventory / Deck Builder Buttons
|
||
const btnInvStart = document.getElementById('btn-inventory-start');
|
||
const btnDrawerInv = document.getElementById('btn-drawer-inventory');
|
||
if (btnInvStart) {
|
||
btnInvStart.addEventListener('click', () => {
|
||
this.openSeedSelectionModal(this.currentLevel || 1, this.currentMode || 'adventure');
|
||
});
|
||
}
|
||
if (btnDrawerInv) {
|
||
btnDrawerInv.addEventListener('click', () => {
|
||
const drawer = document.getElementById('mobile-drawer');
|
||
const backdrop = document.getElementById('drawer-backdrop');
|
||
if (drawer) drawer.classList.add('hidden');
|
||
if (backdrop) backdrop.classList.add('hidden');
|
||
this.openSeedSelectionModal(this.currentLevel || 1, this.currentMode || 'adventure');
|
||
});
|
||
}
|
||
|
||
// Shop Button & Tabs
|
||
const btnShop = document.getElementById('btn-shop');
|
||
const shopModal = document.getElementById('shop-modal');
|
||
const btnCloseShop = document.getElementById('btn-close-shop');
|
||
const tabShopPlants = document.getElementById('tab-shop-plants');
|
||
const tabShopUpgrades = document.getElementById('tab-shop-upgrades');
|
||
const panelShopPlants = document.getElementById('shop-plants-panel');
|
||
const panelShopUpgrades = document.getElementById('shop-upgrades-panel');
|
||
|
||
if (btnShop && shopModal) {
|
||
btnShop.addEventListener('click', () => {
|
||
this.updateShopUI();
|
||
shopModal.classList.remove('hidden');
|
||
});
|
||
}
|
||
if (btnCloseShop && shopModal) {
|
||
btnCloseShop.addEventListener('click', () => {
|
||
shopModal.classList.add('hidden');
|
||
this.updateRecordDisplay();
|
||
});
|
||
}
|
||
|
||
if (tabShopPlants && tabShopUpgrades && panelShopPlants && panelShopUpgrades) {
|
||
tabShopPlants.addEventListener('click', () => {
|
||
tabShopPlants.classList.add('active');
|
||
tabShopUpgrades.classList.remove('active');
|
||
panelShopPlants.classList.remove('hidden');
|
||
panelShopUpgrades.classList.add('hidden');
|
||
});
|
||
tabShopUpgrades.addEventListener('click', () => {
|
||
tabShopUpgrades.classList.add('active');
|
||
tabShopPlants.classList.remove('active');
|
||
panelShopUpgrades.classList.remove('hidden');
|
||
panelShopPlants.classList.add('hidden');
|
||
});
|
||
}
|
||
|
||
// Pre-Battle Seed Selection & Inventory Modal Handlers
|
||
const seedSelectModal = document.getElementById('seed-select-modal');
|
||
const btnSeedStart = document.getElementById('btn-seed-start-battle');
|
||
const btnSeedAuto = document.getElementById('btn-seed-auto-fill');
|
||
const btnSeedShop = document.getElementById('btn-seed-open-shop');
|
||
const btnSeedCancel = document.getElementById('btn-seed-cancel');
|
||
|
||
if (btnSeedStart) {
|
||
btnSeedStart.addEventListener('click', () => {
|
||
if (!this.economy.selectedDeck || this.economy.selectedDeck.length === 0) {
|
||
this.economy.autoFillDeck();
|
||
}
|
||
if (seedSelectModal) seedSelectModal.classList.add('hidden');
|
||
const startModal = document.getElementById('start-modal');
|
||
if (startModal) startModal.classList.add('hidden');
|
||
|
||
if (window.PvZAudio) window.PvZAudio.init();
|
||
const lvl = this.pendingLevel || this.currentLevel || 1;
|
||
const mode = this.pendingMode || this.currentMode || 'adventure';
|
||
|
||
this.startLevel(lvl, mode);
|
||
if (!this.loopStarted) {
|
||
this.loopStarted = true;
|
||
requestAnimationFrame(this.loop.bind(this));
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnSeedAuto) {
|
||
btnSeedAuto.addEventListener('click', () => {
|
||
this.economy.autoFillDeck();
|
||
this.renderSeedSelectionUI();
|
||
if (window.PvZAudio) window.PvZAudio.playButtonClick();
|
||
});
|
||
}
|
||
|
||
if (btnSeedShop) {
|
||
btnSeedShop.addEventListener('click', () => {
|
||
if (seedSelectModal) seedSelectModal.classList.add('hidden');
|
||
this.updateShopUI();
|
||
if (shopModal) shopModal.classList.remove('hidden');
|
||
});
|
||
}
|
||
|
||
if (btnSeedCancel) {
|
||
btnSeedCancel.addEventListener('click', () => {
|
||
if (seedSelectModal) seedSelectModal.classList.add('hidden');
|
||
const startModal = document.getElementById('start-modal');
|
||
if (startModal) startModal.classList.remove('hidden');
|
||
});
|
||
}
|
||
|
||
// Shop Item Purchases
|
||
const btnBuySlot = document.getElementById('btn-buy-slot');
|
||
if (btnBuySlot) {
|
||
btnBuySlot.addEventListener('click', () => {
|
||
if (this.economy.buyUpgrade('extraSlot')) {
|
||
if (window.PvZAudio) window.PvZAudio.playWin();
|
||
this.saveUserData();
|
||
this.updateShopUI();
|
||
this.renderSeedSelectionUI();
|
||
}
|
||
});
|
||
}
|
||
|
||
const btnBuyRake = document.getElementById('btn-buy-rake');
|
||
if (btnBuyRake) {
|
||
btnBuyRake.addEventListener('click', () => {
|
||
if (this.economy.buyUpgrade('rake')) {
|
||
if (window.PvZAudio) window.PvZAudio.playWin();
|
||
this.saveUserData();
|
||
this.updateShopUI();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Workshop Modal Handlers
|
||
const workshopModal = document.getElementById('workshop-modal');
|
||
const btnWorkshopNav = document.getElementById('btn-workshop');
|
||
const btnWorkshopStart = document.getElementById('btn-workshop-start');
|
||
const btnCloseWorkshop = document.getElementById('btn-close-workshop');
|
||
const tabPlants = document.getElementById('tab-plants');
|
||
const tabPerks = document.getElementById('tab-perks');
|
||
const contentPlants = document.getElementById('ws-content-plants');
|
||
const contentPerks = document.getElementById('ws-content-perks');
|
||
|
||
const openWorkshop = () => {
|
||
this.updateWorkshopUI();
|
||
if (workshopModal) workshopModal.classList.remove('hidden');
|
||
};
|
||
|
||
if (btnWorkshopNav) btnWorkshopNav.addEventListener('click', openWorkshop);
|
||
if (btnWorkshopStart) btnWorkshopStart.addEventListener('click', openWorkshop);
|
||
if (btnCloseWorkshop && workshopModal) {
|
||
btnCloseWorkshop.addEventListener('click', () => {
|
||
workshopModal.classList.add('hidden');
|
||
this.updateRecordDisplay();
|
||
});
|
||
}
|
||
|
||
if (tabPlants && tabPerks && contentPlants && contentPerks) {
|
||
tabPlants.addEventListener('click', () => {
|
||
tabPlants.classList.add('active');
|
||
tabPerks.classList.remove('active');
|
||
contentPlants.classList.remove('hidden');
|
||
contentPerks.classList.add('hidden');
|
||
});
|
||
tabPerks.addEventListener('click', () => {
|
||
tabPerks.classList.add('active');
|
||
tabPlants.classList.remove('active');
|
||
contentPerks.classList.remove('hidden');
|
||
contentPlants.classList.add('hidden');
|
||
});
|
||
}
|
||
|
||
// Zen Garden Modal Handlers
|
||
const zenModal = document.getElementById('zengarden-modal');
|
||
const btnZenNav = document.getElementById('btn-zengarden');
|
||
const btnZenStart = document.getElementById('btn-zengarden-start');
|
||
const btnCloseZen = document.getElementById('btn-close-zengarden');
|
||
|
||
const openZenGarden = () => {
|
||
if (this.zengarden) {
|
||
this.zengarden.calculateOfflineEarnings();
|
||
}
|
||
this.updateZenGardenUI();
|
||
if (zenModal) zenModal.classList.remove('hidden');
|
||
};
|
||
|
||
if (btnZenNav) btnZenNav.addEventListener('click', openZenGarden);
|
||
if (btnZenStart) btnZenStart.addEventListener('click', openZenGarden);
|
||
if (btnCloseZen && zenModal) {
|
||
btnCloseZen.addEventListener('click', () => {
|
||
zenModal.classList.add('hidden');
|
||
this.updateRecordDisplay();
|
||
});
|
||
}
|
||
|
||
// Suburban Almanac Modal Handlers
|
||
const almanacModal = document.getElementById('almanac-modal');
|
||
const btnAlmanacNav = document.getElementById('btn-almanac');
|
||
const btnAlmanacStart = document.getElementById('btn-almanac-start');
|
||
const btnCloseAlmanac = document.getElementById('btn-close-almanac');
|
||
|
||
const openAlmanac = () => {
|
||
this.renderAlmanacUI();
|
||
if (almanacModal) almanacModal.classList.remove('hidden');
|
||
};
|
||
|
||
if (btnAlmanacNav) btnAlmanacNav.addEventListener('click', openAlmanac);
|
||
if (btnAlmanacStart) btnAlmanacStart.addEventListener('click', openAlmanac);
|
||
if (btnCloseAlmanac && almanacModal) {
|
||
btnCloseAlmanac.addEventListener('click', () => {
|
||
almanacModal.classList.add('hidden');
|
||
});
|
||
}
|
||
|
||
// Achievements Modal Handlers
|
||
const achModal = document.getElementById('achievements-modal');
|
||
const btnAchNav = document.getElementById('btn-achievements');
|
||
const btnAchStart = document.getElementById('btn-achievements-start');
|
||
const btnCloseAch = document.getElementById('btn-close-achievements');
|
||
|
||
const openAchievements = () => {
|
||
this.renderAchievementsUI();
|
||
if (achModal) achModal.classList.remove('hidden');
|
||
};
|
||
|
||
if (btnAchNav) btnAchNav.addEventListener('click', openAchievements);
|
||
if (btnAchStart) btnAchStart.addEventListener('click', openAchievements);
|
||
if (btnCloseAch && achModal) {
|
||
btnCloseAch.addEventListener('click', () => {
|
||
achModal.classList.add('hidden');
|
||
});
|
||
}
|
||
|
||
// Bug Reporting Modal Handlers
|
||
const bugModal = document.getElementById('bugreport-modal');
|
||
const btnBugNav = document.getElementById('btn-bugreport');
|
||
const btnBugStart = document.getElementById('btn-bugreport-start');
|
||
const btnCloseBug = document.getElementById('btn-bug-close');
|
||
|
||
const openBugReport = () => {
|
||
if (this.authSystem && this.authSystem.currentUser) {
|
||
const nameInput = document.getElementById('bug-input-name');
|
||
if (nameInput && !nameInput.value) {
|
||
nameInput.value = this.authSystem.currentUser.username;
|
||
}
|
||
}
|
||
if (window.PvZFeedback) {
|
||
window.PvZFeedback.fetchReports();
|
||
}
|
||
if (bugModal) bugModal.classList.remove('hidden');
|
||
};
|
||
|
||
if (btnBugNav) btnBugNav.addEventListener('click', openBugReport);
|
||
if (btnBugStart) btnBugStart.addEventListener('click', openBugReport);
|
||
if (btnCloseBug && bugModal) {
|
||
btnCloseBug.addEventListener('click', () => {
|
||
bugModal.classList.add('hidden');
|
||
});
|
||
}
|
||
|
||
// Initialize Zen Garden & Almanac & Feedback DOM bindings
|
||
this.initZenGarden();
|
||
this.initAlmanac();
|
||
this.initFeedbackUI();
|
||
|
||
// Mode Buttons on Start Screen
|
||
const btnModeAdv = document.getElementById('btn-mode-adv');
|
||
if (btnModeAdv) {
|
||
btnModeAdv.addEventListener('click', () => {
|
||
this.currentMode = 'adventure';
|
||
this.highlightModeBtn('btn-mode-adv');
|
||
this.updateRecordDisplay();
|
||
});
|
||
}
|
||
|
||
const btnModeSurv = document.getElementById('btn-mode-surv');
|
||
if (btnModeSurv) {
|
||
btnModeSurv.addEventListener('click', () => {
|
||
this.currentMode = 'survival';
|
||
this.highlightModeBtn('btn-mode-surv');
|
||
this.updateRecordDisplay();
|
||
});
|
||
}
|
||
|
||
const btnModeBowl = document.getElementById('btn-mode-bowl');
|
||
if (btnModeBowl) {
|
||
btnModeBowl.addEventListener('click', () => {
|
||
this.currentMode = 'bowling';
|
||
this.highlightModeBtn('btn-mode-bowl');
|
||
this.updateRecordDisplay();
|
||
});
|
||
}
|
||
|
||
const btnModeIzombie = document.getElementById('btn-mode-izombie');
|
||
if (btnModeIzombie) {
|
||
btnModeIzombie.addEventListener('click', () => {
|
||
this.currentMode = 'izombie';
|
||
this.highlightModeBtn('btn-mode-izombie');
|
||
this.updateRecordDisplay();
|
||
});
|
||
}
|
||
|
||
const btnModeRogue = document.getElementById('btn-mode-rogue');
|
||
if (btnModeRogue) {
|
||
btnModeRogue.addEventListener('click', () => {
|
||
this.currentMode = 'roguelike';
|
||
if (this.roguelike) this.roguelike.reset();
|
||
this.highlightModeBtn('btn-mode-rogue');
|
||
this.updateRecordDisplay();
|
||
});
|
||
}
|
||
|
||
const btnModeBoss = document.getElementById('btn-mode-boss');
|
||
if (btnModeBoss) {
|
||
btnModeBoss.addEventListener('click', () => {
|
||
this.currentMode = 'boss';
|
||
this.highlightModeBtn('btn-mode-boss');
|
||
this.updateRecordDisplay();
|
||
});
|
||
}
|
||
|
||
const btnModeBossRush = document.getElementById('btn-mode-bossrush');
|
||
if (btnModeBossRush) {
|
||
btnModeBossRush.addEventListener('click', () => {
|
||
this.currentMode = 'bossrush';
|
||
this.highlightModeBtn('btn-mode-bossrush');
|
||
this.updateRecordDisplay();
|
||
});
|
||
}
|
||
|
||
// Theme Switcher Button
|
||
const btnTheme = document.getElementById('btn-theme');
|
||
if (btnTheme) {
|
||
btnTheme.addEventListener('click', () => {
|
||
this.cycleTheme();
|
||
});
|
||
}
|
||
|
||
// Tree of Wisdom Modal Handlers
|
||
const treeModal = document.getElementById('tree-modal');
|
||
const btnTreeNav = document.getElementById('btn-tree');
|
||
const btnTreeStart = document.getElementById('btn-tree-start');
|
||
const btnCloseTree = document.getElementById('btn-tree-close');
|
||
const btnTreeFeed = document.getElementById('btn-tree-feed');
|
||
|
||
const openTree = () => {
|
||
this.renderTreeUI();
|
||
if (treeModal) treeModal.classList.remove('hidden');
|
||
};
|
||
|
||
if (btnTreeNav) btnTreeNav.addEventListener('click', openTree);
|
||
if (btnTreeStart) btnTreeStart.addEventListener('click', openTree);
|
||
if (btnCloseTree && treeModal) {
|
||
btnCloseTree.addEventListener('click', () => {
|
||
treeModal.classList.add('hidden');
|
||
});
|
||
}
|
||
|
||
if (btnTreeFeed) {
|
||
btnTreeFeed.addEventListener('click', () => {
|
||
if (this.tree && this.tree.feedFertilizer()) {
|
||
this.renderTreeUI();
|
||
this.updateRecordDisplay();
|
||
this.updateShopUI();
|
||
}
|
||
});
|
||
}
|
||
|
||
// Leaderboards Modal Handlers
|
||
const lbModal = document.getElementById('leaderboard-modal');
|
||
const btnLbNav = document.getElementById('btn-leaderboard');
|
||
const btnLbStart = document.getElementById('btn-leaderboard-start');
|
||
const btnCloseLb = document.getElementById('btn-leaderboard-close');
|
||
|
||
const openLeaderboard = (mode = 'survival') => {
|
||
this.renderLeaderboardUI(mode);
|
||
if (lbModal) lbModal.classList.remove('hidden');
|
||
};
|
||
|
||
if (btnLbNav) btnLbNav.addEventListener('click', () => openLeaderboard('survival'));
|
||
if (btnLbStart) btnLbStart.addEventListener('click', () => openLeaderboard('survival'));
|
||
if (btnCloseLb && lbModal) {
|
||
btnCloseLb.addEventListener('click', () => {
|
||
lbModal.classList.add('hidden');
|
||
});
|
||
}
|
||
|
||
const lbTabs = [
|
||
{ id: 'tab-lb-survival', mode: 'survival' },
|
||
{ id: 'tab-lb-rogue', mode: 'roguelike' },
|
||
{ id: 'tab-lb-adv', mode: 'adventure' },
|
||
{ id: 'tab-lb-izombie', mode: 'izombie' },
|
||
{ id: 'tab-lb-bowl', mode: 'bowling' },
|
||
{ id: 'tab-lb-boss', mode: 'boss' },
|
||
{ id: 'tab-lb-bossrush', mode: 'bossrush' }
|
||
];
|
||
|
||
lbTabs.forEach(t => {
|
||
const el = document.getElementById(t.id);
|
||
if (el) {
|
||
el.addEventListener('click', () => {
|
||
lbTabs.forEach(other => {
|
||
const oEl = document.getElementById(other.id);
|
||
if (oEl) oEl.classList.remove('active');
|
||
});
|
||
el.classList.add('active');
|
||
this.renderLeaderboardUI(t.mode);
|
||
});
|
||
}
|
||
});
|
||
|
||
// Campaign Map Modal Handlers
|
||
const campaignModal = document.getElementById('campaign-modal');
|
||
const btnCampaignNav = document.getElementById('btn-campaign-open');
|
||
const btnCampaignStart = document.getElementById('btn-campaign-select');
|
||
const btnCloseCampaign = document.getElementById('btn-campaign-close');
|
||
|
||
if (btnCampaignNav) btnCampaignNav.addEventListener('click', () => this.openCampaignModal());
|
||
if (btnCampaignStart) btnCampaignStart.addEventListener('click', () => this.openCampaignModal());
|
||
if (btnCloseCampaign && campaignModal) {
|
||
btnCloseCampaign.addEventListener('click', () => {
|
||
campaignModal.classList.add('hidden');
|
||
this.updateRecordDisplay();
|
||
});
|
||
}
|
||
|
||
for (let w = 1; w <= 5; w++) {
|
||
const tab = document.getElementById(`tab-world-${w}`);
|
||
if (tab) {
|
||
tab.addEventListener('click', () => {
|
||
this.renderCampaignMapUI(w);
|
||
});
|
||
}
|
||
}
|
||
}
|
||
|
||
openCampaignModal(worldId = null) {
|
||
if (!worldId) {
|
||
if (window.CampaignSystem) {
|
||
const currentItem = window.CampaignSystem.getLevel(this.currentLevel);
|
||
worldId = currentItem ? currentItem.world : 1;
|
||
} else {
|
||
worldId = 1;
|
||
}
|
||
}
|
||
this.renderCampaignMapUI(worldId);
|
||
const modal = document.getElementById('campaign-modal');
|
||
if (modal) modal.classList.remove('hidden');
|
||
}
|
||
|
||
renderCampaignMapUI(activeWorldId = 1) {
|
||
const gridEl = document.getElementById('campaign-levels-grid');
|
||
if (!gridEl || !window.CampaignSystem) return;
|
||
|
||
// Update active tab styling
|
||
for (let w = 1; w <= 5; w++) {
|
||
const tab = document.getElementById(`tab-world-${w}`);
|
||
if (tab) {
|
||
if (w === activeWorldId) {
|
||
tab.classList.add('active');
|
||
} else {
|
||
tab.classList.remove('active');
|
||
}
|
||
}
|
||
}
|
||
|
||
const levels = window.CampaignSystem.getLevelsByWorld(activeWorldId);
|
||
const world = window.CampaignSystem.getWorld(activeWorldId);
|
||
|
||
let html = '';
|
||
levels.forEach(lvl => {
|
||
const isPassed = lvl.level < this.currentLevel;
|
||
const isCurrent = lvl.level === this.currentLevel;
|
||
const isUnlocked = lvl.level <= (this.maxUnlockedLevel || 1);
|
||
|
||
let badgeClass = 'badge-locked';
|
||
let badgeText = '🔒 Заблокировано';
|
||
if (isCurrent) {
|
||
badgeClass = 'badge-current';
|
||
badgeText = '⚔️ Текущий';
|
||
} else if (isPassed) {
|
||
badgeClass = 'badge-done';
|
||
badgeText = '⭐ Пройдено';
|
||
} else if (isUnlocked) {
|
||
badgeClass = 'badge-unlocked';
|
||
badgeText = '🔓 Доступно';
|
||
}
|
||
|
||
const cardClass = `campaign-level-card${isCurrent ? ' current' : ''}${!isUnlocked ? ' locked' : ''}`;
|
||
const buttonHtml = isUnlocked
|
||
? `<button class="btn-play-level" data-level="${lvl.level}">ИГРАТЬ (ЭТАП ${lvl.code})</button>`
|
||
: `<button class="btn-play-level locked" disabled>🔒 ЗАКРЫТО</button>`;
|
||
|
||
const biomeNames = {
|
||
day: '☀️ День',
|
||
night: '🌙 Ночь',
|
||
pool: '🏊 Бассейн (6 линий)',
|
||
fog: '🌫️ Туман (6 линий)',
|
||
roof: '🏠 Крыша'
|
||
};
|
||
|
||
html += `
|
||
<div class="${cardClass}">
|
||
<div class="level-card-header">
|
||
<span class="level-code">${world.icon} Этап ${lvl.code}</span>
|
||
<span class="level-badge ${badgeClass}">${badgeText}</span>
|
||
</div>
|
||
<div class="level-card-body">
|
||
<div class="level-title">${lvl.title}</div>
|
||
<div class="level-desc">${lvl.desc}</div>
|
||
</div>
|
||
<div class="level-card-info">
|
||
<span>🌊 Волн: <strong>${lvl.waves}</strong></span>
|
||
<span>💰 Награда: <strong>+${lvl.reward}</strong></span>
|
||
<span>🏞️ ${biomeNames[lvl.biome] || lvl.biome}</span>
|
||
</div>
|
||
${buttonHtml}
|
||
</div>
|
||
`;
|
||
});
|
||
|
||
gridEl.innerHTML = html;
|
||
|
||
// Attach click listeners to level launch buttons
|
||
gridEl.querySelectorAll('.btn-play-level[data-level]').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
const targetLevel = parseInt(e.currentTarget.getAttribute('data-level'), 10);
|
||
if (targetLevel) {
|
||
const campModal = document.getElementById('campaign-modal');
|
||
if (campModal) campModal.classList.add('hidden');
|
||
const startModal = document.getElementById('start-modal');
|
||
if (startModal) startModal.classList.add('hidden');
|
||
|
||
if (window.PvZAudio) window.PvZAudio.init();
|
||
if (targetLevel === 25) {
|
||
this.startLevel(targetLevel, 'adventure');
|
||
} else {
|
||
this.openSeedSelectionModal(targetLevel, 'adventure');
|
||
}
|
||
if (!this.loopStarted) {
|
||
this.loopStarted = true;
|
||
requestAnimationFrame(this.loop.bind(this));
|
||
}
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
highlightModeBtn(activeId) {
|
||
['btn-mode-adv', 'btn-mode-surv', 'btn-mode-bowl', 'btn-mode-izombie', 'btn-mode-rogue', 'btn-mode-boss', 'btn-mode-bossrush'].forEach(id => {
|
||
const btn = document.getElementById(id);
|
||
if (btn) {
|
||
const isActive = id === activeId;
|
||
btn.classList.toggle('active', isActive);
|
||
btn.style.borderColor = isActive ? '#ffeb3b' : '';
|
||
btn.style.opacity = isActive ? '1' : '0.7';
|
||
}
|
||
});
|
||
}
|
||
|
||
openSeedSelectionModal(targetLevel = null, targetMode = null) {
|
||
this.pendingLevel = targetLevel !== null ? targetLevel : (this.currentLevel || 1);
|
||
this.pendingMode = targetMode !== null ? targetMode : (this.currentMode || 'adventure');
|
||
|
||
this.renderSeedSelectionUI();
|
||
|
||
const modal = document.getElementById('seed-select-modal');
|
||
if (modal) modal.classList.remove('hidden');
|
||
|
||
const startModal = document.getElementById('start-modal');
|
||
if (startModal) startModal.classList.add('hidden');
|
||
const campaignModal = document.getElementById('campaign-modal');
|
||
if (campaignModal) campaignModal.classList.add('hidden');
|
||
}
|
||
|
||
renderSeedSelectionUI() {
|
||
const activeDeckContainer = document.getElementById('seed-active-deck');
|
||
const inventoryContainer = document.getElementById('seed-inventory-grid');
|
||
const deckCountEl = document.getElementById('seed-deck-count');
|
||
const deckMaxEl = document.getElementById('seed-deck-max');
|
||
|
||
const maxSlots = this.economy.getMaxSlots();
|
||
const selectedDeck = this.economy.selectedDeck || [];
|
||
|
||
if (deckCountEl) deckCountEl.textContent = selectedDeck.length;
|
||
if (deckMaxEl) deckMaxEl.textContent = maxSlots;
|
||
|
||
// 1. Render Active Deck Slots
|
||
if (activeDeckContainer) {
|
||
activeDeckContainer.innerHTML = '';
|
||
selectedDeck.forEach(plantId => {
|
||
const p = window.PLANT_CATALOGUE.find(item => item.id === plantId);
|
||
if (!p) return;
|
||
|
||
const slot = document.createElement('div');
|
||
slot.className = 'seed-slot-card';
|
||
slot.title = `Нажмите, чтобы убрать ${p.name} из колоды`;
|
||
slot.innerHTML = `
|
||
<div class="slot-remove-badge">✕</div>
|
||
<div class="slot-icon">${p.icon}</div>
|
||
<div class="slot-cost">☀️ ${p.cost}</div>
|
||
`;
|
||
slot.addEventListener('click', () => {
|
||
this.economy.removeFromDeck(plantId);
|
||
this.renderSeedSelectionUI();
|
||
if (window.PvZAudio) window.PvZAudio.playButtonClick();
|
||
});
|
||
activeDeckContainer.appendChild(slot);
|
||
});
|
||
|
||
// Render Empty Slots up to maxSlots
|
||
for (let i = selectedDeck.length; i < maxSlots; i++) {
|
||
const emptySlot = document.createElement('div');
|
||
emptySlot.className = 'seed-slot-empty';
|
||
emptySlot.textContent = `+ Слот ${i + 1}`;
|
||
activeDeckContainer.appendChild(emptySlot);
|
||
}
|
||
}
|
||
|
||
// 2. Render Inventory Grid
|
||
if (inventoryContainer && window.PLANT_CATALOGUE) {
|
||
inventoryContainer.innerHTML = '';
|
||
window.PLANT_CATALOGUE.forEach(p => {
|
||
const isUnlocked = this.economy.isPlantUnlocked(p.id);
|
||
const isInDeck = selectedDeck.includes(p.id);
|
||
const canAfford = this.economy.totalCoins >= (p.price || 0);
|
||
|
||
const item = document.createElement('div');
|
||
item.className = `seed-inv-item ${isUnlocked ? (isInDeck ? 'in-deck' : 'unlocked') : 'locked'}`;
|
||
|
||
let statusBadge = '';
|
||
if (isInDeck) {
|
||
statusBadge = '<span class="seed-inv-status status-deck">✓ В колоде</span>';
|
||
} else if (isUnlocked) {
|
||
statusBadge = '<span class="seed-inv-status status-ready">+ Выбрать</span>';
|
||
} else {
|
||
statusBadge = `<button type="button" class="btn btn-sm btn-shop" style="font-size: 10px; padding: 2px 6px; margin-top: 3px;" ${canAfford ? '' : 'disabled'}>🔒 ${p.price} 💰</button>`;
|
||
}
|
||
|
||
item.innerHTML = `
|
||
<div class="seed-inv-icon">${p.icon}</div>
|
||
<div class="seed-inv-name" title="${p.name}">${p.name}</div>
|
||
<div class="seed-inv-sun">☀️ ${p.cost}</div>
|
||
${statusBadge}
|
||
`;
|
||
|
||
if (isUnlocked) {
|
||
item.addEventListener('click', () => {
|
||
if (isInDeck) {
|
||
this.economy.removeFromDeck(p.id);
|
||
} else {
|
||
this.economy.addToDeck(p.id);
|
||
}
|
||
this.renderSeedSelectionUI();
|
||
if (window.PvZAudio) window.PvZAudio.playButtonClick();
|
||
});
|
||
} else {
|
||
const buyBtn = item.querySelector('button');
|
||
if (buyBtn) {
|
||
buyBtn.addEventListener('click', (e) => {
|
||
e.stopPropagation();
|
||
const res = this.economy.buyPlant(p.id);
|
||
if (res.success) {
|
||
if (window.PvZAudio) window.PvZAudio.playCoin();
|
||
this.saveUserData();
|
||
this.updateShopUI();
|
||
this.renderSeedSelectionUI();
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
inventoryContainer.appendChild(item);
|
||
});
|
||
}
|
||
}
|
||
|
||
updateShopUI() {
|
||
const elCoins = document.getElementById('shop-coins');
|
||
if (elCoins) elCoins.textContent = `💰 Монеты: ${this.economy.totalCoins.toLocaleString('ru-RU')}`;
|
||
|
||
const btnBuySlot = document.getElementById('btn-buy-slot');
|
||
if (btnBuySlot) {
|
||
btnBuySlot.textContent = this.economy.upgrades.extraSlot ? 'Куплено ✔️' : 'Купить (500 💰)';
|
||
btnBuySlot.disabled = this.economy.upgrades.extraSlot || this.economy.totalCoins < 500;
|
||
}
|
||
|
||
const btnBuyRake = document.getElementById('btn-buy-rake');
|
||
if (btnBuyRake) {
|
||
btnBuyRake.textContent = this.economy.upgrades.hasRake ? 'Куплено ✔️' : 'Купить (150 💰)';
|
||
btnBuyRake.disabled = this.economy.upgrades.hasRake || this.economy.totalCoins < 150;
|
||
}
|
||
|
||
// Populate Shop Plants Grid
|
||
const shopPlantsGrid = document.getElementById('shop-plants-grid');
|
||
if (shopPlantsGrid && window.PLANT_CATALOGUE) {
|
||
shopPlantsGrid.innerHTML = '';
|
||
window.PLANT_CATALOGUE.forEach(p => {
|
||
const isBasic = this.economy.defaultUnlocked.includes(p.id);
|
||
const isUnlocked = this.economy.isPlantUnlocked(p.id);
|
||
const canAfford = this.economy.totalCoins >= (p.price || 0);
|
||
|
||
const card = document.createElement('div');
|
||
card.className = 'shop-plant-card';
|
||
|
||
let actionHtml = '';
|
||
if (isBasic) {
|
||
actionHtml = '<span class="badge-purchased" style="background: rgba(33, 150, 243, 0.25); border-color: #64b5f6; color: #90caf9;">Базовое ✅</span>';
|
||
} else if (isUnlocked) {
|
||
actionHtml = '<span class="badge-purchased">Куплено ✅</span>';
|
||
} else {
|
||
actionHtml = `<button type="button" class="btn-buy" data-plant-id="${p.id}" ${canAfford ? '' : 'disabled'}>Купить (${p.price} 💰)</button>`;
|
||
}
|
||
|
||
card.innerHTML = `
|
||
<div class="shop-plant-icon">${p.icon}</div>
|
||
<div class="shop-plant-info">
|
||
<h4>${p.name}</h4>
|
||
<div class="shop-plant-cost">☀️ ${p.cost} солнца</div>
|
||
<p>${p.desc}</p>
|
||
</div>
|
||
<div>${actionHtml}</div>
|
||
`;
|
||
|
||
const buyBtn = card.querySelector('button.btn-buy');
|
||
if (buyBtn) {
|
||
buyBtn.addEventListener('click', () => {
|
||
const res = this.economy.buyPlant(p.id);
|
||
if (res.success) {
|
||
if (window.PvZAudio) window.PvZAudio.playCoin();
|
||
this.saveUserData();
|
||
this.updateShopUI();
|
||
this.renderSeedSelectionUI();
|
||
}
|
||
});
|
||
}
|
||
|
||
shopPlantsGrid.appendChild(card);
|
||
});
|
||
}
|
||
}
|
||
|
||
updateWorkshopUI() {
|
||
if (!this.workshop) return;
|
||
|
||
// 1. Materials bar
|
||
const matBio = document.getElementById('mat-bio');
|
||
const matScrap = document.getElementById('mat-scrap');
|
||
const matSun = document.getElementById('mat-sun');
|
||
const matCrystal = document.getElementById('mat-crystal');
|
||
const matCoins = document.getElementById('mat-coins');
|
||
|
||
if (matBio) matBio.textContent = this.workshop.materials.bioHumus;
|
||
if (matScrap) matScrap.textContent = this.workshop.materials.scrapMetal;
|
||
if (matSun) matSun.textContent = this.workshop.materials.sunEssence;
|
||
if (matCrystal) matCrystal.textContent = this.workshop.materials.crystals;
|
||
if (matCoins) matCoins.textContent = this.economy ? this.economy.totalCoins : 0;
|
||
|
||
// 2. Plants Upgrades List
|
||
const plantsContainer = document.getElementById('plants-upgrade-list');
|
||
if (plantsContainer) {
|
||
plantsContainer.innerHTML = '';
|
||
const pDefs = this.workshop.plantDefs;
|
||
for (const [id, def] of Object.entries(pDefs)) {
|
||
const lvl = this.workshop.getPlantLevel(id);
|
||
const curTier = def.tiers[lvl];
|
||
const nextTier = lvl < 3 ? def.tiers[lvl + 1] : null;
|
||
|
||
const card = document.createElement('div');
|
||
card.className = 'upgrade-card';
|
||
|
||
let costHtml = '';
|
||
let canAfford = false;
|
||
if (nextTier) {
|
||
canAfford = this.workshop.canAffordCost(nextTier.cost);
|
||
const parts = [];
|
||
if (nextTier.cost.coins) {
|
||
const hasCoins = this.economy && this.economy.totalCoins >= nextTier.cost.coins;
|
||
parts.push(`<span class="cost-badge ${hasCoins ? '' : 'insufficient'}">💰 ${nextTier.cost.coins}</span>`);
|
||
}
|
||
if (nextTier.cost.bioHumus) {
|
||
const hasBio = this.workshop.materials.bioHumus >= nextTier.cost.bioHumus;
|
||
parts.push(`<span class="cost-badge ${hasBio ? '' : 'insufficient'}">🧪 ${nextTier.cost.bioHumus}</span>`);
|
||
}
|
||
if (nextTier.cost.scrapMetal) {
|
||
const hasScrap = this.workshop.materials.scrapMetal >= nextTier.cost.scrapMetal;
|
||
parts.push(`<span class="cost-badge ${hasScrap ? '' : 'insufficient'}">⚙️ ${nextTier.cost.scrapMetal}</span>`);
|
||
}
|
||
if (nextTier.cost.sunEssence) {
|
||
const hasSun = this.workshop.materials.sunEssence >= nextTier.cost.sunEssence;
|
||
parts.push(`<span class="cost-badge ${hasSun ? '' : 'insufficient'}">☀️ ${nextTier.cost.sunEssence}</span>`);
|
||
}
|
||
if (nextTier.cost.crystals) {
|
||
const hasCryst = this.workshop.materials.crystals >= nextTier.cost.crystals;
|
||
parts.push(`<span class="cost-badge ${hasCryst ? '' : 'insufficient'}">💎 ${nextTier.cost.crystals}</span>`);
|
||
}
|
||
costHtml = parts.join(' ');
|
||
}
|
||
|
||
const stars = lvl === 3 ? '★★★' : (lvl === 2 ? '★★☆' : '★☆☆');
|
||
const badgeClass = lvl === 3 ? 'upgrade-level-badge maxed' : 'upgrade-level-badge';
|
||
|
||
card.innerHTML = `
|
||
<div class="upgrade-card-header">
|
||
<div class="upgrade-title"><span>${def.icon}</span> <span>${def.name}</span></div>
|
||
<div class="${badgeClass}">${stars} Ур. ${lvl}</div>
|
||
</div>
|
||
<div class="upgrade-desc">
|
||
<strong>Текущий эффект:</strong> ${curTier.desc}
|
||
</div>
|
||
${nextTier ? `
|
||
<div class="upgrade-stats-next">
|
||
<strong>След. ранг:</strong> ${nextTier.title} — ${nextTier.desc}
|
||
</div>
|
||
<div class="upgrade-cost-row">
|
||
<div class="cost-items">${costHtml}</div>
|
||
<button class="btn-upgrade-action" ${canAfford ? '' : 'disabled'} data-upgrade-plant="${id}">
|
||
Улучшить
|
||
</button>
|
||
</div>
|
||
` : `
|
||
<div class="upgrade-stats-next" style="border-left-color: #ffd700; color: #ffe082;">
|
||
Максимальный 3-й уровень разблокирован!
|
||
</div>
|
||
`}
|
||
`;
|
||
|
||
plantsContainer.appendChild(card);
|
||
}
|
||
|
||
// Bind upgrade buttons for plants
|
||
plantsContainer.querySelectorAll('[data-upgrade-plant]').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
const plantId = e.currentTarget.getAttribute('data-upgrade-plant');
|
||
if (this.workshop.upgradePlant(plantId)) {
|
||
this.saveUserData();
|
||
this.updateWorkshopUI();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
|
||
// 3. Perks Upgrades List
|
||
const perksContainer = document.getElementById('perks-upgrade-list');
|
||
if (perksContainer) {
|
||
perksContainer.innerHTML = '';
|
||
const perkDefs = this.workshop.perkDefs;
|
||
for (const [id, def] of Object.entries(perkDefs)) {
|
||
const lvl = this.workshop.getPerkLevel(id);
|
||
const curTier = lvl > 0 ? def.tiers[lvl] : null;
|
||
const nextTier = lvl < 3 ? def.tiers[lvl + 1] : null;
|
||
|
||
const card = document.createElement('div');
|
||
card.className = 'upgrade-card';
|
||
|
||
let costHtml = '';
|
||
let canAfford = false;
|
||
if (nextTier) {
|
||
canAfford = this.workshop.canAffordCost(nextTier.cost);
|
||
const parts = [];
|
||
if (nextTier.cost.coins) {
|
||
const hasCoins = this.economy && this.economy.totalCoins >= nextTier.cost.coins;
|
||
parts.push(`<span class="cost-badge ${hasCoins ? '' : 'insufficient'}">💰 ${nextTier.cost.coins}</span>`);
|
||
}
|
||
if (nextTier.cost.bioHumus) {
|
||
const hasBio = this.workshop.materials.bioHumus >= nextTier.cost.bioHumus;
|
||
parts.push(`<span class="cost-badge ${hasBio ? '' : 'insufficient'}">🧪 ${nextTier.cost.bioHumus}</span>`);
|
||
}
|
||
if (nextTier.cost.scrapMetal) {
|
||
const hasScrap = this.workshop.materials.scrapMetal >= nextTier.cost.scrapMetal;
|
||
parts.push(`<span class="cost-badge ${hasScrap ? '' : 'insufficient'}">⚙️ ${nextTier.cost.scrapMetal}</span>`);
|
||
}
|
||
if (nextTier.cost.sunEssence) {
|
||
const hasSun = this.workshop.materials.sunEssence >= nextTier.cost.sunEssence;
|
||
parts.push(`<span class="cost-badge ${hasSun ? '' : 'insufficient'}">☀️ ${nextTier.cost.sunEssence}</span>`);
|
||
}
|
||
if (nextTier.cost.crystals) {
|
||
const hasCryst = this.workshop.materials.crystals >= nextTier.cost.crystals;
|
||
parts.push(`<span class="cost-badge ${hasCryst ? '' : 'insufficient'}">💎 ${nextTier.cost.crystals}</span>`);
|
||
}
|
||
costHtml = parts.join(' ');
|
||
}
|
||
|
||
const stars = lvl === 3 ? '★★★' : (lvl === 2 ? '★★☆' : (lvl === 1 ? '★☆☆' : '☆☆☆'));
|
||
const badgeClass = lvl === 3 ? 'upgrade-level-badge maxed' : 'upgrade-level-badge';
|
||
|
||
card.innerHTML = `
|
||
<div class="upgrade-card-header">
|
||
<div class="upgrade-title"><span>${def.icon}</span> <span>${def.name}</span></div>
|
||
<div class="${badgeClass}">${stars} Ур. ${lvl}</div>
|
||
</div>
|
||
<div class="upgrade-desc">
|
||
${def.desc}. ${curTier ? `<strong>(Сейчас: ${curTier.desc})</strong>` : '<em>(Не изучен)</em>'}
|
||
</div>
|
||
${nextTier ? `
|
||
<div class="upgrade-stats-next">
|
||
<strong>След. уровень:</strong> ${nextTier.desc}
|
||
</div>
|
||
<div class="upgrade-cost-row">
|
||
<div class="cost-items">${costHtml}</div>
|
||
<button class="btn-upgrade-action" ${canAfford ? '' : 'disabled'} data-upgrade-perk="${id}">
|
||
Изучить
|
||
</button>
|
||
</div>
|
||
` : `
|
||
<div class="upgrade-stats-next" style="border-left-color: #ffd700; color: #ffe082;">
|
||
Перк полностью освоен!
|
||
</div>
|
||
`}
|
||
`;
|
||
|
||
perksContainer.appendChild(card);
|
||
}
|
||
|
||
// Bind upgrade buttons for perks
|
||
perksContainer.querySelectorAll('[data-upgrade-perk]').forEach(btn => {
|
||
btn.addEventListener('click', (e) => {
|
||
const perkId = e.currentTarget.getAttribute('data-upgrade-perk');
|
||
if (this.workshop.upgradePerk(perkId)) {
|
||
this.saveUserData();
|
||
this.updateWorkshopUI();
|
||
}
|
||
});
|
||
});
|
||
}
|
||
}
|
||
|
||
initZenGarden() {
|
||
if (!this.zengarden) return;
|
||
|
||
this.zenTool = 'water'; // 'water', 'fertilizer', 'music'
|
||
const zenCanvas = document.getElementById('zenCanvas');
|
||
if (!zenCanvas) return;
|
||
const zenCtx = zenCanvas.getContext('2d');
|
||
|
||
// Tool selectors
|
||
const btnWater = document.getElementById('zen-tool-water');
|
||
const btnFert = document.getElementById('zen-tool-fert');
|
||
const btnMusic = document.getElementById('zen-tool-music');
|
||
const btnFeedSnail = document.getElementById('zen-feed-snail');
|
||
const btnPlantSprout = document.getElementById('zen-plant-sprout');
|
||
|
||
const updateToolBtns = () => {
|
||
[btnWater, btnFert, btnMusic].forEach(b => { if (b) b.classList.remove('active'); });
|
||
if (this.zenTool === 'water' && btnWater) btnWater.classList.add('active');
|
||
if (this.zenTool === 'fertilizer' && btnFert) btnFert.classList.add('active');
|
||
if (this.zenTool === 'music' && btnMusic) btnMusic.classList.add('active');
|
||
|
||
const fertCount = document.getElementById('zen-fert-count');
|
||
const chocCount = document.getElementById('zen-choc-count');
|
||
if (fertCount) fertCount.textContent = this.zengarden.fertilizerCount;
|
||
if (chocCount) chocCount.textContent = this.zengarden.chocolateCount;
|
||
};
|
||
|
||
if (btnWater) btnWater.addEventListener('click', () => { this.zenTool = 'water'; updateToolBtns(); });
|
||
if (btnFert) btnFert.addEventListener('click', () => { this.zenTool = 'fertilizer'; updateToolBtns(); });
|
||
if (btnMusic) btnMusic.addEventListener('click', () => { this.zenTool = 'music'; updateToolBtns(); });
|
||
|
||
if (btnFeedSnail) {
|
||
btnFeedSnail.addEventListener('click', () => {
|
||
if (this.zengarden.feedSnail()) {
|
||
this.saveUserData();
|
||
updateToolBtns();
|
||
}
|
||
});
|
||
}
|
||
|
||
if (btnPlantSprout) {
|
||
btnPlantSprout.addEventListener('click', () => {
|
||
if (this.economy && this.economy.totalCoins >= 100) {
|
||
const types = ['sunflower', 'peashooter', 'wallnut', 'snowpea', 'repeater', 'chomper'];
|
||
const picked = types[Math.floor(Math.random() * types.length)];
|
||
if (this.zengarden.plantSprout(picked)) {
|
||
this.economy.addCoins(-100);
|
||
if (window.PvZAudio) window.PvZAudio.playWin();
|
||
this.saveUserData();
|
||
}
|
||
}
|
||
});
|
||
}
|
||
|
||
// Canvas pot click
|
||
zenCanvas.addEventListener('click', (e) => {
|
||
const rect = zenCanvas.getBoundingClientRect();
|
||
const scaleX = zenCanvas.width / rect.width;
|
||
const scaleY = zenCanvas.height / rect.height;
|
||
const mx = (e.clientX - rect.left) * scaleX;
|
||
const my = (e.clientY - rect.top) * scaleY;
|
||
|
||
// Check coins click
|
||
for (const c of this.zengarden.gardenCoins) {
|
||
if (!c.collected && Math.hypot(mx - c.x, my - c.y) < 25) {
|
||
this.zengarden.collectCoin(c);
|
||
return;
|
||
}
|
||
}
|
||
|
||
// Check pots click (col: 0..3, row: 0..1)
|
||
for (let i = 0; i < 8; i++) {
|
||
const col = i % 4;
|
||
const row = Math.floor(i / 4);
|
||
const px = 120 + col * 150;
|
||
const py = row === 0 ? 150 : 275;
|
||
|
||
if (Math.abs(mx - px) < 45 && my >= py - 50 && my <= py + 30) {
|
||
if (this.zenTool === 'water') {
|
||
this.zengarden.waterPlant(i);
|
||
} else if (this.zenTool === 'fertilizer') {
|
||
this.zengarden.fertilizePlant(i);
|
||
} else if (this.zenTool === 'music') {
|
||
this.zengarden.playMusicToPlant(i);
|
||
}
|
||
this.saveUserData();
|
||
updateToolBtns();
|
||
return;
|
||
}
|
||
}
|
||
});
|
||
|
||
updateToolBtns();
|
||
|
||
// Zen Garden Animation Loop
|
||
let lastZenTime = performance.now();
|
||
const zenLoop = (now) => {
|
||
const modal = document.getElementById('zengarden-modal');
|
||
if (modal && !modal.classList.contains('hidden')) {
|
||
const dt = Math.min((now - lastZenTime) / 1000, 0.1);
|
||
this.zengarden.update(dt);
|
||
this.zengarden.draw(zenCtx);
|
||
}
|
||
lastZenTime = now;
|
||
requestAnimationFrame(zenLoop);
|
||
};
|
||
requestAnimationFrame(zenLoop);
|
||
}
|
||
|
||
updateZenGardenUI() {
|
||
if (!this.zengarden) return;
|
||
|
||
const fertCount = document.getElementById('zen-fert-count');
|
||
const chocCount = document.getElementById('zen-choc-count');
|
||
if (fertCount) fertCount.textContent = this.zengarden.fertilizerCount;
|
||
if (chocCount) chocCount.textContent = this.zengarden.chocolateCount;
|
||
|
||
const alertEl = document.getElementById('zen-offline-alert');
|
||
if (alertEl) {
|
||
if (this.zengarden.offlineEarned > 0) {
|
||
alertEl.textContent = `💰 Пассивный доход за время отсутствия: +${this.zengarden.offlineEarned} монет!`;
|
||
} else {
|
||
alertEl.textContent = `💰 Взрослые растения приносят монеты каждые несколько секунд.`;
|
||
}
|
||
}
|
||
}
|
||
|
||
initAlmanac() {
|
||
if (!this.almanac) return;
|
||
|
||
const tabPlants = document.getElementById('tab-almanac-plants');
|
||
const tabZombies = document.getElementById('tab-almanac-zombies');
|
||
|
||
if (tabPlants) {
|
||
tabPlants.addEventListener('click', () => {
|
||
this.almanac.selectedCategory = 'plants';
|
||
this.almanac.selectedId = 'peashooter';
|
||
tabPlants.classList.add('active');
|
||
if (tabZombies) tabZombies.classList.remove('active');
|
||
this.renderAlmanacUI();
|
||
});
|
||
}
|
||
|
||
if (tabZombies) {
|
||
tabZombies.addEventListener('click', () => {
|
||
this.almanac.selectedCategory = 'zombies';
|
||
this.almanac.selectedId = 'z_normal';
|
||
tabZombies.classList.add('active');
|
||
if (tabPlants) tabPlants.classList.remove('active');
|
||
this.renderAlmanacUI();
|
||
});
|
||
}
|
||
}
|
||
|
||
renderAlmanacUI() {
|
||
if (!this.almanac) return;
|
||
|
||
const listContainer = document.getElementById('almanac-list');
|
||
const detailsContainer = document.getElementById('almanac-details');
|
||
if (!listContainer || !detailsContainer) return;
|
||
|
||
const items = this.almanac.selectedCategory === 'plants' ? this.almanac.plants : this.almanac.zombies;
|
||
listContainer.innerHTML = '';
|
||
|
||
// Render grid items
|
||
for (const item of items) {
|
||
const slot = document.createElement('div');
|
||
const isSelected = item.id === this.almanac.selectedId;
|
||
slot.className = isSelected ? 'almanac-slot selected' : 'almanac-slot';
|
||
slot.innerHTML = `
|
||
<div class="almanac-slot-icon">${item.icon}</div>
|
||
<div class="almanac-slot-name">${item.name}</div>
|
||
`;
|
||
|
||
slot.addEventListener('click', () => {
|
||
this.almanac.selectedId = item.id;
|
||
this.renderAlmanacUI();
|
||
if (window.PvZAudio) window.PvZAudio.playCoin();
|
||
});
|
||
|
||
listContainer.appendChild(slot);
|
||
}
|
||
|
||
// Render detailed view for selected item
|
||
const selectedItem = this.almanac.getItem(this.almanac.selectedId) || items[0];
|
||
if (selectedItem) {
|
||
let statsHtml = '';
|
||
if (this.almanac.selectedCategory === 'plants') {
|
||
statsHtml = `
|
||
<div class="almanac-stat-row"><span>Стоимость:</span> <strong>☀️ ${selectedItem.cost}</strong></div>
|
||
<div class="almanac-stat-row"><span>Перезарядка:</span> <strong>${selectedItem.cooldown}</strong></div>
|
||
<div class="almanac-stat-row"><span>Урон:</span> <strong>${selectedItem.damage}</strong></div>
|
||
<div class="almanac-stat-row"><span>Прочность:</span> <strong>${selectedItem.toughness}</strong></div>
|
||
`;
|
||
} else {
|
||
statsHtml = `
|
||
<div class="almanac-stat-row"><span>Скорость:</span> <strong>${selectedItem.speed}</strong></div>
|
||
<div class="almanac-stat-row"><span>Прочность:</span> <strong>${selectedItem.toughness}</strong></div>
|
||
`;
|
||
}
|
||
|
||
detailsContainer.innerHTML = `
|
||
<div class="almanac-detail-title">
|
||
<div class="almanac-detail-icon">${selectedItem.icon}</div>
|
||
<div class="almanac-detail-name">${selectedItem.name}</div>
|
||
</div>
|
||
<div class="almanac-stats-box" style="display: flex; flex-direction: column; gap: 4px;">
|
||
${statsHtml}
|
||
</div>
|
||
<div class="almanac-desc-text">
|
||
${selectedItem.desc}
|
||
</div>
|
||
<div class="almanac-lore-quote">
|
||
${selectedItem.lore}
|
||
</div>
|
||
`;
|
||
}
|
||
}
|
||
|
||
initFeedbackUI() {
|
||
// Bug Filter Tabs
|
||
const bugTabs = [
|
||
{ id: 'tab-bug-all', filter: 'all' },
|
||
{ id: 'tab-bug-bugs', filter: 'bug' },
|
||
{ id: 'tab-bug-ideas', filter: 'feature' },
|
||
{ id: 'tab-bug-visual', filter: 'visual' },
|
||
{ id: 'tab-bug-fixed', filter: 'resolved' }
|
||
];
|
||
bugTabs.forEach(t => {
|
||
const el = document.getElementById(t.id);
|
||
if (el) {
|
||
el.addEventListener('click', () => {
|
||
bugTabs.forEach(other => {
|
||
const oEl = document.getElementById(other.id);
|
||
if (oEl) oEl.classList.remove('active');
|
||
});
|
||
el.classList.add('active');
|
||
if (window.PvZFeedback) {
|
||
window.PvZFeedback.setFilter(t.filter);
|
||
}
|
||
});
|
||
}
|
||
});
|
||
|
||
// Bug Form Submit
|
||
const btnBugSubmit = document.getElementById('btn-bug-submit');
|
||
if (btnBugSubmit) {
|
||
btnBugSubmit.addEventListener('click', () => {
|
||
const nameInput = document.getElementById('bug-input-name');
|
||
const catInput = document.getElementById('bug-input-category');
|
||
const textInput = document.getElementById('bug-input-text');
|
||
const statusEl = document.getElementById('bug-form-status');
|
||
|
||
const author = nameInput ? nameInput.value.trim() : '';
|
||
const cat = catInput ? catInput.value : 'bug';
|
||
const text = textInput ? textInput.value.trim() : '';
|
||
|
||
if (!text) {
|
||
if (statusEl) {
|
||
statusEl.textContent = 'Пожалуйста, опишите проблему перед отправкой!';
|
||
statusEl.className = 'bug-status-msg error';
|
||
statusEl.classList.remove('hidden');
|
||
}
|
||
if (textInput) textInput.focus();
|
||
return;
|
||
}
|
||
|
||
btnBugSubmit.disabled = true;
|
||
btnBugSubmit.textContent = 'Отправка... ⏳';
|
||
|
||
if (window.PvZFeedback) {
|
||
window.PvZFeedback.sendReport(author, cat, text, (res) => {
|
||
btnBugSubmit.disabled = false;
|
||
btnBugSubmit.textContent = 'Отправить 🚀';
|
||
if (res && res.success) {
|
||
if (textInput) textInput.value = '';
|
||
if (statusEl) {
|
||
statusEl.textContent = res.offline
|
||
? 'Сохранено локально! Сообщение появится в чате.'
|
||
: 'Спасибо! Баг-репорт успешно сохранен на сервере ✅';
|
||
statusEl.className = 'bug-status-msg success';
|
||
statusEl.classList.remove('hidden');
|
||
setTimeout(() => {
|
||
if (statusEl) statusEl.classList.add('hidden');
|
||
}, 4000);
|
||
}
|
||
} else {
|
||
if (statusEl) {
|
||
statusEl.textContent = (res && res.message) || 'Ошибка отправки репорта';
|
||
statusEl.className = 'bug-status-msg error';
|
||
statusEl.classList.remove('hidden');
|
||
}
|
||
}
|
||
});
|
||
}
|
||
});
|
||
}
|
||
}
|
||
|
||
renderAchievementsUI() {
|
||
if (!this.achievements) return;
|
||
const listEl = document.getElementById('achievements-list');
|
||
if (!listEl) return;
|
||
|
||
listEl.innerHTML = '';
|
||
for (const ach of this.achievements.achievements) {
|
||
const card = document.createElement('div');
|
||
const isDone = ach.completed;
|
||
const isClaimed = ach.claimed;
|
||
card.className = `achievement-item ${isDone ? 'completed' : ''} ${isClaimed ? 'claimed' : ''}`;
|
||
|
||
const pct = Math.min(100, Math.round((ach.progress / ach.max) * 100));
|
||
|
||
let rewardText = `💰 +${ach.reward.coins}`;
|
||
if (ach.reward.crystals) {
|
||
rewardText += ` • 💎 +${ach.reward.crystals}`;
|
||
}
|
||
|
||
let btnHtml = '';
|
||
if (isClaimed) {
|
||
btnHtml = `<button class="btn btn-secondary" style="font-size: 11px; padding: 4px 10px; cursor: default; opacity: 0.6;" disabled>Получено ✓</button>`;
|
||
} else if (isDone) {
|
||
btnHtml = `<button class="btn btn-claim-reward" data-ach="${ach.id}" style="font-size: 11px; padding: 4px 10px; background: #ffd54f; color: #1b5e20; font-weight: bold;">Забрать 🎁</button>`;
|
||
} else {
|
||
btnHtml = `<button class="btn btn-secondary" style="font-size: 11px; padding: 4px 8px; opacity: 0.5;" disabled>${ach.progress} / ${ach.max}</button>`;
|
||
}
|
||
|
||
card.innerHTML = `
|
||
<div class="ach-icon">${ach.icon}</div>
|
||
<div class="ach-info">
|
||
<div class="ach-title">${ach.title}</div>
|
||
<div class="ach-desc">${ach.desc}</div>
|
||
<div class="ach-progress-bar">
|
||
<div class="ach-progress-fill" style="width: ${pct}%;"></div>
|
||
</div>
|
||
<div class="ach-reward-tag">Награда: ${rewardText}</div>
|
||
</div>
|
||
<div class="ach-action">
|
||
${btnHtml}
|
||
</div>
|
||
`;
|
||
|
||
const claimBtn = card.querySelector('.btn-claim-reward');
|
||
if (claimBtn) {
|
||
claimBtn.addEventListener('click', () => {
|
||
this.achievements.claimReward(ach.id);
|
||
this.renderAchievementsUI();
|
||
this.updateRecordDisplay();
|
||
this.updateShopUI();
|
||
});
|
||
}
|
||
|
||
listEl.appendChild(card);
|
||
}
|
||
}
|
||
|
||
renderTreeUI() {
|
||
if (!this.tree) return;
|
||
|
||
const heightEl = document.getElementById('tree-height-display');
|
||
const artEl = document.getElementById('tree-art');
|
||
const titleEl = document.getElementById('tree-wisdom-title');
|
||
const quoteEl = document.getElementById('tree-wisdom-quote');
|
||
const cheatsListEl = document.getElementById('tree-cheats-list');
|
||
const feedBtn = document.getElementById('btn-tree-feed');
|
||
|
||
const height = this.tree.height;
|
||
if (heightEl) heightEl.textContent = `Высота: ${height} футов | Древесное удобрение: 150 💰`;
|
||
|
||
if (artEl) {
|
||
if (height >= 100) artEl.textContent = '🌲✨';
|
||
else if (height >= 50) artEl.textContent = '🌳';
|
||
else if (height >= 20) artEl.textContent = '🪴';
|
||
else if (height >= 5) artEl.textContent = '🌿';
|
||
else artEl.textContent = '🌱';
|
||
}
|
||
|
||
const wisdom = this.tree.getCurrentWisdom();
|
||
if (titleEl) titleEl.textContent = wisdom.title || 'Первый росток';
|
||
if (quoteEl) quoteEl.textContent = wisdom.text || '';
|
||
|
||
if (feedBtn) {
|
||
const canAfford = this.economy && this.economy.totalCoins >= this.tree.fertilizerCost;
|
||
feedBtn.disabled = !canAfford;
|
||
feedBtn.textContent = `Подкормить Древо (${this.tree.fertilizerCost} 💰)`;
|
||
}
|
||
|
||
if (cheatsListEl) {
|
||
cheatsListEl.innerHTML = '';
|
||
const cheatMeta = [
|
||
{ id: 'mustache', name: 'Усы (Mustache)', reqHeight: 10, icon: '👨🏻' },
|
||
{ id: 'future', name: 'Киберочки (Future)', reqHeight: 30, icon: '🕶️' },
|
||
{ id: 'daisies', name: 'Цветы (Daisies)', reqHeight: 50, icon: '🌼' },
|
||
{ id: 'dance', name: 'Танцы (Dance)', reqHeight: 100, icon: '🕺' },
|
||
{ id: 'pinata', name: 'Конфетти (Pinata)', reqHeight: 100, icon: '🎉' }
|
||
];
|
||
|
||
cheatMeta.forEach(c => {
|
||
const isUnlocked = height >= c.reqHeight;
|
||
const isActive = !!this.tree.cheats[c.id];
|
||
|
||
const item = document.createElement('div');
|
||
item.className = `tree-cheat-item ${isUnlocked ? (isActive ? 'active' : '') : 'locked'}`;
|
||
|
||
item.innerHTML = `
|
||
<div class="tree-cheat-info">
|
||
<span style="font-size: 18px; margin-right: 6px;">${c.icon}</span>
|
||
<strong>${c.name}</strong>
|
||
<div style="font-size: 11px; opacity: 0.8;">
|
||
${isUnlocked ? (isActive ? 'Статус: ВКЛЮЧЕН ✅' : 'Статус: Выключен') : `Требуется: ${c.reqHeight} футов 🔒`}
|
||
</div>
|
||
</div>
|
||
<button class="btn" style="font-size: 11px; padding: 4px 10px; ${isActive ? 'background: #e91e63;' : ''}" ${isUnlocked ? '' : 'disabled'}>
|
||
${isUnlocked ? (isActive ? 'Выкл' : 'Вкл') : '🔒'}
|
||
</button>
|
||
`;
|
||
|
||
const toggleBtn = item.querySelector('button');
|
||
if (toggleBtn && isUnlocked) {
|
||
toggleBtn.addEventListener('click', () => {
|
||
this.tree.toggleCheat(c.id);
|
||
this.renderTreeUI();
|
||
});
|
||
}
|
||
|
||
cheatsListEl.appendChild(item);
|
||
});
|
||
}
|
||
}
|
||
|
||
renderLeaderboardUI(mode = 'survival') {
|
||
if (!this.leaderboards) return;
|
||
this.leaderboards.selectedMode = mode;
|
||
|
||
const contentEl = document.getElementById('leaderboard-content');
|
||
if (!contentEl) return;
|
||
|
||
const entries = this.leaderboards.getEntries(mode);
|
||
|
||
if (!entries || entries.length === 0) {
|
||
contentEl.innerHTML = `<div style="text-align: center; color: #ffe082; padding: 24px; font-size: 14px;">Пока нет записей. Сыграйте в этот режим, чтобы установить первый рекорд!</div>`;
|
||
return;
|
||
}
|
||
|
||
let rowsHtml = '';
|
||
entries.forEach((item, index) => {
|
||
let medal = `#${index + 1}`;
|
||
if (index === 0) medal = '🥇 1';
|
||
else if (index === 1) medal = '🥈 2';
|
||
else if (index === 2) medal = '🥉 3';
|
||
|
||
rowsHtml += `
|
||
<tr class="${index < 3 ? 'top-rank' : ''}">
|
||
<td style="font-weight: bold; width: 60px; text-align: center;">${medal}</td>
|
||
<td style="font-weight: bold; color: #fff;">${item.name}</td>
|
||
<td style="color: #ffd54f; font-weight: bold;">${item.detail || item.score}</td>
|
||
<td style="color: #b0bec5; font-size: 11px; text-align: right;">${item.date || ''}</td>
|
||
</tr>
|
||
`;
|
||
});
|
||
|
||
contentEl.innerHTML = `
|
||
<table class="leaderboard-table">
|
||
<thead>
|
||
<tr>
|
||
<th style="width: 60px; text-align: center;">Место</th>
|
||
<th>Игрок</th>
|
||
<th>Результат</th>
|
||
<th style="text-align: right;">Дата</th>
|
||
</tr>
|
||
</thead>
|
||
<tbody>
|
||
${rowsHtml}
|
||
</tbody>
|
||
</table>
|
||
`;
|
||
}
|
||
|
||
showDraftModal() {
|
||
if (!this.roguelike) return;
|
||
|
||
const modal = document.getElementById('draft-modal');
|
||
const listEl = document.getElementById('draft-choices-list');
|
||
if (!modal || !listEl) return;
|
||
|
||
const choices = this.roguelike.generateDraftChoices();
|
||
listEl.innerHTML = '';
|
||
|
||
choices.forEach((choice, index) => {
|
||
const card = document.createElement('div');
|
||
card.className = `draft-card-item ${choice.type === 'relic' ? 'relic-choice' : 'seed-choice'}`;
|
||
card.innerHTML = `
|
||
<div class="draft-choice-type">${choice.type === 'relic' ? '✨ РЕЛИКВИЯ' : '🌱 СЕМЯ РАСТЕНИЯ'}</div>
|
||
<div class="draft-choice-icon">${choice.icon}</div>
|
||
<div class="draft-choice-title">${choice.name}</div>
|
||
<div class="draft-choice-desc">${choice.desc}</div>
|
||
<button class="btn draft-choice-btn" style="margin-top: 10px; width: 100%;">Выбрать</button>
|
||
`;
|
||
|
||
const pickBtn = card.querySelector('.draft-choice-btn');
|
||
if (pickBtn) {
|
||
pickBtn.addEventListener('click', () => {
|
||
this.roguelike.selectChoice(index);
|
||
modal.classList.add('hidden');
|
||
if (window.PvZAudio) window.PvZAudio.playCoin();
|
||
this.startLevel(this.roguelike.currentFloor, 'roguelike');
|
||
});
|
||
}
|
||
|
||
listEl.appendChild(card);
|
||
});
|
||
|
||
modal.classList.remove('hidden');
|
||
}
|
||
|
||
createPlant(type, col, row, bounds) {
|
||
const level = this.workshop ? this.workshop.getPlantLevel(type) : 1;
|
||
const plant = new window.Plant(type, col, row, bounds.x, bounds.y, bounds.w, bounds.h, level);
|
||
if (this.workshop) {
|
||
const hardyLvl = this.workshop.getPerkLevel('hardyRoots');
|
||
if (hardyLvl > 0) {
|
||
const mult = 1 + (hardyLvl === 1 ? 0.20 : (hardyLvl === 2 ? 0.35 : 0.50));
|
||
plant.maxHealth = Math.round(plant.maxHealth * mult);
|
||
plant.health = plant.maxHealth;
|
||
}
|
||
}
|
||
// Roguelike: relic_iron_bark (+350 HP to Wallnut)
|
||
if (this.roguelike && this.currentMode === 'roguelike' && this.roguelike.hasRelic('relic_iron_bark') && type === 'wallnut') {
|
||
plant.maxHealth += 350;
|
||
plant.health = plant.maxHealth;
|
||
}
|
||
// Roguelike: relic_fertilizer_boost (20% faster shootInterval)
|
||
if (this.roguelike && this.currentMode === 'roguelike' && this.roguelike.hasRelic('relic_fertilizer_boost')) {
|
||
if (plant.shootInterval) {
|
||
plant.shootInterval *= 0.8;
|
||
}
|
||
}
|
||
// Achievement: first_plant
|
||
if (this.achievements) {
|
||
this.achievements.trigger('first_plant');
|
||
}
|
||
return plant;
|
||
}
|
||
|
||
handleCellClick(col, row) {
|
||
// 0. I, Zombie Mechanic: Click row to deploy zombie!
|
||
if (this.currentMode === 'izombie') {
|
||
const selected = this.economy.selectedCard;
|
||
if (!selected) return;
|
||
|
||
const card = this.economy.cards.find(c => c.id === selected);
|
||
if (card && this.economy.spendSun(card.cost) && card.timer <= 0) {
|
||
this.economy.startCooldown(card.id);
|
||
const zType = selected.replace('z_', '');
|
||
this.spawnZombie(row, zType);
|
||
if (window.PvZAudio) window.PvZAudio.playPlant();
|
||
this.economy.selectedCard = null;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 1. Wall-nut Bowling Mechanic
|
||
if (this.currentMode === 'bowling') {
|
||
const selected = this.economy.selectedCard;
|
||
if (!selected) return;
|
||
|
||
const card = this.economy.cards.find(c => c.id === selected);
|
||
if (card && card.timer <= 0) {
|
||
this.economy.startCooldown(card.id);
|
||
const bounds = this.grid.getCellBounds(0, row);
|
||
|
||
if (selected === 'bowling_cherry') {
|
||
// Exploding bowling cherry
|
||
this.explodeArea(col, row, 3);
|
||
if (window.PvZAudio) window.PvZAudio.playExplosion();
|
||
} else {
|
||
// Rolling bowling nut
|
||
const ball = this.spawnProjectile(130, bounds.centerY, row, 'bowling', 0);
|
||
if (window.PvZAudio) window.PvZAudio.playPlant();
|
||
}
|
||
this.economy.selectedCard = null;
|
||
}
|
||
return;
|
||
}
|
||
|
||
// 2. Standard Planting Mechanic
|
||
const selected = this.economy.selectedCard;
|
||
if (!selected) return;
|
||
|
||
const existingPlant = this.grid.getPlant(col, row);
|
||
const isWater = this.grid.isWaterCell(row);
|
||
const hasGrave = this.grid.hasGrave(col, row);
|
||
|
||
// Shovel
|
||
if (selected === 'shovel') {
|
||
if (existingPlant) {
|
||
this.grid.removePlant(col, row);
|
||
existingPlant.toRemove = true;
|
||
this.economy.selectedCard = null;
|
||
if (window.PvZAudio) window.PvZAudio.playPlant();
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Gravebuster
|
||
if (selected === 'gravebuster') {
|
||
if (hasGrave && !existingPlant) {
|
||
const card = this.economy.cards.find(c => c.id === selected);
|
||
if (card && this.economy.spendSun(card.cost)) {
|
||
this.economy.startCooldown(card.id);
|
||
const bounds = this.grid.getCellBounds(col, row);
|
||
const plant = this.createPlant(card.id, col, row, bounds);
|
||
this.grid.setPlant(col, row, plant);
|
||
this.plants.push(plant);
|
||
this.economy.selectedCard = null;
|
||
if (window.PvZAudio) window.PvZAudio.playPlant();
|
||
}
|
||
}
|
||
return;
|
||
}
|
||
|
||
// Cannot plant on Gravestone with anything else
|
||
if (hasGrave) return;
|
||
|
||
// Water lane checks (Pool biome)
|
||
if (isWater) {
|
||
if (selected === 'lilypad' && !existingPlant) {
|
||
// Plant Lilypad
|
||
const card = this.economy.cards.find(c => c.id === selected);
|
||
if (card && this.economy.spendSun(card.cost)) {
|
||
this.economy.startCooldown(card.id);
|
||
const bounds = this.grid.getCellBounds(col, row);
|
||
const plant = this.createPlant('lilypad', col, row, bounds);
|
||
this.grid.setPlant(col, row, plant);
|
||
this.plants.push(plant);
|
||
this.economy.selectedCard = null;
|
||
if (window.PvZAudio) window.PvZAudio.playPlant();
|
||
}
|
||
return;
|
||
} else if (existingPlant && existingPlant.type === 'lilypad') {
|
||
// Plant normal plant on top of Lilypad!
|
||
const card = this.economy.cards.find(c => c.id === selected);
|
||
if (card && selected !== 'lilypad' && this.economy.spendSun(card.cost)) {
|
||
this.economy.startCooldown(card.id);
|
||
const bounds = this.grid.getCellBounds(col, row);
|
||
const plant = this.createPlant(card.id, col, row, bounds);
|
||
this.grid.setPlant(col, row, plant);
|
||
this.plants.push(plant);
|
||
this.economy.selectedCard = null;
|
||
if (window.PvZAudio) window.PvZAudio.playPlant();
|
||
}
|
||
return;
|
||
} else {
|
||
return; // Water requires lilypad first
|
||
}
|
||
}
|
||
|
||
// Roof checks (Roof biome requires flowerpot)
|
||
if (this.grid.isRoof()) {
|
||
if (selected === 'flowerpot' && !existingPlant) {
|
||
const card = this.economy.cards.find(c => c.id === selected);
|
||
if (card && this.economy.canAfford(card.cost) && card.timer <= 0) {
|
||
if (this.economy.spendSun(card.cost)) {
|
||
this.economy.startCooldown(card.id);
|
||
const bounds = this.grid.getCellBounds(col, row);
|
||
const plant = this.createPlant('flowerpot', col, row, bounds);
|
||
this.grid.setPlant(col, row, plant);
|
||
this.plants.push(plant);
|
||
this.economy.selectedCard = null;
|
||
if (window.PvZAudio) window.PvZAudio.playPlant();
|
||
}
|
||
}
|
||
return;
|
||
} else if (existingPlant && existingPlant.type === 'flowerpot') {
|
||
const card = this.economy.cards.find(c => c.id === selected);
|
||
if (card && selected !== 'flowerpot' && this.economy.canAfford(card.cost) && card.timer <= 0) {
|
||
if (this.economy.spendSun(card.cost)) {
|
||
this.economy.startCooldown(card.id);
|
||
const bounds = this.grid.getCellBounds(col, row);
|
||
const plant = this.createPlant(card.id, col, row, bounds);
|
||
this.grid.setPlant(col, row, plant);
|
||
this.plants.push(plant);
|
||
this.economy.selectedCard = null;
|
||
if (window.PvZAudio) window.PvZAudio.playPlant();
|
||
}
|
||
}
|
||
return;
|
||
} else {
|
||
return; // Roof requires flowerpot first
|
||
}
|
||
}
|
||
|
||
// Normal Grass Planting
|
||
if (!existingPlant) {
|
||
const card = this.economy.cards.find(c => c.id === selected);
|
||
if (card && this.economy.canAfford(card.cost) && card.timer <= 0) {
|
||
if (this.economy.spendSun(card.cost)) {
|
||
this.economy.startCooldown(card.id);
|
||
const bounds = this.grid.getCellBounds(col, row);
|
||
const plant = this.createPlant(card.id, col, row, bounds);
|
||
this.grid.setPlant(col, row, plant);
|
||
this.plants.push(plant);
|
||
this.economy.selectedCard = null;
|
||
if (window.PvZAudio) window.PvZAudio.playPlant();
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
loop(timestamp) {
|
||
const rawDt = Math.min((timestamp - this.lastTime) / 1000, 0.1);
|
||
this.lastTime = timestamp;
|
||
const dt = rawDt * this.gameSpeed;
|
||
|
||
if (this.isRunning && !this.isPaused) {
|
||
this.update(dt);
|
||
}
|
||
this.render();
|
||
|
||
requestAnimationFrame(this.loop.bind(this));
|
||
}
|
||
|
||
update(dt) {
|
||
// 1. Grid & Lawnmowers
|
||
this.grid.updateLawnmowers(dt, this.zombies);
|
||
|
||
// 2. Economy & Workshop Loot & Zen Garden
|
||
this.economy.update(dt);
|
||
if (this.workshop) {
|
||
this.workshop.update(dt, this);
|
||
}
|
||
if (this.zengarden) {
|
||
this.zengarden.update(dt * 0.2);
|
||
}
|
||
|
||
// 3. Waves & Spawns
|
||
this.wave.update(dt, this);
|
||
|
||
// 4. Garden Rake Check
|
||
if (this.hasRakeArmed) {
|
||
const rakeBounds = this.grid.getCellBounds(this.rakeCol, this.rakeRow);
|
||
for (const z of this.zombies) {
|
||
if (z.row === this.rakeRow && Math.abs(z.x - rakeBounds.centerX) < 30 && z.health > 0) {
|
||
this.hasRakeArmed = false;
|
||
z.takeDamage(1000);
|
||
this.combat.createExplosionParticles(rakeBounds.centerX, rakeBounds.centerY);
|
||
if (window.PvZAudio) window.PvZAudio.playStrike();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 5. Plants
|
||
for (const plant of this.plants) {
|
||
plant.update(dt, this);
|
||
if (plant.toRemove) {
|
||
this.grid.removePlant(plant.col, plant.row);
|
||
if (this.currentMode === 'izombie' && plant.type === 'sunflower') {
|
||
this.economy.addSun(150);
|
||
if (window.PvZAudio) window.PvZAudio.playSun();
|
||
}
|
||
}
|
||
}
|
||
this.plants = this.plants.filter(p => !p.toRemove);
|
||
|
||
// 6. Zombies
|
||
for (const zombie of this.zombies) {
|
||
zombie.update(dt, this);
|
||
}
|
||
this.zombies = this.zombies.filter(z => !z.toRemove);
|
||
|
||
// 7. Projectiles (in-place update with pooling release, eliminating GC allocations)
|
||
let projWriteIdx = 0;
|
||
for (let i = 0; i < this.projectiles.length; i++) {
|
||
const proj = this.projectiles[i];
|
||
proj.update(dt);
|
||
if (!proj.toRemove) {
|
||
this.projectiles[projWriteIdx++] = proj;
|
||
} else {
|
||
if (this.projectilePool) this.projectilePool.release(proj);
|
||
}
|
||
}
|
||
this.projectiles.length = projWriteIdx;
|
||
|
||
// 8. Combat
|
||
this.combat.update(dt, this);
|
||
|
||
// 8.5 Roguelike Relics & Timers
|
||
if (this.roguelike && this.currentMode === 'roguelike') {
|
||
this.roguelike.update(dt, this);
|
||
}
|
||
|
||
// 8.6 Dr. Zomboss Boss Fight
|
||
if (this.boss && (this.boss.isActive || this.currentMode === 'boss' || this.currentMode === 'bossrush')) {
|
||
this.boss.update(dt, this);
|
||
}
|
||
|
||
// 9. I, Zombie Win & Defeat Check
|
||
if (this.currentMode === 'izombie' && this.izombieSetupDone && this.isRunning) {
|
||
const sunflowers = this.plants.filter(p => p.type === 'sunflower');
|
||
if (sunflowers.length === 0) {
|
||
this.triggerVictory();
|
||
} else {
|
||
const minZombieCost = 50;
|
||
const hasAliveZombies = this.zombies.some(z => z.health > 0 && !z.toRemove);
|
||
if (!hasAliveZombies && this.economy.sun < minZombieCost) {
|
||
this.triggerGameOver('Закончились солнце и зомби! Подсолнухи уцелели.');
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
render() {
|
||
this.ctx.clearRect(0, 0, this.logicalWidth, this.logicalHeight);
|
||
|
||
// 1. Grid & Biome
|
||
this.grid.draw(this.ctx, this.hoverCell, this.economy.selectedCard);
|
||
|
||
// 2. Garden Rake
|
||
if (this.hasRakeArmed) {
|
||
const rakeBounds = this.grid.getCellBounds(this.rakeCol, this.rakeRow);
|
||
this.drawRake(this.ctx, rakeBounds.centerX, rakeBounds.centerY);
|
||
}
|
||
|
||
// 3. Plants
|
||
for (const plant of this.plants) {
|
||
plant.draw(this.ctx);
|
||
}
|
||
|
||
// 4. Zombies (depth sort)
|
||
const sortedZombies = [...this.zombies].sort((a, b) => a.y - b.y);
|
||
for (const zombie of sortedZombies) {
|
||
zombie.draw(this.ctx, this);
|
||
}
|
||
|
||
// 5. Projectiles
|
||
for (const proj of this.projectiles) {
|
||
proj.draw(this.ctx);
|
||
}
|
||
|
||
// 5.5 Workshop Floating Loot
|
||
if (this.workshop) {
|
||
this.workshop.draw(this.ctx);
|
||
}
|
||
|
||
// 6. Combat Particles
|
||
this.combat.draw(this.ctx);
|
||
|
||
// 7. Top UI & Suns
|
||
this.economy.draw(this.ctx);
|
||
|
||
// 8. Wave status & Banners
|
||
this.wave.draw(this.ctx);
|
||
|
||
// 8.5 Roguelike Active Relics HUD
|
||
if (this.roguelike && this.currentMode === 'roguelike') {
|
||
this.roguelike.draw(this.ctx);
|
||
}
|
||
|
||
// 8.6 Dr. Zomboss Robot & Hazards
|
||
if (this.boss && (this.boss.isActive || this.currentMode === 'boss' || this.currentMode === 'bossrush')) {
|
||
this.boss.draw(this.ctx);
|
||
}
|
||
|
||
// 9. Ghost Preview
|
||
if (this.hoverCell && this.economy.selectedCard && this.economy.selectedCard !== 'shovel') {
|
||
const bounds = this.grid.getCellBounds(this.hoverCell.col, this.hoverCell.row);
|
||
this.ctx.save();
|
||
this.ctx.globalAlpha = 0.55;
|
||
this.economy.drawCardThumbnail(this.ctx, this.economy.selectedCard, bounds.centerX, bounds.centerY);
|
||
this.ctx.restore();
|
||
}
|
||
|
||
// 10. Level & Score indicator in top right
|
||
this.ctx.save();
|
||
this.ctx.fillStyle = '#ffeb3b';
|
||
this.ctx.font = 'bold 12px sans-serif';
|
||
this.ctx.textAlign = 'right';
|
||
const modeLabel = this.currentMode === 'survival'
|
||
? 'Выживание'
|
||
: (this.currentMode === 'bowling'
|
||
? 'Боулинг'
|
||
: (this.currentMode === 'boss'
|
||
? 'Босс: Д-р Зомбосс'
|
||
: (this.currentMode === 'roguelike' ? `Рогалик: Этаж ${this.roguelike ? this.roguelike.currentFloor : 1}` : `Уровень ${this.currentLevel}`)));
|
||
this.ctx.fillText(`${modeLabel} | Счёт: ${this.score}`, 880, 78);
|
||
this.ctx.restore();
|
||
|
||
// 11. Pause Banner Overlay
|
||
if (this.isPaused) {
|
||
this.ctx.save();
|
||
this.ctx.fillStyle = 'rgba(0, 0, 0, 0.65)';
|
||
this.ctx.fillRect(0, 0, this.logicalWidth, this.logicalHeight);
|
||
|
||
// Center Modal Card
|
||
this.ctx.fillStyle = '#2a160c';
|
||
this.ctx.strokeStyle = '#e09f3e';
|
||
this.ctx.lineWidth = 4;
|
||
this.ctx.beginPath();
|
||
this.ctx.roundRect(260, 210, 380, 180, 16);
|
||
this.ctx.fill();
|
||
this.ctx.stroke();
|
||
|
||
this.ctx.fillStyle = '#ffeb3b';
|
||
this.ctx.font = 'bold 28px sans-serif';
|
||
this.ctx.textAlign = 'center';
|
||
this.ctx.fillText('⏸️ ИГРА НА ПАУЗЕ', this.logicalWidth / 2, 270);
|
||
|
||
this.ctx.fillStyle = '#e0e0e0';
|
||
this.ctx.font = '14px sans-serif';
|
||
this.ctx.fillText('Все растения и зомби остановлены.', this.logicalWidth / 2, 308);
|
||
|
||
this.ctx.fillStyle = '#81c784';
|
||
this.ctx.font = 'bold 15px sans-serif';
|
||
this.ctx.fillText('Нажмите Пробел или кнопку «▶️ Играть»', this.logicalWidth / 2, 345);
|
||
this.ctx.restore();
|
||
}
|
||
}
|
||
|
||
drawRake(ctx, x, y) {
|
||
ctx.save();
|
||
ctx.translate(x, y);
|
||
ctx.strokeStyle = '#8d6e63';
|
||
ctx.lineWidth = 3;
|
||
ctx.beginPath();
|
||
ctx.moveTo(-10, 18);
|
||
ctx.lineTo(15, -12);
|
||
ctx.stroke();
|
||
|
||
ctx.strokeStyle = '#757575';
|
||
ctx.lineWidth = 2.5;
|
||
ctx.beginPath();
|
||
ctx.moveTo(8, -16);
|
||
ctx.lineTo(20, -6);
|
||
for (let i = 0; i < 4; i++) {
|
||
ctx.moveTo(8 + i * 3, -16 + i * 2.5);
|
||
ctx.lineTo(5 + i * 3, -22 + i * 2.5);
|
||
}
|
||
ctx.stroke();
|
||
ctx.restore();
|
||
}
|
||
};
|