From 701b4f75e69139918c80c57ff8669b6df0278def Mon Sep 17 00:00:00 2001 From: Brian Fertig Date: Sat, 6 Jun 2026 20:29:58 -0600 Subject: [PATCH] feat: add visual animation when banking scores in Farkel Refactor bank() to return bankable amount instead of mutating state, enabling the UI layer to handle score updates and display a floating +score animation that slides to the player's score pad. Applies to both human and AI turns. --- public/src/games/farkel/FarkelGame.js | 44 ++++++++++++++++++++++++-- public/src/games/farkel/FarkelLogic.js | 8 +++-- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/public/src/games/farkel/FarkelGame.js b/public/src/games/farkel/FarkelGame.js index 956a4af..31a61ca 100644 --- a/public/src/games/farkel/FarkelGame.js +++ b/public/src/games/farkel/FarkelGame.js @@ -559,8 +559,13 @@ export default class FarkelGame extends Phaser.Scene { if (this.busy || !this.isHumanTurn()) return; if (this.gs.phase === 'awaitPick' && !this.commitSelection()) return; if (this.gs.phase !== 'awaitDecision') return; - bank(this.gs); + const seat = this.gs.current; + const { bankable } = bank(this.gs); playSound(this, SFX.PENCIL_WRITE); + if (bankable > 0) { + await this.animateBank(seat, bankable); + this.gs.players[seat].score += bankable; + } this.render(); this.advance(); } @@ -629,8 +634,12 @@ export default class FarkelGame extends Phaser.Scene { await this.delay(360); if (decideReroll(this.gs, skill)) continue; const seat = this.gs.current; - bank(this.gs); + const { bankable } = bank(this.gs); playSound(this, SFX.PENCIL_WRITE); + if (bankable > 0) { + await this.animateBank(seat, bankable); + this.gs.players[seat].score += bankable; + } if (this.gs.players[seat].score >= 1000) { this.portraitCtrls[seat]?.controller?.playEmotion?.('happy'); } @@ -659,6 +668,37 @@ export default class FarkelGame extends Phaser.Scene { }); } + animateBank(seat, banked) { + return new Promise((resolve) => { + playSound(this, SFX.CASINO_WIN); + const row = this.scratchRows[seat]; + const destX = PAPER_X + PAPER_W - 22; + const destY = row.y; + + // Create floating text at dice area + const label = this.add.text(TRAY_CX, TRAY_CY, `+${banked}`, { + fontFamily: 'Righteous', fontSize: '80px', color: COLORS.goldHex, + }).setOrigin(0.5).setDepth(DEPTH.toast); + + // Hold for 1 second + this.time.delayedCall(1000, () => { + // Animate to score pad + this.tweens.add({ + targets: label, + x: destX, + y: destY, + scale: 0.5, + duration: 1000, + ease: 'Cubic.Out', + onComplete: () => { + label.destroy(); + resolve(); + }, + }); + }); + }); + } + // ── game over ────────────────────────────────────────────────────────────────── showGameOver() { if (this.gameOverShown) return; diff --git a/public/src/games/farkel/FarkelLogic.js b/public/src/games/farkel/FarkelLogic.js index 6d76924..e8b621d 100644 --- a/public/src/games/farkel/FarkelLogic.js +++ b/public/src/games/farkel/FarkelLogic.js @@ -191,13 +191,17 @@ export function applySetAside(state, indices) { } // Bank the turn total (subject to the on-board minimum) and pass play. +// Returns { bankable, scoreBefore, scoreAfter } so the caller can +// handle score updates (e.g. for animations). Does NOT mutate state.score. export function bank(state) { const p = state.players[state.current]; const t = state.turn; const bankable = (p.onBoard || t.kept >= ON_BOARD_MIN) ? t.kept : 0; - if (bankable > 0) { p.score += bankable; p.onBoard = true; } + const scoreBefore = p.score; + const scoreAfter = scoreBefore + bankable; + if (bankable > 0) p.onBoard = true; advanceTurn(state); - return state; + return { bankable, scoreBefore, scoreAfter }; } // Forfeit the turn total and pass play.