// Wave Management, Level Progression & Game Modes System window.WaveSystem = class WaveSystem { constructor() { this.mode = 'adventure'; // 'adventure', 'survival', 'bowling', 'izombie' this.level = 1; this.biome = 'day'; this.currentWave = 0; this.totalWaves = 4; this.waveTimer = 0; this.spawnQueue = []; this.waveState = 'starting'; this.bannerText = ''; this.bannerTimer = 0; this.isVictory = false; // Endless Survival tracking this.survivalFlags = 0; } setLevel(level = 1, mode = 'adventure') { this.level = level; this.mode = mode; if (mode === 'bowling') { this.biome = 'day'; this.totalWaves = 5; } else if (mode === 'izombie') { this.biome = 'day'; this.totalWaves = 1; } else if (mode === 'boss') { this.biome = 'roof'; this.totalWaves = 1; } else if (mode === 'bossrush') { this.biome = 'roof'; this.totalWaves = 3; } else if (mode === 'roguelike') { this.totalWaves = 2; this.biome = (level <= 2) ? 'day' : (level <= 4 ? 'night' : (level <= 6 ? 'pool' : 'roof')); } else if (mode === 'survival') { this.biome = 'day'; this.totalWaves = 999; } else { // Adventure Campaign Mode with CampaignSystem if (window.CampaignSystem) { const campLvl = window.CampaignSystem.getLevel(level); this.biome = campLvl.biome || 'day'; this.totalWaves = campLvl.waves || 4; if (campLvl.isBoss) { this.mode = 'boss'; } } else { if (level <= 5) { this.biome = 'day'; this.totalWaves = (level === 1) ? 3 : 4; } else if (level <= 10) { this.biome = 'night'; this.totalWaves = 4; } else if (level <= 15) { this.biome = 'pool'; this.totalWaves = 4; } else if (level <= 20) { this.biome = 'fog'; this.totalWaves = 4; } else if (level < 25) { this.biome = 'roof'; this.totalWaves = 4; } else { this.biome = 'roof'; this.totalWaves = 1; this.mode = 'boss'; } } } this.reset(); } reset() { this.currentWave = 0; this.waveTimer = 0; this.spawnQueue = []; this.waveState = 'starting'; this.survivalFlags = 0; // reset per-game, not per-session let advBanner = `УРОВЕНЬ ${this.level}`; if (window.CampaignSystem && this.mode === 'adventure') { const campFmt = window.CampaignSystem.formatLevel(this.level); advBanner = `${campFmt.worldIcon} ЭТАП ${campFmt.code}: ${campFmt.title.toUpperCase()}`; } this.bannerText = this.mode === 'bowling' ? '🎳 БОУЛИНГ СТЕНООРЕХАМИ!' : (this.mode === 'izombie' ? '🧠 СЪЕШЬТЕ ВСЕ ПОДСОЛНУХИ!' : (this.mode === 'bossrush' ? '⚡ BOSS RUSH: ЧЕРЕДА БОССОВ!' : (this.mode === 'boss' ? '👑 БИТВА: ДОКТОР ЗОМБОСС!' : (this.mode === 'roguelike' ? `🎲 РОГАЛИК: ЭТАЖ ${this.level}` : (this.mode === 'survival' ? '⚔️ БЕСКОНЕЧНОЕ ВЫЖИВАНИЕ' : advBanner))))); this.bannerTimer = 3.5; this.isVictory = false; } startNextWave(gameState) { this.currentWave++; this.waveState = 'spawning'; if (this.mode === 'boss') { this.bannerText = '👑 ДОКТОР ЗОМБОСС АТАКУЕТ!'; this.bannerTimer = 3.5; if (gameState.boss) { gameState.boss.startFight(); } return; } if (this.mode === 'bossrush') { if (this.currentWave === 1) { this.bannerText = '⚡ ВОЛНА 1: ОРДА ТЯЖЕЛОВЕСОВ И СПРИНТЕРОВ!'; this.bannerTimer = 3.5; this.spawnQueue = []; const count = 7; for (let i = 0; i < count; i++) { const types = ['football', 'buckethead', 'conehead']; const t = types[i % types.length]; this.spawnQueue.push({ delay: 2.0 + i * 2.2, type: t, row: i % gameState.grid.rows }); } } else if (this.currentWave === 2) { this.bannerText = '⚡ ВОЛНА 2: ЭЛИТНЫЙ ЗОМБИ-СПЕЦНАЗ!'; this.bannerTimer = 3.5; this.spawnQueue = []; const count = 9; for (let i = 0; i < count; i++) { const types = ['newspaper', 'polevaulter', 'buckethead', 'football']; const t = types[i % types.length]; this.spawnQueue.push({ delay: 1.8 + i * 1.8, type: t, row: (i * 2) % gameState.grid.rows }); } } else if (this.currentWave === 3) { this.bannerText = '👑 ФИНАЛЬНЫЙ БОСС: Д-Р ЗОМБОСС & ЗОМБОТ!'; this.bannerTimer = 4.0; if (gameState.boss) { gameState.boss.startFight(); } } return; } if (this.mode === 'bowling') { this.bannerText = `ВОЛНА ${this.currentWave} / ${this.totalWaves}`; this.bannerTimer = 2.5; const count = 4 + this.currentWave * 2; this.spawnQueue = []; for (let i = 0; i < count; i++) { const types = ['normal', 'conehead', 'newspaper', 'polevaulter', 'buckethead']; const t = types[Math.floor(Math.random() * Math.min(types.length, 2 + this.currentWave))]; this.spawnQueue.push({ delay: 2.0 + i * 2.5, type: t, row: Math.floor(Math.random() * gameState.grid.rows) }); } return; } if (this.mode === 'survival') { this.survivalFlags++; this.bannerText = `🚩 ФЛАГ ${this.survivalFlags}!`; this.bannerTimer = 3.0; if (window.PvZAudio) window.PvZAudio.playHugeWave(); const waveSize = 8 + this.survivalFlags * 3; this.spawnQueue = []; const types = ['normal', 'conehead', 'buckethead', 'polevaulter', 'newspaper', 'football']; for (let i = 0; i < waveSize; i++) { this.spawnQueue.push({ delay: 1.5 + i * (22 / waveSize), type: types[Math.floor(Math.random() * types.length)], row: Math.floor(Math.random() * gameState.grid.rows) }); } return; } if (this.mode === 'roguelike') { this.bannerText = `🎲 ЭТАЖ ${this.level} - ВОЛНА ${this.currentWave} / ${this.totalWaves}`; this.bannerTimer = 2.8; const count = 3 + this.level * 2 + this.currentWave * 2; this.spawnQueue = []; const types = ['normal', 'conehead', 'newspaper', 'polevaulter', 'buckethead', 'football']; const maxTypeIndex = Math.min(types.length - 1, Math.floor(this.level / 2) + this.currentWave); for (let i = 0; i < count; i++) { const t = types[Math.floor(Math.random() * (maxTypeIndex + 1))]; this.spawnQueue.push({ delay: 1.5 + i * (14 / count), type: t, row: this.biome === 'pool' && Math.random() < 0.35 ? (Math.random() > 0.5 ? 2 : 3) : this.randomRow(gameState) }); } return; } // Adventure Campaign Waves if (this.currentWave === 1) { this.bannerText = 'ПЕРВЫЕ ЗОМБИ...'; this.bannerTimer = 3.0; this.spawnQueue = [ { delay: 3.0, type: 'normal', row: this.randomRow(gameState) }, { delay: 8.0, type: 'normal', row: this.randomRow(gameState) }, { delay: 14.0, type: 'conehead', row: this.randomRow(gameState) }, { delay: 19.0, type: 'normal', row: this.randomRow(gameState) } ]; } else if (this.currentWave === 2) { this.bannerText = 'ВОЛНА 2'; this.bannerTimer = 2.5; const isWaterBiome = this.biome === 'pool' || this.biome === 'fog'; const typeList = isWaterBiome ? ['normal', 'duckytube', 'conehead', 'duckytube', 'polevaulter'] : ['normal', 'newspaper', 'polevaulter', 'conehead', 'normal']; this.spawnQueue = typeList.map((t, idx) => ({ delay: 2.5 + idx * 3.5, type: t, row: t === 'duckytube' ? (Math.random() > 0.5 ? 2 : 3) : this.randomRow(gameState) })); } else if (this.currentWave === 3) { this.bannerText = '⚠️ БОЛЬШАЯ ВОЛНА ЗОМБИ!'; this.bannerTimer = 4.0; if (window.PvZAudio) window.PvZAudio.playHugeWave(); const isWaterBiome = this.biome === 'pool' || this.biome === 'fog'; this.spawnQueue = [ { delay: 2.0, type: 'flag', row: isWaterBiome ? 1 : 2 }, { delay: 2.5, type: 'conehead', row: 0 }, { delay: 2.8, type: isWaterBiome ? 'duckytube' : 'newspaper', row: isWaterBiome ? 2 : 1 }, { delay: 3.2, type: 'football', row: isWaterBiome ? 4 : 3 }, { delay: 3.5, type: isWaterBiome ? 'duckytube' : 'polevaulter', row: isWaterBiome ? 3 : 4 }, { delay: 7.0, type: 'buckethead', row: this.randomRow(gameState) }, { delay: 9.0, type: isWaterBiome ? 'duckytube' : 'normal', row: isWaterBiome ? (Math.random() > 0.5 ? 2 : 3) : this.randomRow(gameState) }, { delay: 12.0, type: 'conehead', row: this.randomRow(gameState) } ]; // Night mode: Zombies rise from gravestones! if (this.biome === 'night') { for (const grave of gameState.grid.graves) { setTimeout(() => { if (gameState.isRunning) { const bounds = gameState.grid.getCellBounds(grave.col, grave.row); const z = new window.Zombie(grave.row, 'normal', bounds.centerX); z.y = bounds.centerY - 5; gameState.zombies.push(z); gameState.combat.createExplosionParticles(bounds.centerX, bounds.centerY); } }, 2500); } } } else if (this.currentWave >= 4) { this.bannerText = '☠️ ПОСЛЕДНЯЯ ВОЛНА!'; this.bannerTimer = 4.5; if (window.PvZAudio) window.PvZAudio.playHugeWave(); const isWaterBiome = this.biome === 'pool' || this.biome === 'fog'; this.spawnQueue = [ { delay: 2.0, type: 'flag', row: isWaterBiome ? 1 : 2 }, { delay: 2.4, type: 'football', row: 0 }, { delay: 2.8, type: isWaterBiome ? 'duckytube' : 'buckethead', row: isWaterBiome ? 2 : 1 }, { delay: 3.2, type: 'polevaulter', row: isWaterBiome ? 4 : 2 }, { delay: 3.5, type: isWaterBiome ? 'duckytube' : 'newspaper', row: isWaterBiome ? 3 : 3 }, { delay: 3.8, type: 'conehead', row: isWaterBiome ? 5 : 4 }, { delay: 6.5, type: 'buckethead', row: this.randomRow(gameState) }, { delay: 8.0, type: 'football', row: this.randomRow(gameState) }, { delay: 10.0, type: isWaterBiome ? 'duckytube' : 'conehead', row: isWaterBiome ? (Math.random() > 0.5 ? 2 : 3) : this.randomRow(gameState) }, { delay: 12.0, type: 'normal', row: this.randomRow(gameState) } ]; } } randomRow(gameState) { return Math.floor(Math.random() * gameState.grid.rows); } update(dt, gameState) { if (this.bannerTimer > 0) { this.bannerTimer -= dt; } if (this.mode === 'izombie') return; if (this.waveState === 'starting') { this.waveTimer += dt; if (this.waveTimer >= 3.5) { this.waveTimer = 0; this.startNextWave(gameState); } return; } if (this.mode === 'boss') return; // Boss mode fight is updated by boss system if (this.waveState === 'spawning') { this.waveTimer += dt; for (let i = this.spawnQueue.length - 1; i >= 0; i--) { const item = this.spawnQueue[i]; if (this.waveTimer >= item.delay) { gameState.spawnZombie(item.row, item.type); this.spawnQueue.splice(i, 1); } } // In bossrush wave 3, victory is triggered when boss is defeated, not immediately const isBossRushFight = this.mode === 'bossrush' && this.currentWave === 3; if (this.spawnQueue.length === 0 && gameState.zombies.length === 0 && !isBossRushFight) { if (this.currentWave >= this.totalWaves && this.mode !== 'survival') { this.waveState = 'completed'; this.isVictory = true; gameState.triggerVictory(); } else { this.waveState = 'waiting'; this.waveTimer = 0; } } } else if (this.waveState === 'waiting') { this.waveTimer += dt; if (this.waveTimer >= 4.0) { this.waveTimer = 0; this.startNextWave(gameState); } } } draw(ctx) { // 1. Progress Bar in Top Right const barX = 685; const barY = 22; const barW = 115; const barH = 22; ctx.fillStyle = '#212121'; ctx.beginPath(); ctx.roundRect(barX, barY, barW, barH, 4); ctx.fill(); const progress = this.mode === 'survival' ? 1.0 : Math.min(1, (this.currentWave - 1 + (this.waveState === 'waiting' || this.waveState === 'completed' ? 1 : 0.5)) / this.totalWaves); ctx.fillStyle = '#4caf50'; ctx.beginPath(); ctx.roundRect(barX + barW * (1 - progress), barY + 2, barW * progress - 2, barH - 4, 3); ctx.fill(); ctx.strokeStyle = '#8d6e63'; ctx.lineWidth = 2; ctx.strokeRect(barX, barY, barW, barH); ctx.fillStyle = '#fff'; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'center'; const label = this.mode === 'survival' ? `ФЛАГ ${this.survivalFlags}` : (this.mode === 'roguelike' ? `ЭТАЖ ${this.level} (${Math.min(this.currentWave, this.totalWaves)}/${this.totalWaves})` : `ВОЛНА ${Math.min(this.currentWave, this.totalWaves)}/${this.totalWaves}`); ctx.fillText(label, barX + barW / 2, barY + 16); // 2. Center Banner Display if (this.bannerTimer > 0 && this.bannerText) { ctx.save(); const alpha = Math.min(1, this.bannerTimer); ctx.globalAlpha = alpha; ctx.fillStyle = 'rgba(0, 0, 0, 0.75)'; ctx.fillRect(0, 240, 900, 70); ctx.fillStyle = this.bannerText.includes('БОЛЬШАЯ') || this.bannerText.includes('ПОСЛЕДНЯЯ') ? '#ff1744' : '#ffeb3b'; ctx.font = 'bold 30px sans-serif'; ctx.textAlign = 'center'; ctx.shadowColor = '#000'; ctx.shadowBlur = 8; ctx.fillText(this.bannerText, 450, 285); ctx.restore(); } } };