feat: implement Smash TV-style zone exit mechanic
Adds a new "zone exit" phase that triggers after all waves in a zone are cleared: - Listens for `zone-waves-complete` event to start the exit sequence. - Displays blinking arrows on all screen edges and a "ZONE CLEARED" prompt. - Disables world bounds to allow the player to walk off-screen. - Detects when the player crosses an edge and triggers a transition: - Shows a white flash animation. - Respawns the player on the opposite edge, maintaining their lateral position. - Re-enables world bounds and starts the next zone via `WaveManager.startNextZone()`. Refactors `WaveManager` to defer zone advancement until the player exits, ensuring a smoother flow between zones.
This commit is contained in:
parent
ea0a95c4e5
commit
ea501968e3
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
Binary file not shown.
|
|
@ -49,6 +49,9 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
this._levelUpPending = false;
|
||||
this._frozen = false;
|
||||
this._waitingForExit = false;
|
||||
|
||||
this.events.on('zone-waves-complete', () => this._startZoneExit());
|
||||
|
||||
this.waveManager.start();
|
||||
}
|
||||
|
|
@ -78,6 +81,8 @@ export class GameScene extends Phaser.Scene {
|
|||
this._checkBulletHits();
|
||||
this._checkEnemyProjectileHits();
|
||||
this._pruneEnemyProjectiles();
|
||||
|
||||
if (this._waitingForExit) this._checkPlayerExit();
|
||||
}
|
||||
|
||||
_checkBulletHits() {
|
||||
|
|
@ -114,6 +119,123 @@ export class GameScene extends Phaser.Scene {
|
|||
this._enemyProjectiles = this._enemyProjectiles.filter(p => p?.active);
|
||||
}
|
||||
|
||||
// ── Zone exit (Smash TV style) ─────────────────────────────────────────────
|
||||
|
||||
_startZoneExit() {
|
||||
this._waitingForExit = true;
|
||||
// Let the player walk off any edge
|
||||
this.player.sprite.body.setCollideWorldBounds(false);
|
||||
this._showExitArrows();
|
||||
}
|
||||
|
||||
_showExitArrows() {
|
||||
const W = this.scale.width;
|
||||
const H = this.scale.height;
|
||||
|
||||
const g = this.add.graphics();
|
||||
g.setDepth(15);
|
||||
this._exitArrowsGfx = g;
|
||||
|
||||
const ARROW_W = 30; // triangle base half-width
|
||||
const ARROW_D = 28; // triangle depth (pointing direction)
|
||||
const MARGIN = 18; // gap from screen edge to arrow tip
|
||||
|
||||
const drawUp = (cx, tipY) => g.fillTriangle(cx - ARROW_W, tipY + ARROW_D, cx + ARROW_W, tipY + ARROW_D, cx, tipY);
|
||||
const drawDown = (cx, tipY) => g.fillTriangle(cx - ARROW_W, tipY - ARROW_D, cx + ARROW_W, tipY - ARROW_D, cx, tipY);
|
||||
const drawLeft = (tipX, cy) => g.fillTriangle(tipX + ARROW_D, cy - ARROW_W, tipX + ARROW_D, cy + ARROW_W, tipX, cy);
|
||||
const drawRight = (tipX, cy) => g.fillTriangle(tipX - ARROW_D, cy - ARROW_W, tipX - ARROW_D, cy + ARROW_W, tipX, cy);
|
||||
|
||||
// Three arrows per edge
|
||||
const xPositions = [W * 0.25, W * 0.5, W * 0.75];
|
||||
const yPositions = [H * 0.25, H * 0.5, H * 0.75];
|
||||
|
||||
g.fillStyle(0xffffff, 1);
|
||||
xPositions.forEach(cx => {
|
||||
drawUp(cx, MARGIN);
|
||||
drawDown(cx, H - MARGIN);
|
||||
});
|
||||
yPositions.forEach(cy => {
|
||||
drawLeft(MARGIN, cy);
|
||||
drawRight(W - MARGIN, cy);
|
||||
});
|
||||
|
||||
// Blink the arrows
|
||||
this._exitArrowsTween = this.tweens.add({
|
||||
targets: g,
|
||||
alpha: 0.1,
|
||||
duration: 350,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
ease: 'Sine.easeInOut',
|
||||
});
|
||||
|
||||
// "Zone cleared" prompt
|
||||
this._exitText = this.add.text(W / 2, 60, 'ZONE CLEARED — EXIT THROUGH ANY DOOR', {
|
||||
fontSize: '20px',
|
||||
fill: '#ffdd44',
|
||||
fontStyle: 'bold',
|
||||
stroke: '#000000',
|
||||
strokeThickness: 4,
|
||||
}).setOrigin(0.5).setDepth(15);
|
||||
|
||||
this.tweens.add({
|
||||
targets: this._exitText,
|
||||
alpha: 0,
|
||||
duration: 400,
|
||||
yoyo: true,
|
||||
repeat: -1,
|
||||
});
|
||||
}
|
||||
|
||||
_hideExitArrows() {
|
||||
this._exitArrowsTween?.stop();
|
||||
this._exitArrowsGfx?.destroy();
|
||||
this._exitText?.destroy();
|
||||
this._exitArrowsGfx = null;
|
||||
this._exitText = null;
|
||||
}
|
||||
|
||||
_checkPlayerExit() {
|
||||
const W = this.scale.width;
|
||||
const H = this.scale.height;
|
||||
const px = this.player.x;
|
||||
const py = this.player.y;
|
||||
|
||||
if (px < -24) this._doZoneTransition('left');
|
||||
else if (px > W + 24) this._doZoneTransition('right');
|
||||
else if (py < -24) this._doZoneTransition('top');
|
||||
else if (py > H + 24) this._doZoneTransition('bottom');
|
||||
}
|
||||
|
||||
_doZoneTransition(direction) {
|
||||
this._waitingForExit = false;
|
||||
this._hideExitArrows();
|
||||
|
||||
const W = this.scale.width;
|
||||
const H = this.scale.height;
|
||||
|
||||
// Respawn player on the opposite edge, same lateral position
|
||||
const px = Phaser.Math.Clamp(this.player.x, 48, W - 48);
|
||||
const py = Phaser.Math.Clamp(this.player.y, 48, H - 48);
|
||||
const spawnX = direction === 'left' ? W - 48 : direction === 'right' ? 48 : px;
|
||||
const spawnY = direction === 'top' ? H - 48 : direction === 'bottom' ? 48 : py;
|
||||
|
||||
// White flash
|
||||
const flash = this.add.rectangle(W / 2, H / 2, W, H, 0xffffff).setDepth(50);
|
||||
this.tweens.add({
|
||||
targets: flash,
|
||||
alpha: 0,
|
||||
duration: 300,
|
||||
onComplete: () => flash.destroy(),
|
||||
});
|
||||
|
||||
this.player.sprite.setPosition(spawnX, spawnY);
|
||||
this.player.sprite.body.setCollideWorldBounds(true);
|
||||
this.waveManager.startNextZone();
|
||||
}
|
||||
|
||||
// ── Level up ───────────────────────────────────────────────────────────────
|
||||
|
||||
_showLevelUp(level) {
|
||||
if (this._levelUpPending) return;
|
||||
this._levelUpPending = true;
|
||||
|
|
@ -161,6 +283,8 @@ export class GameScene extends Phaser.Scene {
|
|||
|
||||
shutdown() {
|
||||
this.events.removeAllListeners();
|
||||
this._hideExitArrows();
|
||||
this._waitingForExit = false;
|
||||
this.player?.destroy();
|
||||
this.waveManager?.reset();
|
||||
this.hud?.destroy();
|
||||
|
|
|
|||
|
|
@ -80,26 +80,27 @@ export class WaveManager {
|
|||
this.waveIndex++;
|
||||
|
||||
if (this.waveIndex >= this.currentZone.waves.length) {
|
||||
// Zone complete
|
||||
this.waveIndex = 0;
|
||||
this.zoneIndex++;
|
||||
|
||||
if (this.zoneIndex >= this.zones.length) {
|
||||
this.scene.events.emit('victory');
|
||||
return;
|
||||
}
|
||||
|
||||
// All waves cleared — let the player exit to trigger next zone
|
||||
this.scene.events.emit('zone-clear', { zone: this.zoneNum });
|
||||
this.scene.time.delayedCall(2500, () => {
|
||||
this.active = true;
|
||||
this._spawnWave();
|
||||
});
|
||||
this.scene.events.emit('zone-waves-complete');
|
||||
} else {
|
||||
this.active = true;
|
||||
this._spawnWave();
|
||||
}
|
||||
}
|
||||
|
||||
/** Called by GameScene after the player exits the screen. */
|
||||
startNextZone() {
|
||||
this.waveIndex = 0;
|
||||
this.zoneIndex++;
|
||||
if (this.zoneIndex >= this.zones.length) {
|
||||
this.scene.events.emit('victory');
|
||||
return;
|
||||
}
|
||||
this.active = true;
|
||||
this._spawnWave();
|
||||
}
|
||||
|
||||
reset() {
|
||||
this.enemies.forEach(e => { if (e.active) e.destroy(); });
|
||||
this.enemies = [];
|
||||
|
|
|
|||
Loading…
Reference in New Issue