Add death/explosion visual effects and impact sparks

- Implement `AlienShip.explode()` with cyan flash, shrinking spin animation and a shockwave ring.
- Enhance `Player.die()` to flash white, hide the sprite, and spawn shattering shard graphics.
- Introduce `GameScene.spawnImpactSparks()` to create brief yellow spark particles on bullet impacts.
- Update bullet‑asteroid and bullet‑alien collision handling to trigger impact sparks and use the new alien explosion.
- Minor refactor to use the new visual effects for a more polished death sequence.
This commit is contained in:
Brian Fertig 2026-02-21 09:07:56 -07:00
parent 9a40e9a23a
commit dc7f18c17c
3 changed files with 126 additions and 4 deletions

View File

@ -106,6 +106,56 @@ export default class AlienShip {
}); });
} }
explode() {
if (!this.alive) return;
this.alive = false;
this.sprite.body.setVelocity(0, 0);
const scene = this.scene;
const x = this.sprite.x;
const y = this.sprite.y;
// Brief cyan flash before implosion
this.sprite.setTint(0x00ffff);
scene.time.delayedCall(60, () => {
if (this.sprite && this.sprite.active) this.sprite.clearTint();
});
// Phase 1 (0400ms): sprite collapses and spins 360°
scene.tweens.add({
targets: this.sprite,
scaleX: 0,
scaleY: 0,
angle: this.sprite.angle + 360,
duration: 400,
ease: 'Cubic.In',
onComplete: () => {
if (this.sprite && this.sprite.active) this.sprite.destroy();
}
});
// Phase 2 (200700ms): cyan shockwave ring expands and fades
scene.time.delayedCall(200, () => {
const gfx = scene.add.graphics();
gfx.setDepth(5);
const counter = { t: 0 };
scene.tweens.add({
targets: counter,
t: 1,
duration: 500,
onUpdate: () => {
const t = counter.t;
gfx.clear();
const radius = t * 120;
if (radius < 1) return;
gfx.lineStyle(3, 0x00ffff, Math.max(0, 1 - t));
gfx.strokeCircle(x, y, radius);
},
onComplete: () => gfx.destroy()
});
});
}
destroy() { destroy() {
if (!this.alive) return; if (!this.alive) return;
this.alive = false; this.alive = false;

View File

@ -102,11 +102,57 @@ export default class Player {
// Hide the ship and go invincible immediately call before delaying respawn // Hide the ship and go invincible immediately call before delaying respawn
die() { die() {
this.alive = false; this.alive = false;
this.sprite.setActive(false).setVisible(false);
this.sprite.body.setVelocity(0, 0);
// Go invincible right away so no further collision callbacks count
this.invincible = true; this.invincible = true;
this.invincibleUntil = this.scene.time.now + 99999; this.invincibleUntil = this.scene.time.now + 99999;
this.sprite.body.setVelocity(0, 0);
const x = this.sprite.x;
const y = this.sprite.y;
const angle = this.sprite.angle;
// White flash for 100ms, then hide
this.sprite.setTint(0xffffff);
this.scene.time.delayedCall(100, () => {
if (this.sprite && this.sprite.active) {
this.sprite.clearTint();
this.sprite.setActive(false).setVisible(false);
}
});
this._spawnShatterShards(x, y, angle);
}
_spawnShatterShards(x, y, angle) {
const scene = this.scene;
const defs = [
{ angleOffset: -115, dist: 65, size: 7, spin: 270 },
{ angleOffset: 115, dist: 65, size: 9, spin: -270 },
{ angleOffset: 175, dist: 80, size: 11, spin: 240 },
];
defs.forEach((def, i) => {
const gfx = scene.add.graphics();
gfx.setDepth(5);
gfx.fillStyle(0x00ff44, 1);
const s = def.size;
if (i === 0) gfx.fillTriangle(0, -s, -s * 0.6, s * 0.5, s * 0.4, s * 0.5);
else if (i === 1) gfx.fillTriangle(s * 0.5, -s * 0.8, -s * 0.5, -s * 0.3, 0, s);
else gfx.fillTriangle(-s * 0.7, 0, s * 0.7, 0, 0, s);
gfx.x = x;
gfx.y = y;
const rad = Phaser.Math.DegToRad(angle + def.angleOffset);
scene.tweens.add({
targets: gfx,
x: x + Math.cos(rad) * def.dist,
y: y + Math.sin(rad) * def.dist,
angle: gfx.angle + def.spin,
alpha: 0,
duration: 800,
ease: 'Quad.Out',
onComplete: () => gfx.destroy()
});
});
} }
makeInvincible() { makeInvincible() {

View File

@ -172,6 +172,7 @@ export default class GameScene extends Phaser.Scene {
const scoreTable = { large: 20, medium: 50, small: 100 }; const scoreTable = { large: 20, medium: 50, small: 100 };
this.addScore(scoreTable[asteroid.size] || 20); this.addScore(scoreTable[asteroid.size] || 20);
this.spawnImpactSparks(bulletSprite.x, bulletSprite.y);
this.splitAsteroid(asteroid); this.splitAsteroid(asteroid);
bullet.destroy(); bullet.destroy();
@ -185,7 +186,8 @@ export default class GameScene extends Phaser.Scene {
if (!bullet || !alien || !bullet.alive || !alien.alive) return; if (!bullet || !alien || !bullet.alive || !alien.alive) return;
this.addScore(200); this.addScore(200);
alien.destroy(); this.spawnImpactSparks(bulletSprite.x, bulletSprite.y);
alien.explode();
this.aliens = this.aliens.filter(a => a !== alien); this.aliens = this.aliens.filter(a => a !== alien);
bullet.destroy(); bullet.destroy();
@ -270,6 +272,30 @@ export default class GameScene extends Phaser.Scene {
this.scoreText.setText(`SCORE: ${this.score}`); this.scoreText.setText(`SCORE: ${this.score}`);
} }
spawnImpactSparks(x, y) {
const count = Phaser.Math.Between(3, 5);
for (let i = 0; i < count; i++) {
const gfx = this.add.graphics();
gfx.setDepth(5);
gfx.fillStyle(0xffff00, 1);
gfx.fillCircle(0, 0, Phaser.Math.Between(2, 4));
gfx.x = x;
gfx.y = y;
const angle = Math.random() * Math.PI * 2;
const dist = Phaser.Math.Between(20, 50);
this.tweens.add({
targets: gfx,
x: x + Math.cos(angle) * dist,
y: y + Math.sin(angle) * dist,
alpha: 0,
duration: 250,
ease: 'Quad.Out',
onComplete: () => gfx.destroy()
});
}
}
triggerGameOver() { triggerGameOver() {
this.gameOver = true; this.gameOver = true;
this.player.alive = false; this.player.alive = false;