// 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'; } this.customWavesConfig = campLvl.wavesConfig || campLvl.waves || null; this.modifiers = campLvl.modifiers || {}; this.speedMultiplier = this.modifiers.speedMultiplier || campLvl.speedMultiplier || 1.0; this.hpMultiplier = this.modifiers.hpMultiplier || campLvl.hpMultiplier || 1.0; this.startingSun = this.modifiers.startingSun !== undefined ? this.modifiers.startingSun : (campLvl.startingSun !== undefined ? campLvl.startingSun : null); } else { this.customWavesConfig = null; this.modifiers = {}; this.speedMultiplier = 1.0; this.hpMultiplier = 1.0; this.startingSun = null; 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 (Custom Server Waves or Standard Biome Progression) if (this.customWavesConfig) { let customWave = null; if (Array.isArray(this.customWavesConfig)) { customWave = this.customWavesConfig.find(w => w && w.wave === this.currentWave) || this.customWavesConfig[this.currentWave - 1]; } else if (typeof this.customWavesConfig === 'object') { if (Array.isArray(this.customWavesConfig.list)) { customWave = this.customWavesConfig.list.find(w => w && w.wave === this.currentWave) || this.customWavesConfig.list[this.currentWave - 1]; } else { customWave = this.customWavesConfig[String(this.currentWave)] || this.customWavesConfig[this.currentWave - 1]; } } if (customWave && (customWave.squad || customWave.zombies)) { const isLastWave = this.currentWave >= this.totalWaves; const defaultBanner = isLastWave ? '☠️ ПОСЛЕДНЯЯ ВОЛНА!' : `ВОЛНА ${this.currentWave} / ${this.totalWaves}`; this.bannerText = customWave.banner || customWave.bannerText || defaultBanner; this.bannerTimer = customWave.bannerTimer || (isLastWave ? 4.0 : 3.0); if (customWave.isHuge || customWave.is_huge || isLastWave || this.bannerText.includes('БОЛЬШАЯ') || this.bannerText.includes('ПОСЛЕДНЯЯ')) { if (window.PvZAudio) window.PvZAudio.playHugeWave(); } this.spawnQueue = []; const rawSquad = customWave.squad || customWave.zombies || []; if (Array.isArray(rawSquad)) { for (let i = 0; i < rawSquad.length; i++) { const item = rawSquad[i]; const zType = typeof item === 'string' ? item : (item.type || 'normal'); const delay = (typeof item === 'object' && item.delay !== undefined) ? item.delay : (2.0 + i * 2.2); let row = (typeof item === 'object' && item.row !== undefined && item.row >= 0) ? item.row : null; if (row === null) { row = (zType === 'duckytube' && (this.biome === 'pool' || this.biome === 'fog')) ? (Math.random() > 0.5 ? 2 : 3) : this.randomRow(gameState); } this.spawnQueue.push({ delay, type: zType, row }); } } // Night mode gravestones rise on final or huge wave if (this.biome === 'night' && (isLastWave || customWave.isHuge)) { 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; if (this.hpMultiplier && this.hpMultiplier !== 1.0) { z.maxHp *= this.hpMultiplier; z.hp *= this.hpMultiplier; } if (this.speedMultiplier && this.speedMultiplier !== 1.0) { z.speed *= this.speedMultiplier; } gameState.zombies.push(z); gameState.combat.createExplosionParticles(bounds.centerX, bounds.centerY); } }, 2500); } } this.initialWaveZombies = Math.max(1, this.spawnQueue.length); return; } } // Default Biome Standard Campaign Wave Progression 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); } } this.initialWaveZombies = Math.max(1, this.spawnQueue.length); } 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) } ]; this.initialWaveZombies = Math.max(1, this.spawnQueue.length); } if (!this.initialWaveZombies) { this.initialWaveZombies = Math.max(1, this.spawnQueue.length); } } randomRow(gameState) { return Math.floor(Math.random() * gameState.grid.rows); } getFlagWaves() { if (this.mode === 'survival' || this.mode === 'izombie') return []; const flags = []; if (this.totalWaves >= 4) { flags.push({ wave: this.totalWaves - 1, type: 'huge' }); flags.push({ wave: this.totalWaves, type: 'final' }); } else if (this.totalWaves === 3) { flags.push({ wave: 2, type: 'huge' }); flags.push({ wave: 3, type: 'final' }); } else if (this.totalWaves >= 1) { flags.push({ wave: this.totalWaves, type: 'final' }); } return flags; } update(dt, gameState) { if (this.bannerTimer > 0) { this.bannerTimer -= dt; } if (this.mode === 'izombie') return; // Smooth wave progress calculation const total = Math.max(1, this.totalWaves); let currentFrac = 0; if (this.waveState === 'waiting' || this.waveState === 'completed') { currentFrac = 1.0; } else if (this.waveState === 'spawning') { const initial = Math.max(1, this.initialWaveZombies || 1); const remainingInQueue = this.spawnQueue.length; const spawnedFrac = Math.max(0, 1 - (remainingInQueue / initial)); const aliveCount = (gameState && gameState.zombies) ? gameState.zombies.length : 0; const defeatFrac = aliveCount > 0 ? 0.5 : 1.0; currentFrac = Math.min(0.95, spawnedFrac * 0.7 + defeatFrac * 0.25); } else { currentFrac = 0.05; } const targetProgress = this.mode === 'survival' ? 1.0 : Math.min(1, Math.max(0, (Math.max(0, this.currentWave - 1) + currentFrac) / total)); if (this.smoothProgress === undefined) this.smoothProgress = 0; this.smoothProgress += (targetProgress - this.smoothProgress) * Math.min(1, dt * 3.5); 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; this.smoothProgress = 1.0; 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) { if (this.mode === 'izombie') return; // 1. Visual Wave Progress Bar in Top Right HUD const barX = 680; const barY = 16; const barW = 205; const barH = 24; // Outer frame & drop shadow ctx.save(); ctx.fillStyle = '#1e1008'; ctx.beginPath(); ctx.roundRect(barX, barY, barW, barH, 5); ctx.fill(); // Inset track ctx.fillStyle = '#110b06'; ctx.beginPath(); ctx.roundRect(barX + 2, barY + 2, barW - 4, barH - 4, 3); ctx.fill(); // Progress fill with PvZ gradient const progress = this.mode === 'survival' ? 1.0 : Math.min(1, Math.max(0, this.smoothProgress || 0)); const fillW = Math.max(0, (barW - 4) * progress); if (fillW > 0) { const grad = ctx.createLinearGradient(barX + 2, barY, barX + 2 + fillW, barY); const isFinalOrHuge = this.currentWave >= this.totalWaves || (this.totalWaves >= 4 && this.currentWave === this.totalWaves - 1); if (isFinalOrHuge) { grad.addColorStop(0, '#e65100'); grad.addColorStop(0.5, '#f57c00'); grad.addColorStop(1, '#ffd54f'); } else { grad.addColorStop(0, '#1b5e20'); grad.addColorStop(0.5, '#388e3c'); grad.addColorStop(1, '#76ff03'); } ctx.fillStyle = grad; ctx.beginPath(); ctx.roundRect(barX + 2, barY + 2, fillW, barH - 4, 3); ctx.fill(); } // Flag Markers on the Progress Bar const flagWaves = this.getFlagWaves(); for (const flag of flagWaves) { const flagRatio = flag.wave / this.totalWaves; const fx = barX + 2 + (barW - 4) * flagRatio; const isReached = this.currentWave >= flag.wave; // Flag pole ctx.strokeStyle = '#9e9e9e'; ctx.lineWidth = 2; ctx.beginPath(); ctx.moveTo(fx, barY + 1); ctx.lineTo(fx, barY + barH - 2); ctx.stroke(); // Flag banner ctx.save(); if (flag.type === 'final') { // Final wave skull / red flag ctx.fillStyle = isReached ? '#d50000' : '#8d6e63'; ctx.beginPath(); ctx.moveTo(fx, barY + 3); ctx.lineTo(fx - 12, barY + 7); ctx.lineTo(fx, barY + 12); ctx.closePath(); ctx.fill(); // Skull dot / marker ctx.fillStyle = '#ffffff'; ctx.font = 'bold 8px sans-serif'; ctx.textAlign = 'right'; ctx.fillText('☠️', fx - 1, barY - 2); } else { // Huge wave orange/red flag ctx.fillStyle = isReached ? '#ff3d00' : '#8d6e63'; ctx.beginPath(); ctx.moveTo(fx, barY + 3); ctx.lineTo(fx - 10, barY + 7); ctx.lineTo(fx, barY + 11); ctx.closePath(); ctx.fill(); ctx.fillStyle = '#ffffff'; ctx.font = 'bold 8px sans-serif'; ctx.textAlign = 'right'; ctx.fillText('🚩', fx - 1, barY - 2); } ctx.restore(); } // Border highlight ctx.strokeStyle = '#8d6e63'; ctx.lineWidth = 2; ctx.beginPath(); ctx.roundRect(barX, barY, barW, barH, 5); ctx.stroke(); // Moving Zombie Head icon on progress tip if (this.mode !== 'survival') { const zx = barX + 2 + fillW; const zy = barY + barH / 2; ctx.save(); // Green zombie head circle ctx.fillStyle = '#689f38'; ctx.strokeStyle = '#33691e'; ctx.lineWidth = 1.5; ctx.beginPath(); ctx.arc(zx, zy, 7, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); // Eyes ctx.fillStyle = '#ffffff'; ctx.beginPath(); ctx.arc(zx - 2.5, zy - 1.5, 2, 0, Math.PI * 2); ctx.arc(zx + 2.5, zy - 1.5, 1.5, 0, Math.PI * 2); ctx.fill(); ctx.fillStyle = '#000000'; ctx.beginPath(); ctx.arc(zx - 2.5, zy - 1.5, 1, 0, Math.PI * 2); ctx.arc(zx + 2.5, zy - 1.5, 0.8, 0, Math.PI * 2); ctx.fill(); ctx.restore(); } // Text Label inside progress bar ctx.fillStyle = '#ffffff'; ctx.font = 'bold 11px sans-serif'; ctx.textAlign = 'center'; ctx.shadowColor = '#000000'; ctx.shadowBlur = 4; ctx.shadowOffsetX = 1; ctx.shadowOffsetY = 1; let label = ''; if (this.mode === 'survival') { label = `🚩 ВЫЖИВАНИЕ: ФЛАГ ${this.survivalFlags}`; } else if (this.mode === 'boss' || this.mode === 'bossrush') { label = `👑 БОСС (${Math.min(this.currentWave, this.totalWaves)}/${this.totalWaves})`; } else if (this.mode === 'roguelike') { label = `🎲 ЭТАЖ ${this.level} (${Math.min(this.currentWave, this.totalWaves)}/${this.totalWaves})`; } else if (this.currentWave >= this.totalWaves && this.totalWaves > 1) { label = `☠️ ПОСЛЕДНЯЯ ВОЛНА (${this.totalWaves}/${this.totalWaves})`; } else if (this.totalWaves >= 4 && this.currentWave === this.totalWaves - 1) { label = `⚠️ БОЛЬШАЯ ВОЛНА (${this.currentWave}/${this.totalWaves})`; } else { label = `ВОЛНА ${Math.min(this.currentWave, this.totalWaves)} / ${this.totalWaves}`; } ctx.fillText(label, barX + barW / 2, barY + 16); ctx.restore(); // 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(); } } };