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.
This commit is contained in:
Brian Fertig 2026-06-06 20:29:58 -06:00
parent 23c0804a54
commit 701b4f75e6
2 changed files with 48 additions and 4 deletions

View File

@ -559,8 +559,13 @@ export default class FarkelGame extends Phaser.Scene {
if (this.busy || !this.isHumanTurn()) return; if (this.busy || !this.isHumanTurn()) return;
if (this.gs.phase === 'awaitPick' && !this.commitSelection()) return; if (this.gs.phase === 'awaitPick' && !this.commitSelection()) return;
if (this.gs.phase !== 'awaitDecision') 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); playSound(this, SFX.PENCIL_WRITE);
if (bankable > 0) {
await this.animateBank(seat, bankable);
this.gs.players[seat].score += bankable;
}
this.render(); this.render();
this.advance(); this.advance();
} }
@ -629,8 +634,12 @@ export default class FarkelGame extends Phaser.Scene {
await this.delay(360); await this.delay(360);
if (decideReroll(this.gs, skill)) continue; if (decideReroll(this.gs, skill)) continue;
const seat = this.gs.current; const seat = this.gs.current;
bank(this.gs); const { bankable } = bank(this.gs);
playSound(this, SFX.PENCIL_WRITE); 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) { if (this.gs.players[seat].score >= 1000) {
this.portraitCtrls[seat]?.controller?.playEmotion?.('happy'); 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 ────────────────────────────────────────────────────────────────── // ── game over ──────────────────────────────────────────────────────────────────
showGameOver() { showGameOver() {
if (this.gameOverShown) return; if (this.gameOverShown) return;

View File

@ -191,13 +191,17 @@ export function applySetAside(state, indices) {
} }
// Bank the turn total (subject to the on-board minimum) and pass play. // 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) { export function bank(state) {
const p = state.players[state.current]; const p = state.players[state.current];
const t = state.turn; const t = state.turn;
const bankable = (p.onBoard || t.kept >= ON_BOARD_MIN) ? t.kept : 0; 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); advanceTurn(state);
return state; return { bankable, scoreBefore, scoreAfter };
} }
// Forfeit the turn total and pass play. // Forfeit the turn total and pass play.