Add comprehensive shatter animations for vial destruction with evil and holy effects
This commit implements detailed shatter animations for both vial destruction effects: - Added evil shatter animation (sin vial implodes with dark, decaying explosion) for when The Lord wins - Added holy shatter animation (lord vial ascends with divine golden explosion) for when Sin wins - Implemented comprehensive visual effects including explosions, particle systems, smoke, rays, and text animations - Integrated shatter animations with victory video playback in GameScene - Added proper timing and sequencing for dramatic victory presentation - Maintained existing functionality while adding rich visual feedback for game endings - Created reusable animation helper methods in VialDisplay class for consistent effects
This commit is contained in:
parent
d30c018100
commit
5ed7efabd9
|
|
@ -296,4 +296,369 @@ export class VialDisplay {
|
|||
|
||||
scene.game.events.emit('vial-winner', { winner: this.label });
|
||||
}
|
||||
|
||||
// ── Shatter animations ────────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
* Evil destruction: Sin's vial implodes in a dark, decaying explosion.
|
||||
* Called when The Lord wins.
|
||||
*/
|
||||
shatterEvil() {
|
||||
const scene = this.scene;
|
||||
const cx = this.baseX;
|
||||
const cy = this.baseY + this.VIAL_H / 2;
|
||||
const D = 80; // base depth
|
||||
|
||||
if (this._shakeTween) { this._shakeTween.stop(); this._shakeTween = null; }
|
||||
this.container.setPosition(this.baseX, this.baseY);
|
||||
|
||||
// Dark red screen pulse
|
||||
const overlay = scene.add.rectangle(800, 450, 1600, 900, 0x330000).setAlpha(0).setDepth(D + 20);
|
||||
scene.tweens.add({
|
||||
targets: overlay, alpha: { from: 0, to: 0.6 },
|
||||
duration: 100, yoyo: true, repeat: 3,
|
||||
onComplete: () => overlay.destroy(),
|
||||
});
|
||||
|
||||
// Violent random shake, escalating in intensity, then explode
|
||||
const proxy = { t: 0 };
|
||||
scene.tweens.add({
|
||||
targets: proxy, t: 1, duration: 380,
|
||||
onUpdate: () => {
|
||||
const amp = proxy.t * 15;
|
||||
this.container.setX(this.baseX + (Math.random() - 0.5) * amp);
|
||||
this.container.setY(this.baseY + (Math.random() - 0.5) * amp * 0.4);
|
||||
},
|
||||
onComplete: () => {
|
||||
this.container.setPosition(this.baseX, this.baseY);
|
||||
this._doEvilExplosion(cx, cy, D);
|
||||
},
|
||||
});
|
||||
|
||||
// Crack lines drawn at 180 ms
|
||||
scene.time.delayedCall(180, () => {
|
||||
const cracks = scene.add.graphics().setDepth(D + 5);
|
||||
cracks.lineStyle(1.5, 0x880000, 1);
|
||||
for (let i = 0; i < 7; i++) {
|
||||
const a = (Math.PI * 2 / 7) * i + (Math.random() - 0.5) * 0.4;
|
||||
const len = 18 + Math.random() * 28;
|
||||
cracks.beginPath();
|
||||
cracks.moveTo(cx, cy);
|
||||
cracks.lineTo(cx + Math.cos(a) * len, cy + Math.sin(a) * len);
|
||||
const bA = a + (Math.random() - 0.5) * 0.9;
|
||||
cracks.moveTo(cx + Math.cos(a) * len * 0.55, cy + Math.sin(a) * len * 0.55);
|
||||
cracks.lineTo(
|
||||
cx + Math.cos(a) * len * 0.55 + Math.cos(bA) * len * 0.55,
|
||||
cy + Math.sin(a) * len * 0.55 + Math.sin(bA) * len * 0.55,
|
||||
);
|
||||
cracks.strokePath();
|
||||
}
|
||||
scene.tweens.add({ targets: cracks, alpha: 0, delay: 220, duration: 80, onComplete: () => cracks.destroy() });
|
||||
});
|
||||
}
|
||||
|
||||
_doEvilExplosion(cx, cy, D) {
|
||||
const scene = this.scene;
|
||||
|
||||
// Vial implodes inward
|
||||
scene.tweens.add({
|
||||
targets: this.container, alpha: 0, scaleX: 0, scaleY: 0,
|
||||
duration: 300, ease: 'Back.easeIn',
|
||||
});
|
||||
|
||||
// Two expanding dark rings
|
||||
[{ color: 0xaa0000, r: 15, delay: 0, toScale: 9, dur: 650 },
|
||||
{ color: 0x550066, r: 22, delay: 90, toScale: 6, dur: 750 }].forEach(o => {
|
||||
const ring = scene.add.graphics().setDepth(D);
|
||||
ring.lineStyle(5, o.color, 1);
|
||||
ring.strokeCircle(cx, cy, o.r);
|
||||
scene.tweens.add({
|
||||
targets: ring, scaleX: o.toScale, scaleY: o.toScale, alpha: 0,
|
||||
delay: o.delay, duration: o.dur, ease: 'Expo.easeOut',
|
||||
onComplete: () => ring.destroy(),
|
||||
});
|
||||
});
|
||||
|
||||
// Central dark burst
|
||||
const burst = scene.add.graphics().setDepth(D + 2);
|
||||
burst.fillStyle(0x110000, 1);
|
||||
burst.fillCircle(cx, cy, 28);
|
||||
scene.tweens.add({
|
||||
targets: burst, scaleX: 4.5, scaleY: 4.5, alpha: 0,
|
||||
duration: 350, ease: 'Expo.easeOut', onComplete: () => burst.destroy(),
|
||||
});
|
||||
|
||||
// Jagged dark shards radiate outward with gravity fall
|
||||
const shardColors = [0x990000, 0x440022, 0x221100, 0x000000, 0x660044];
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const angle = (Math.PI * 2 / 14) * i + (Math.random() - 0.5) * 0.35;
|
||||
const dist = 110 + Math.random() * 190;
|
||||
const sz = 7 + Math.random() * 13;
|
||||
const shard = scene.add.graphics().setDepth(D + 1);
|
||||
shard.fillStyle(shardColors[i % shardColors.length], 1);
|
||||
shard.fillTriangle(0, -sz * 1.3, -sz * 0.45, sz * 0.4, sz * 0.5, sz * 0.3);
|
||||
shard.setPosition(cx, cy).setRotation(Math.random() * Math.PI * 2);
|
||||
scene.tweens.add({
|
||||
targets: shard,
|
||||
x: cx + Math.cos(angle) * dist,
|
||||
y: cy + Math.sin(angle) * dist + dist * 0.25,
|
||||
rotation: shard.rotation + (Math.random() - 0.5) * 9,
|
||||
scaleX: 0.1, scaleY: 0.1, alpha: 0,
|
||||
duration: 850 + Math.random() * 350,
|
||||
ease: 'Cubic.easeOut', delay: Math.random() * 80,
|
||||
onComplete: () => shard.destroy(),
|
||||
});
|
||||
}
|
||||
|
||||
// Dark smoke clouds rise and dissolve
|
||||
const smokeColors = [0x1a0000, 0x0a000a, 0x110011, 0x001100];
|
||||
for (let i = 0; i < 9; i++) {
|
||||
const smoke = scene.add.graphics().setDepth(D - 1);
|
||||
smoke.fillStyle(smokeColors[i % smokeColors.length], 0.78);
|
||||
smoke.fillCircle(0, 0, 13 + Math.random() * 12);
|
||||
smoke.setPosition(cx + (Math.random() - 0.5) * 55, cy + (Math.random() - 0.5) * 40);
|
||||
smoke.setScale(0.4);
|
||||
scene.tweens.add({
|
||||
targets: smoke,
|
||||
x: smoke.x + (Math.random() - 0.5) * 75,
|
||||
y: smoke.y - 95 - Math.random() * 95,
|
||||
scaleX: 3, scaleY: 3, alpha: 0,
|
||||
duration: 1300 + Math.random() * 700,
|
||||
delay: Math.random() * 200, ease: 'Cubic.easeOut',
|
||||
onComplete: () => smoke.destroy(),
|
||||
});
|
||||
}
|
||||
|
||||
// Evil energy particles scatter (red, purple, sickly green)
|
||||
const pColors = [0xff0000, 0xaa0000, 0x880055, 0x005500, 0x440000];
|
||||
for (let i = 0; i < 22; i++) {
|
||||
const dot = scene.add.graphics().setDepth(D + 1);
|
||||
dot.fillStyle(pColors[i % pColors.length], 1);
|
||||
dot.fillCircle(0, 0, 2 + Math.random() * 4);
|
||||
dot.setPosition(cx + (Math.random() - 0.5) * 25, cy + (Math.random() - 0.5) * 40);
|
||||
const pa = Math.random() * Math.PI * 2;
|
||||
const pd = 70 + Math.random() * 160;
|
||||
scene.tweens.add({
|
||||
targets: dot,
|
||||
x: dot.x + Math.cos(pa) * pd,
|
||||
y: dot.y + Math.sin(pa) * pd + 25,
|
||||
alpha: 0, scaleX: 0.1, scaleY: 0.1,
|
||||
duration: 750 + Math.random() * 450,
|
||||
delay: Math.random() * 150, ease: 'Cubic.easeOut',
|
||||
onComplete: () => dot.destroy(),
|
||||
});
|
||||
}
|
||||
|
||||
// Skull icon rises from the wreckage
|
||||
const skull = scene.add.text(cx, cy - 10, '💀', { fontSize: '60px' })
|
||||
.setOrigin(0.5).setDepth(D + 6).setAlpha(0).setScale(0.2);
|
||||
scene.tweens.add({
|
||||
targets: skull, alpha: 1, scale: 1, duration: 250, ease: 'Back.easeOut',
|
||||
onComplete: () => {
|
||||
scene.tweens.add({
|
||||
targets: skull, y: skull.y - 140, alpha: 0, scale: 1.9,
|
||||
duration: 1900, delay: 400, ease: 'Cubic.easeOut',
|
||||
onComplete: () => skull.destroy(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// "CONDEMNED" label punches in then sinks
|
||||
const condText = scene.add.text(cx, cy + 58, '☠ CONDEMNED ☠', {
|
||||
fontSize: '18px', fontFamily: 'Georgia, serif',
|
||||
color: '#cc0000', stroke: '#000000', strokeThickness: 4,
|
||||
}).setOrigin(0.5).setDepth(D + 6).setAlpha(0).setScale(0.4);
|
||||
scene.tweens.add({
|
||||
targets: condText, alpha: 1, scale: 1, delay: 260, duration: 370, ease: 'Back.easeOut',
|
||||
onComplete: () => {
|
||||
scene.tweens.add({
|
||||
targets: condText, alpha: 0, y: condText.y + 65,
|
||||
delay: 1100, duration: 900, ease: 'Cubic.easeIn',
|
||||
onComplete: () => condText.destroy(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Holy destruction: The Lord's vial ascends in a divine golden explosion.
|
||||
* Called when Sin wins.
|
||||
*/
|
||||
shatterHoly() {
|
||||
const scene = this.scene;
|
||||
const cx = this.baseX;
|
||||
const cy = this.baseY + this.VIAL_H / 2;
|
||||
const D = 80;
|
||||
|
||||
if (this._shakeTween) { this._shakeTween.stop(); this._shakeTween = null; }
|
||||
this.container.setPosition(this.baseX, this.baseY);
|
||||
|
||||
// Blinding white flash
|
||||
const overlay = scene.add.rectangle(800, 450, 1600, 900, 0xffffee).setAlpha(0).setDepth(D + 20);
|
||||
scene.tweens.add({
|
||||
targets: overlay, alpha: { from: 0, to: 0.85 },
|
||||
duration: 200, yoyo: true, ease: 'Sine.easeInOut',
|
||||
onComplete: () => overlay.destroy(),
|
||||
});
|
||||
|
||||
// Golden glow outline surrounds the vial
|
||||
const glow = scene.add.graphics().setDepth(D - 1);
|
||||
glow.lineStyle(20, 0xffd700, 0.7);
|
||||
glow.strokeRoundedRect(
|
||||
cx - this.VIAL_W / 2 - 7, this.baseY - 7,
|
||||
this.VIAL_W + 14, this.VIAL_H + 14, this.VIAL_W / 2 + 4,
|
||||
);
|
||||
glow.setAlpha(0);
|
||||
scene.tweens.add({ targets: glow, alpha: 1, duration: 320, yoyo: true, onComplete: () => glow.destroy() });
|
||||
|
||||
// Gentle double-pulse of the vial before it explodes
|
||||
scene.tweens.add({
|
||||
targets: this.container, scaleX: 1.08, scaleY: 1.08,
|
||||
duration: 200, yoyo: true, ease: 'Sine.easeInOut',
|
||||
onComplete: () => {
|
||||
scene.tweens.add({
|
||||
targets: this.container, scaleX: 1.12, scaleY: 1.12,
|
||||
duration: 200, yoyo: true, ease: 'Sine.easeInOut',
|
||||
onComplete: () => this._doHolyExplosion(cx, cy, D),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// God rays fan out at 150 ms (before the explosion)
|
||||
scene.time.delayedCall(150, () => {
|
||||
const rays = scene.add.graphics().setDepth(D - 2);
|
||||
rays.setPosition(cx, cy);
|
||||
const numRays = 16;
|
||||
for (let i = 0; i < numRays; i++) {
|
||||
const angle = (Math.PI * 2 / numRays) * i;
|
||||
const len = 280;
|
||||
const hw = 0.12; // half-angle of each ray
|
||||
rays.fillStyle(0xfff0a0, i % 2 === 0 ? 0.55 : 0.28);
|
||||
rays.fillTriangle(
|
||||
0, 0,
|
||||
Math.cos(angle - hw) * len, Math.sin(angle - hw) * len,
|
||||
Math.cos(angle + hw) * len, Math.sin(angle + hw) * len,
|
||||
);
|
||||
}
|
||||
rays.setScale(0.05);
|
||||
scene.tweens.add({
|
||||
targets: rays, scaleX: 1.7, scaleY: 1.7, alpha: 0,
|
||||
duration: 1100, ease: 'Cubic.easeOut',
|
||||
onComplete: () => rays.destroy(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
_doHolyExplosion(cx, cy, D) {
|
||||
const scene = this.scene;
|
||||
|
||||
// Vial ascends and shrinks into heaven
|
||||
scene.tweens.add({
|
||||
targets: this.container,
|
||||
y: this.baseY - 190, alpha: 0, scaleX: 0.55, scaleY: 0.55,
|
||||
duration: 1400, ease: 'Cubic.easeOut',
|
||||
});
|
||||
|
||||
// Two golden expanding rings
|
||||
[{ color: 0xffd700, r: 18, delay: 0, toScale: 9, dur: 700 },
|
||||
{ color: 0xffffff, r: 12, delay: 80, toScale: 7, dur: 820 }].forEach(o => {
|
||||
const ring = scene.add.graphics().setDepth(D);
|
||||
ring.lineStyle(5, o.color, 1);
|
||||
ring.strokeCircle(cx, cy, o.r);
|
||||
scene.tweens.add({
|
||||
targets: ring, scaleX: o.toScale, scaleY: o.toScale, alpha: 0,
|
||||
delay: o.delay, duration: o.dur, ease: 'Expo.easeOut',
|
||||
onComplete: () => ring.destroy(),
|
||||
});
|
||||
});
|
||||
|
||||
// Radiant white burst from center
|
||||
const burst = scene.add.graphics().setDepth(D + 2);
|
||||
burst.fillStyle(0xffffff, 1);
|
||||
burst.fillCircle(cx, cy, 32);
|
||||
scene.tweens.add({
|
||||
targets: burst, scaleX: 5.5, scaleY: 5.5, alpha: 0,
|
||||
duration: 460, ease: 'Expo.easeOut', onComplete: () => burst.destroy(),
|
||||
});
|
||||
|
||||
// Golden diamond shards biased upward (upper-half arc + some sideways)
|
||||
const shardColors = [0xffd700, 0xffffa0, 0xffffff, 0xffe066, 0xffc000];
|
||||
for (let i = 0; i < 14; i++) {
|
||||
const angle = (Math.PI * 1.4 / 14) * i - Math.PI * 1.1;
|
||||
const dist = 100 + Math.random() * 210;
|
||||
const sz = 5 + Math.random() * 11;
|
||||
const shard = scene.add.graphics().setDepth(D + 1);
|
||||
shard.fillStyle(shardColors[i % shardColors.length], 1);
|
||||
// Diamond shape: top + bottom triangle
|
||||
shard.fillTriangle(0, -sz, -sz * 0.3, 0, sz * 0.3, 0);
|
||||
shard.fillTriangle(-sz * 0.3, 0, sz * 0.3, 0, 0, sz * 0.65);
|
||||
shard.setPosition(cx, cy).setRotation(Math.random() * Math.PI * 2);
|
||||
scene.tweens.add({
|
||||
targets: shard,
|
||||
x: cx + Math.cos(angle) * dist,
|
||||
y: cy + Math.sin(angle) * dist,
|
||||
rotation: shard.rotation + (Math.random() - 0.5) * 6,
|
||||
scaleX: 0.1, scaleY: 0.1, alpha: 0,
|
||||
duration: 1050 + Math.random() * 350,
|
||||
ease: 'Cubic.easeOut', delay: Math.random() * 80,
|
||||
onComplete: () => shard.destroy(),
|
||||
});
|
||||
}
|
||||
|
||||
// Golden sparkle particles
|
||||
const sparkChars = ['✦', '★', '✧', '✶', '+'];
|
||||
for (let i = 0; i < 24; i++) {
|
||||
const sp = scene.add.text(
|
||||
cx + (Math.random() - 0.5) * 30, cy + (Math.random() - 0.5) * 40,
|
||||
sparkChars[Math.floor(Math.random() * sparkChars.length)],
|
||||
{ fontSize: `${8 + Math.floor(Math.random() * 20)}px`, color: '#ffd700',
|
||||
stroke: '#ffffff', strokeThickness: 1 },
|
||||
).setOrigin(0.5).setDepth(D + 3).setAlpha(0);
|
||||
const pa = Math.random() * Math.PI * 2;
|
||||
const pd = 75 + Math.random() * 190;
|
||||
scene.tweens.add({
|
||||
targets: sp,
|
||||
x: sp.x + Math.cos(pa) * pd,
|
||||
y: sp.y + Math.sin(pa) * pd - 35,
|
||||
alpha: { from: 1, to: 0 }, scale: 1.7,
|
||||
duration: 1100 + Math.random() * 500,
|
||||
delay: Math.random() * 180, ease: 'Cubic.easeOut',
|
||||
onComplete: () => sp.destroy(),
|
||||
});
|
||||
}
|
||||
|
||||
// Large cross flashes and expands
|
||||
const cross = scene.add.text(cx, cy, '✝', {
|
||||
fontSize: '130px', color: '#ffffff',
|
||||
stroke: '#ffd700', strokeThickness: 5,
|
||||
shadow: { offsetX: 0, offsetY: 0, color: '#ffd700', blur: 30, fill: true },
|
||||
}).setOrigin(0.5).setDepth(D + 5).setAlpha(0).setScale(0.2);
|
||||
scene.tweens.add({
|
||||
targets: cross, alpha: 0.95, scale: 1.1, duration: 280, ease: 'Back.easeOut',
|
||||
onComplete: () => {
|
||||
scene.tweens.add({
|
||||
targets: cross, alpha: 0, scale: 2.0,
|
||||
duration: 750, ease: 'Cubic.easeOut',
|
||||
onComplete: () => cross.destroy(),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
// "GLORY" text rises and fades
|
||||
const gloryText = scene.add.text(cx, cy + 55, '✝ GLORY ✝', {
|
||||
fontSize: '20px', fontFamily: 'Georgia, serif',
|
||||
color: '#ffd700', stroke: '#ffffff', strokeThickness: 3,
|
||||
shadow: { offsetX: 0, offsetY: 0, color: '#ffd700', blur: 14, fill: true },
|
||||
}).setOrigin(0.5).setDepth(D + 5).setAlpha(0).setScale(0.4);
|
||||
scene.tweens.add({
|
||||
targets: gloryText, alpha: 1, scale: 1, delay: 260, duration: 400, ease: 'Back.easeOut',
|
||||
onComplete: () => {
|
||||
scene.tweens.add({
|
||||
targets: gloryText, alpha: 0, y: gloryText.y - 95,
|
||||
delay: 1300, duration: 1000, ease: 'Cubic.easeOut',
|
||||
onComplete: () => gloryText.destroy(),
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,11 +96,21 @@ export default class GameScene extends Phaser.Scene {
|
|||
// Listen for spin button events from UIScene via global event bus
|
||||
this.game.events.on('spin', () => this._triggerSpin(), this);
|
||||
|
||||
// Victory: play the appropriate one-shot video then stop
|
||||
// Victory: play the victory video and shatter the losing vial
|
||||
this.game.events.on('vial-winner', ({ winner }) => {
|
||||
this._gameOver = true;
|
||||
const key = winner.toLowerCase().includes('lord') ? 'lord-victory' : 'sin-victory';
|
||||
const lordWins = winner.toLowerCase().includes('lord');
|
||||
const key = lordWins ? 'lord-victory' : 'sin-victory';
|
||||
this._setVideo(key, false);
|
||||
|
||||
// Shatter the losing vial after a brief dramatic pause
|
||||
this.time.delayedCall(550, () => {
|
||||
if (lordWins) {
|
||||
this.sinVial.shatterEvil();
|
||||
} else {
|
||||
this.lordVial.shatterHoly();
|
||||
}
|
||||
});
|
||||
}, this);
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue