From 74d5470d1104369c0c1c5f53a0111ccb256d25f0 Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Sun, 7 Jun 2026 19:05:04 -0600 Subject: [PATCH] **feat(triominoes): animate drawn tiles from pool to player portrait** Add `animateDrawTile()` to tween a face-down triangle from the pool area to the drawing player's portrait over 1.2 seconds. Integrates the animation into both the human draw flow (`onPoolClick`) and the AI draw loop (`runAITurn`), ensuring visual feedback before the game state updates and the hand refreshes. --- public/src/games/triominoes/TriominoesGame.js | 55 +++++++++++++++++-- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/public/src/games/triominoes/TriominoesGame.js b/public/src/games/triominoes/TriominoesGame.js index c3a7812..66ba21f 100644 --- a/public/src/games/triominoes/TriominoesGame.js +++ b/public/src/games/triominoes/TriominoesGame.js @@ -527,6 +527,49 @@ export default class TriominoesGame extends Phaser.Scene { } } + // ─── Draw animation ───────────────────────────────────────────────────── + // Animate a face-down triangle from the pool to the player's portrait. + // Returns a promise that resolves when the animation completes. + animateDrawTile(seat) { + return new Promise((resolve) => { + const portrait = this.portraitCtrls[seat]; + if (!portrait) { resolve(); return; } + const px = portrait.x; + const py = portrait.y; + const poolX = POOL_X; + const poolY = POOL_Y; + + const size = HAND_BASE * 0.45; + const h = Math.round(size * 0.8660254); + const triPts = [[0, -h * 2 / 3], [size / 2, h / 3], [-size / 2, h / 3]]; + + const container = this.add.container(poolX, poolY).setDepth(DEPTH.drag); + const gfx = this.add.graphics(); + // Face-down: dark fill, subtle border, small center dot. + gfx.fillStyle(COLORS.panel, 1); + gfx.fillTriangle(triPts[0][0], triPts[0][1], triPts[1][0], triPts[1][1], triPts[2][0], triPts[2][1]); + gfx.lineStyle(2, COLORS.accent, 0.9); + gfx.strokeTriangle(triPts[0][0], triPts[0][1], triPts[1][0], triPts[1][1], triPts[2][0], triPts[2][1]); + gfx.fillStyle(COLORS.accent, 0.35); + gfx.fillCircle(0, 0, Math.max(3, size * 0.06)); + container.add(gfx); + + container.setScale(1).setAngle(0); + + this.tweens.add({ + targets: container, + x: px, + y: py, + duration: 1200, + ease: 'Cubic.easeInOut', + onComplete: () => { + container.destroy(); + resolve(); + }, + }); + }); + } + // ─── AI tile animation ────────────────────────────────────────────────── // Create a small triangle at the opponent's portrait and tween it to the // board cell over 1.2 s, rotating and scaling as it goes. Returns a promise @@ -625,10 +668,13 @@ export default class TriominoesGame extends Phaser.Scene { if (!this._poolActive) return; this.showPoolClickable(false); playSound(this, SFX.CARD_DEAL); - this.gs = drawTile(this.gs); - this.refresh(); - // Re-evaluate: a drawn tile may now be playable. - this.time.delayedCall(260, () => this.beginHumanTurn()); + // Animate tile from pool → player's portrait. + this.animateDrawTile(0).then(() => { + this.gs = drawTile(this.gs); + this.refresh(); + // Re-evaluate: a drawn tile may now be playable. + this.time.delayedCall(260, () => this.beginHumanTurn()); + }); } showPoolClickable(on) { @@ -656,6 +702,7 @@ export default class TriominoesGame extends Phaser.Scene { if (canDraw(this.gs) && this.gs.players[seat].draws < MAX_DRAWS) { await this.delay(360); playSound(this, SFX.CARD_DEAL); + await this.animateDrawTile(seat); this.gs = drawTile(this.gs); this.refresh(); continue;