Files
ai-qpid-pvz/game/js/systems/wave.js
T
Antigravity 6457a8904e feat: Initial commit Plants vs Zombies Web Edition v2.4
- 5 Biomes: Day, Night, Pool, Fog, Roof (25 Campaign levels)
- Minigames: Wall-nut Bowling, I Zombie, Roguelike, Boss Rush & Dr. Zomboss
- Systems: Zen Garden, Workshop Crafting, Almanac, Tree of Wisdom, Achievements
- Backend: REST API + SQLite Accounts & Save Synchronization
- Infrastructure: Nginx HTTPS & Docker deployment
2026-09-17 09:30:52 +00:00

394 lines
14 KiB
JavaScript

// 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();
}
}
};