665 lines
23 KiB
JavaScript
665 lines
23 KiB
JavaScript
export class VialDisplay {
|
|
constructor(scene, x, y, label, fillColor, borderColor) {
|
|
this.scene = scene;
|
|
this.baseX = x;
|
|
this.baseY = y;
|
|
this.label = label;
|
|
this.fillColor = fillColor;
|
|
this.borderColor = borderColor;
|
|
|
|
this.VIAL_W = 62;
|
|
this.VIAL_H = 370;
|
|
this.MAX = 2000;
|
|
this.currentAmount = 0;
|
|
this._shakeTween = null;
|
|
this._shakeApplied = 0; // last threshold at which shake was started
|
|
this._won = false;
|
|
|
|
this._build();
|
|
}
|
|
|
|
_build() {
|
|
const { scene, baseX, baseY, VIAL_W, VIAL_H, fillColor, borderColor } = this;
|
|
const R = VIAL_W / 2; // radius for rounded top cap
|
|
|
|
this.container = scene.add.container(baseX, baseY);
|
|
|
|
// Dark glass interior
|
|
const glassBg = scene.add.graphics();
|
|
glassBg.fillStyle(0x060214, 0.95);
|
|
glassBg.fillRoundedRect(-VIAL_W / 2, 0, VIAL_W, VIAL_H, { tl: R, tr: R, bl: 6, br: 6 });
|
|
this.container.add(glassBg);
|
|
|
|
// Fill graphic — redrawn dynamically
|
|
this.fillGfx = scene.add.graphics();
|
|
this.container.add(this.fillGfx);
|
|
|
|
// Glass border rendered on top of fill so it stays crisp
|
|
const border = scene.add.graphics();
|
|
border.lineStyle(3, borderColor, 0.9);
|
|
border.strokeRoundedRect(-VIAL_W / 2, 0, VIAL_W, VIAL_H, { tl: R, tr: R, bl: 6, br: 6 });
|
|
// Outer glow ring
|
|
border.lineStyle(10, borderColor, 0.18);
|
|
border.strokeRoundedRect(-VIAL_W / 2 - 5, -5, VIAL_W + 10, VIAL_H + 10, { tl: R + 5, tr: R + 5, bl: 10, br: 10 });
|
|
// Left-side glass highlight
|
|
border.lineStyle(2, 0xffffff, 0.22);
|
|
border.beginPath();
|
|
border.moveTo(-VIAL_W / 2 + 6, R + 6);
|
|
border.lineTo(-VIAL_W / 2 + 6, VIAL_H - 12);
|
|
border.strokePath();
|
|
this.container.add(border);
|
|
|
|
// Tick marks at $500, $1000, $1500, $2000
|
|
const ticks = scene.add.graphics();
|
|
[500, 1000, 1500, 2000].forEach(amt => {
|
|
const ty = VIAL_H - (amt / this.MAX) * VIAL_H;
|
|
ticks.lineStyle(amt === 2000 ? 2 : 1, borderColor, amt === 2000 ? 0.6 : 0.25);
|
|
ticks.beginPath();
|
|
ticks.moveTo(-VIAL_W / 2 + 4, ty);
|
|
ticks.lineTo( VIAL_W / 2 - 4, ty);
|
|
ticks.strokePath();
|
|
|
|
const lbl = scene.add.text(VIAL_W / 2 + 7, ty, `$${amt}`, {
|
|
fontSize: amt === 2000 ? '12px' : '10px',
|
|
fontFamily: 'Georgia, serif',
|
|
color: amt === 2000 ? '#ffd700' : '#4a5a6a',
|
|
}).setOrigin(0, 0.5);
|
|
this.container.add(lbl);
|
|
});
|
|
this.container.add(ticks);
|
|
|
|
// Floating amount text shown just above the fill surface
|
|
this.amtText = scene.add.text(0, VIAL_H - 6, '$0', {
|
|
fontSize: '11px',
|
|
fontFamily: 'Georgia, serif',
|
|
fontStyle: 'bold',
|
|
color: '#ffffff',
|
|
stroke: '#000000',
|
|
strokeThickness: 2,
|
|
}).setOrigin(0.5, 1).setAlpha(0);
|
|
this.container.add(this.amtText);
|
|
|
|
// Vial label below
|
|
this.container.add(
|
|
scene.add.text(0, VIAL_H + 14, this.label, {
|
|
fontSize: '15px',
|
|
fontFamily: 'Georgia, serif',
|
|
color: '#c8a87e',
|
|
stroke: '#000000',
|
|
strokeThickness: 2,
|
|
}).setOrigin(0.5, 0)
|
|
);
|
|
|
|
// Goal text
|
|
this.container.add(
|
|
scene.add.text(0, VIAL_H + 36, 'GOAL: $2,000', {
|
|
fontSize: '10px',
|
|
fontFamily: 'Georgia, serif',
|
|
color: '#3a4a5a',
|
|
}).setOrigin(0.5, 0)
|
|
);
|
|
}
|
|
|
|
_drawFill(amount) {
|
|
const { VIAL_W, VIAL_H, MAX, fillColor } = this;
|
|
const capped = Math.min(amount, MAX);
|
|
const fillH = (capped / MAX) * VIAL_H;
|
|
|
|
this.fillGfx.clear();
|
|
if (fillH < 2) {
|
|
this.amtText.setAlpha(0);
|
|
return;
|
|
}
|
|
|
|
const fillY = VIAL_H - fillH;
|
|
const R = VIAL_W / 2;
|
|
|
|
// Main fill body
|
|
this.fillGfx.fillStyle(fillColor, 0.88);
|
|
if (fillY < R) {
|
|
// Near the rounded top — match the cap shape
|
|
this.fillGfx.fillRoundedRect(-VIAL_W / 2 + 3, fillY, VIAL_W - 6, fillH, { tl: R - 3, tr: R - 3, bl: 4, br: 4 });
|
|
} else {
|
|
this.fillGfx.fillRoundedRect(-VIAL_W / 2 + 3, fillY, VIAL_W - 6, fillH, 4);
|
|
}
|
|
|
|
// Bright liquid surface shimmer
|
|
this.fillGfx.fillStyle(0xffffff, 0.38);
|
|
this.fillGfx.fillRoundedRect(-VIAL_W / 2 + 5, fillY, VIAL_W - 10, 5, 3);
|
|
|
|
// Reposition floating amount label just above the surface
|
|
this.amtText.setY(Math.max(fillY - 4, 4));
|
|
this.amtText.setAlpha(1);
|
|
this.amtText.setText(`$${Math.round(capped)}`);
|
|
}
|
|
|
|
/**
|
|
* Animate mini coins flying from world position (fromX, fromY) into the vial,
|
|
* then animate the fill rising to targetAmount.
|
|
*/
|
|
animateUpdate(targetAmount, fromX, fromY, onComplete) {
|
|
const scene = this.scene;
|
|
const toAmount = Math.min(targetAmount, this.MAX);
|
|
|
|
if (toAmount <= this.currentAmount || this._won) {
|
|
if (onComplete) onComplete();
|
|
return;
|
|
}
|
|
|
|
const aimX = this.baseX;
|
|
const aimY = this.baseY + this.VIAL_H * 0.78;
|
|
const numCoins = Phaser.Math.Between(5, 8);
|
|
let landed = 0;
|
|
|
|
for (let i = 0; i < numCoins; i++) {
|
|
const coin = scene.add.graphics();
|
|
coin.fillStyle(this.fillColor, 1);
|
|
coin.fillCircle(0, 0, 7);
|
|
coin.lineStyle(1.5, 0xffffff, 0.5);
|
|
coin.strokeCircle(0, 0, 7);
|
|
coin.setPosition(fromX + Phaser.Math.Between(-14, 14), fromY);
|
|
coin.setScale(0.1).setAlpha(0);
|
|
|
|
scene.tweens.add({
|
|
targets: coin,
|
|
alpha: 1,
|
|
scale: 1,
|
|
duration: 140,
|
|
delay: i * 80,
|
|
ease: 'Back.easeOut',
|
|
onComplete: () => {
|
|
scene.tweens.add({
|
|
targets: coin,
|
|
x: aimX + Phaser.Math.Between(-6, 6),
|
|
y: aimY,
|
|
scale: 0.3,
|
|
alpha: 0.7,
|
|
duration: 480 + i * 50,
|
|
ease: 'Cubic.easeIn',
|
|
onComplete: () => {
|
|
coin.destroy();
|
|
if (++landed === numCoins) this._animateFill(toAmount, onComplete);
|
|
},
|
|
});
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
_animateFill(toAmount, onComplete) {
|
|
const scene = this.scene;
|
|
const proxy = { value: this.currentAmount };
|
|
|
|
scene.tweens.add({
|
|
targets: proxy,
|
|
value: toAmount,
|
|
duration: 1100,
|
|
ease: 'Cubic.easeOut',
|
|
onUpdate: () => {
|
|
this.currentAmount = proxy.value;
|
|
this._drawFill(proxy.value);
|
|
// Start shaking the first time the fill crosses $1500
|
|
if (proxy.value >= 1500 && this._shakeApplied < 1500) {
|
|
this._shakeApplied = 1500;
|
|
this._applyShake(1500);
|
|
}
|
|
},
|
|
onComplete: () => {
|
|
this.currentAmount = toAmount;
|
|
this._drawFill(toAmount);
|
|
// Update shake to final intensity
|
|
if (toAmount >= 1500) this._applyShake(toAmount);
|
|
if (toAmount >= this.MAX && !this._won) {
|
|
this._won = true;
|
|
this._celebrateWin();
|
|
}
|
|
if (onComplete) onComplete();
|
|
},
|
|
});
|
|
}
|
|
|
|
_applyShake(amount) {
|
|
if (amount < 1500) return;
|
|
|
|
if (this._shakeTween) {
|
|
this._shakeTween.stop();
|
|
this._shakeTween = null;
|
|
this.container.setX(this.baseX);
|
|
}
|
|
|
|
const pct = Math.min((amount - 1500) / 500, 1);
|
|
const intensity = 3 + pct * 15; // 3px at $1500 → 18px at $2000
|
|
const speed = Math.max(22, 100 - pct * 82); // 100ms → 18ms (faster = more frantic)
|
|
|
|
this._shakeTween = this.scene.tweens.add({
|
|
targets: this.container,
|
|
x: { from: this.baseX - intensity, to: this.baseX + intensity },
|
|
duration: speed,
|
|
yoyo: true,
|
|
repeat: -1,
|
|
ease: 'Sine.easeInOut',
|
|
});
|
|
}
|
|
|
|
_celebrateWin() {
|
|
const scene = this.scene;
|
|
|
|
// Stop shaking and snap back to position
|
|
if (this._shakeTween) {
|
|
this._shakeTween.stop();
|
|
this._shakeTween = null;
|
|
}
|
|
this.container.setX(this.baseX);
|
|
|
|
// Rapid flash on the fill
|
|
scene.tweens.add({
|
|
targets: this.fillGfx,
|
|
alpha: { from: 1, to: 0.1 },
|
|
duration: 90,
|
|
yoyo: true,
|
|
repeat: 7,
|
|
});
|
|
|
|
// Expanding burst ring from vial center
|
|
const burst = scene.add.graphics();
|
|
burst.lineStyle(6, this.borderColor, 1);
|
|
burst.strokeCircle(0, 0, 20);
|
|
burst.setPosition(this.baseX, this.baseY + this.VIAL_H / 2);
|
|
scene.tweens.add({
|
|
targets: burst,
|
|
scaleX: 6, scaleY: 6,
|
|
alpha: 0,
|
|
duration: 700,
|
|
ease: 'Cubic.easeOut',
|
|
onComplete: () => burst.destroy(),
|
|
});
|
|
|
|
// Winner badge punches in above the vial
|
|
const badge = scene.add.text(this.baseX, this.baseY - 22, '✦ FULL ✦', {
|
|
fontSize: '16px',
|
|
fontFamily: 'Georgia, serif',
|
|
fontStyle: 'bold',
|
|
color: '#ffd700',
|
|
stroke: '#000000',
|
|
strokeThickness: 3,
|
|
shadow: { offsetX: 0, offsetY: 0, color: '#ffd700', blur: 14, fill: true },
|
|
}).setOrigin(0.5, 0.5).setAlpha(0).setScale(0.3);
|
|
|
|
scene.tweens.add({
|
|
targets: badge,
|
|
alpha: 1,
|
|
scale: 1,
|
|
y: this.baseY - 40,
|
|
duration: 500,
|
|
ease: 'Back.easeOut',
|
|
});
|
|
|
|
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(),
|
|
});
|
|
},
|
|
});
|
|
}
|
|
}
|