feat(qol & backup): add wave progress bar, hover collection, area explosion preview, hotkeys, auto-sync and json save export/import

This commit is contained in:
2026-09-17 13:20:29 +00:00
parent 7cd0792799
commit 23918dab1f
7 changed files with 710 additions and 38 deletions
+184 -18
View File
@@ -262,6 +262,7 @@ window.WaveSystem = class WaveSystem {
}, 2500);
}
}
this.initialWaveZombies = Math.max(1, this.spawnQueue.length);
} else if (this.currentWave >= 4) {
this.bannerText = '☠️ ПОСЛЕДНЯЯ ВОЛНА!';
this.bannerTimer = 4.5;
@@ -280,6 +281,10 @@ window.WaveSystem = class WaveSystem {
{ 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);
}
}
@@ -287,6 +292,21 @@ window.WaveSystem = class WaveSystem {
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;
@@ -294,6 +314,29 @@ window.WaveSystem = class WaveSystem {
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) {
@@ -322,6 +365,7 @@ window.WaveSystem = class WaveSystem {
if (this.currentWave >= this.totalWaves && this.mode !== 'survival') {
this.waveState = 'completed';
this.isVictory = true;
this.smoothProgress = 1.0;
gameState.triggerVictory();
} else {
this.waveState = 'waiting';
@@ -338,37 +382,159 @@ window.WaveSystem = class WaveSystem {
}
draw(ctx) {
// 1. Progress Bar in Top Right
const barX = 685;
const barY = 22;
const barW = 115;
const barH = 22;
if (this.mode === 'izombie') return;
ctx.fillStyle = '#212121';
// 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, 4);
ctx.roundRect(barX, barY, barW, barH, 5);
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';
// Inset track
ctx.fillStyle = '#110b06';
ctx.beginPath();
ctx.roundRect(barX + barW * (1 - progress), barY + 2, barW * progress - 2, barH - 4, 3);
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.strokeRect(barX, barY, barW, barH);
ctx.beginPath();
ctx.roundRect(barX, barY, barW, barH, 5);
ctx.stroke();
ctx.fillStyle = '#fff';
// 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';
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.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) {