Virtue-Slots/objects/VialDisplay.js

300 lines
8.8 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 });
}
}