feat(dominion): add animated comet trails to playable hand cards

- Implement a visual effect using Phaser Graphics to draw animated "comet" trails along the perimeter of playable action and treasure cards.
- Add `perimPoint` helper to calculate coordinates along a rectangle's perimeter.
- Manage particle graphics and tweens via new `_handFxGraphics` and `_handFxTweens` arrays, ensuring proper cleanup during render cycles.
- Replace simple face highlighting with the new `_buildHandFxItem` method for enhanced visual feedback.
This commit is contained in:
Brian Fertig 2026-05-27 17:32:58 -06:00
parent 285d992915
commit 41466e14f3
1 changed files with 75 additions and 2 deletions

View File

@ -18,6 +18,20 @@ const CX = GAME_WIDTH / 2;
const D = { bg: -1, supply: 5, inplay: 6, hand: 10, hud: 15, portrait: 20, prompt: 40, modal: 80, popup: 95 };
// Returns the screen (x, y) at distance d along the clockwise perimeter of a
// rectangle with top-left corner (left, top) and dimensions W×H.
function perimPoint(d, W, H, left, top) {
const perim = 2 * (W + H);
d = ((d % perim) + perim) % perim;
if (d < W) return [left + d, top ];
d -= W;
if (d < H) return [left + W, top + d];
d -= H;
if (d < W) return [left + W - d, top + H];
d -= W;
return [left, top + H - d];
}
const SUPPLY_W = 100, SUPPLY_H = 144;
const HAND_W = 132, HAND_H = 190;
const PLAY_W = 78, PLAY_H = 112;
@ -84,6 +98,8 @@ export default class DominionGame extends Phaser.Scene {
this._arrowSeat = null;
this._boughtThisTurn = false;
this._suppressTurnUi = false;
this._handFxGraphics = [];
this._handFxTweens = [];
}
create() {
@ -404,6 +420,7 @@ export default class DominionGame extends Phaser.Scene {
// ── Render (dynamic layer) ───────────────────────────────────────────────────
render() {
this._clearHandFx();
if (this.hoverTimer) { this.hoverTimer.remove(); this.hoverTimer = null; }
this.hideHover();
this.dynamicLayer?.destroy(true);
@ -554,8 +571,8 @@ export default class DominionGame extends Phaser.Scene {
const hs = { iid: c.iid, id: c.id, def, x, baseY, face, hit, isPlayableAction, isPlayableTreasure };
this.handSprites.push(hs);
if (isPlayableAction) this.highlightFace(face, COLORS.accent);
else if (isPlayableTreasure) this.highlightFace(face, COLORS.gold);
if (isPlayableAction) this._buildHandFxItem(hs, COLORS.accent);
else if (isPlayableTreasure) this._buildHandFxItem(hs, COLORS.gold);
hit.on('pointerdown', (ptr) => {
if (this._animating) return;
@ -577,6 +594,62 @@ export default class DominionGame extends Phaser.Scene {
face.addAt(glow, 0);
}
_clearHandFx() {
this._handFxTweens.forEach(t => t.destroy());
this._handFxGraphics.forEach(g => g.destroy());
this._handFxTweens = [];
this._handFxGraphics = [];
}
_buildHandFxItem(hs, color) {
const gfx = this.add.graphics();
gfx.setDepth(D.hand + 25);
this._handFxGraphics.push(gfx);
const W = HAND_W, H = HAND_H;
const perim = 2 * (W + H);
const TAIL_PX = 90;
const SEGMENTS = 20;
const COMETS = 2;
const tracker = { t: 0 };
const tween = this.tweens.add({
targets: tracker,
t: { from: 0, to: 1 },
duration: 1200,
repeat: -1,
ease: 'Linear',
onUpdate: () => {
const left = hs.x - W / 2;
const top = hs.face.y - H / 2;
gfx.clear();
gfx.lineStyle(2, color, 0.2);
gfx.strokeRect(left, top, W, H);
for (let c = 0; c < COMETS; c++) {
const headDist = ((tracker.t + c / COMETS) % 1) * perim;
for (let seg = 0; seg < SEGMENTS; seg++) {
const frac = seg / SEGMENTS;
const d1 = ((headDist - (1 - frac) * TAIL_PX + perim * 100) % perim);
const d2 = ((headDist - (1 - (seg + 1) / SEGMENTS) * TAIL_PX + perim * 100) % perim);
const [x1, y1] = perimPoint(d1, W, H, left, top);
const [x2, y2] = perimPoint(d2, W, H, left, top);
const alpha = 0.1 + frac * 0.85;
const lineColor = frac > 0.85 ? 0xffffff : color;
const lineWidth = frac > 0.85 ? 4 : 2;
gfx.lineStyle(lineWidth, lineColor, alpha);
gfx.beginPath();
gfx.moveTo(x1, y1);
gfx.lineTo(x2, y2);
gfx.strokePath();
}
}
},
});
this._handFxTweens.push(tween);
}
renderCounts() {
const gs = this.gs;
// Human deck — face-down card pile with count badge