feat(phase10): add character voiceovers and enforce staging rules

- Add speech assets and configuration for Brad and Mario
- Update opponents.json with new speech arrays for intro, happy, and upset states
- Prevent discarding cards while cards are staged in Phase 10
- Add visual feedback (_shakeStagingButtons) when staging rules are violated
- Add _hasStagedCards helper to check staging group state
- Move staged cards back to hand if user attempts to discard during staging
This commit is contained in:
Brian Fertig 2026-05-22 20:05:05 -06:00
parent 3fcfbcc921
commit 6856baa395
27 changed files with 70 additions and 2 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@ -30,7 +30,27 @@
"id": "brad",
"spriteIndex": 1,
"name": "Brad",
"bio": "Came for the honey, stayed for the salmon!"
"bio": "Came for the honey, stayed for the salmon!",
"speech": {
"intro": [
"brad-intro-01",
"brad-intro-02"
],
"happy": [
"brad-happy-01",
"brad-happy-02",
"brad-happy-03",
"brad-happy-04",
"brad-happy-05"
],
"upset": [
"brad-upset-01",
"brad-upset-02",
"brad-upset-03",
"brad-upset-04",
"brad-upset-05"
]
}
},
{
"id": "victor",
@ -88,7 +108,20 @@
"id": "mario",
"spriteIndex": 4,
"name": "Mario",
"bio": "Can you find your way out of my maze of puzzles, games and fun!"
"bio": "Can you find your way out of my maze of puzzles, games and fun!",
"speech": {
"intro": [
"mario-intro-01"
],
"happy": [
"mario-happy-01",
"mario-happy-02"
],
"upset": [
"mario-upset-01",
"mario-upset-02"
]
}
},
{
"id": "ethel",

View File

@ -599,6 +599,15 @@ export default class Phase10Game extends Phaser.Scene {
// ── Discard path ──────────────────────────────────────────────────────
if (dropTarget?.type === 'discard') {
if (this._hasStagedCards()) {
this.tweens.killTweensOf(card);
this._settleNonDraggedCards();
this.dragState = null;
this.tweens.add({ targets: card, x: 460 + ds.cardIdx * HAND_SPREAD, y: 990, rotation: 0, scaleX: 1, scaleY: 1, duration: 200, ease: 'Back.easeOut' });
this.setStatus('Submit or clear your phase first — you have cards staged.');
this._shakeStagingButtons();
return;
}
const handIdx = ds.cardIdx;
const handCard = this.gs.players[0].hand[handIdx];
this.tweens.killTweensOf(card);
@ -1305,6 +1314,11 @@ export default class Phase10Game extends Phaser.Scene {
if (!this.isLocalTurn() || this.animating) return;
// If we have a card selected and we've already drawn, discarding ends turn.
if (this.gs.drawnThisTurn && this.selectedHandIdx != null) {
if (this._hasStagedCards()) {
this.setStatus('Submit or clear your phase first — you have cards staged.');
this._shakeStagingButtons();
return;
}
const handIdx = this.selectedHandIdx;
this.selectedHandIdx = null;
this.clearHighlights();
@ -1737,6 +1751,27 @@ export default class Phase10Game extends Phaser.Scene {
// ── HUD helpers ─────────────────────────────────────────────────────────
_hasStagedCards() {
return this.stagingGroups?.some((g) => g.slots.some((s) => s !== null)) ?? false;
}
_shakeStagingButtons() {
const targets = [this.submitBtn, this.clearBtn].filter(Boolean);
if (!targets.length) return;
for (const btn of targets) {
const ox = btn.x;
this.tweens.add({
targets: btn,
x: { from: ox - 8, to: ox + 8 },
duration: 60,
ease: 'Sine.easeInOut',
yoyo: true,
repeat: 3,
onComplete: () => { btn.x = ox; },
});
}
}
setStatus(s) {
if (!this.statusText) return;
this.statusText.setText(s);