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:
+184
-18
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user