560 lines
20 KiB
JavaScript
560 lines
20 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);
|
|
}
|
|
}
|
|
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();
|
|
}
|
|
}
|
|
};
|