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(), }); }, }); } }