feat: enhance GoFish and Parchisi game UX with initial pair animations and pawn movement hints
- GoFish: Display animated banners when players are dealt starting pairs at the beginning of the game. - Parchisi: Add pulsing glow and expanding ripple visual cues to indicate movable pawns. - Parchisi: Implement click-outside dismissal for pawn selection highlights to improve interaction clarity.
This commit is contained in:
parent
9c3774fb1f
commit
fcee81a9d6
Binary file not shown.
|
|
@ -356,11 +356,35 @@ export default class GoFishGame extends Phaser.Scene {
|
|||
this.time.delayedCall(doneAt, () => {
|
||||
this.scatterPool(finalState.pool);
|
||||
this.gs = finalState;
|
||||
this.animating = false;
|
||||
this.renderAll();
|
||||
this.updateStatus();
|
||||
const initialPairs = (finalState.initialDealPairs ?? []).filter((e) => e.pairedCards.length >= 2);
|
||||
if (initialPairs.length > 0) {
|
||||
this.playInitialDealPairs(initialPairs, 0, () => {
|
||||
this.animating = false;
|
||||
this.maybeStartAITurn();
|
||||
});
|
||||
} else {
|
||||
this.animating = false;
|
||||
this.maybeStartAITurn();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
playInitialDealPairs(pairs, idx, onComplete) {
|
||||
if (idx >= pairs.length) { onComplete(); return; }
|
||||
const { seat, pairedCards } = pairs[idx];
|
||||
const name = this.opponentName(seat);
|
||||
const rank = pairedCards[0].rank === 'T' ? '10' : pairedCards[0].rank;
|
||||
const pairCount = pairedCards.length / 2;
|
||||
const label = pairCount === 1
|
||||
? `${name} dealt a starting pair of ${rank}s!`
|
||||
: `${name} dealt ${pairCount} starting pairs!`;
|
||||
this.showBanner(label);
|
||||
this.animatePairedCards(seat, pairedCards, () => {
|
||||
this.hideBanner();
|
||||
this.time.delayedCall(250, () => this.playInitialDealPairs(pairs, idx + 1, onComplete));
|
||||
});
|
||||
}
|
||||
|
||||
// ── Card sprite factory ───────────────────────────────────────────────────
|
||||
|
|
|
|||
|
|
@ -103,9 +103,13 @@ export function createInitialState({ playerCount = 4, seed } = {}) {
|
|||
winnerSeats: [],
|
||||
seed: seed ?? null,
|
||||
turnCount: 0,
|
||||
initialDealPairs: [],
|
||||
};
|
||||
// Any starting pairs are scored immediately.
|
||||
for (const p of state.players) collectPairs(p, state);
|
||||
for (const p of state.players) {
|
||||
const { count, pairedCards } = collectPairs(p, state);
|
||||
if (count > 0) state.initialDealPairs.push({ seat: p.seat, pairedCards });
|
||||
}
|
||||
// Edge: if a player was dealt all duplicates and now has 0 cards, refill.
|
||||
for (const p of state.players) ensureHasCards(state, p.seat);
|
||||
checkGameOver(state);
|
||||
|
|
|
|||
|
|
@ -164,6 +164,7 @@ export default class ParchisiGame extends Phaser.Scene {
|
|||
this.buildTurnIndicator();
|
||||
this.buildPawns();
|
||||
this.initGame();
|
||||
this.buildDismissHandler();
|
||||
}
|
||||
|
||||
buildPlayfield() {
|
||||
|
|
@ -594,6 +595,20 @@ export default class ParchisiGame extends Phaser.Scene {
|
|||
return this.add.container(x, y, [g]);
|
||||
}
|
||||
|
||||
buildDismissHandler() {
|
||||
this.input.on('pointerdown', (_ptr, gameObjects) => {
|
||||
if (this.selectedPawnIdx === null || this.animating) return;
|
||||
const hitRelevant = gameObjects.some((o) =>
|
||||
this.highlightObjs.includes(o) ||
|
||||
PCOLORS.some((c) => this.pawnObjs[c]?.includes(o))
|
||||
);
|
||||
if (!hitRelevant) {
|
||||
this.clearHighlights();
|
||||
this.showMovablePawnHints();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ── Game flow ─────────────────────────────────────────────────────────────
|
||||
initGame() {
|
||||
this.clearHighlights();
|
||||
|
|
@ -695,6 +710,7 @@ export default class ParchisiGame extends Phaser.Scene {
|
|||
this.time.delayedCall(600, () => this.runAITurnMoves());
|
||||
} else {
|
||||
this.setStatus('Choose a pawn to move');
|
||||
this.showMovablePawnHints();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -794,6 +810,7 @@ export default class ParchisiGame extends Phaser.Scene {
|
|||
}
|
||||
this.setStatus('Choose a pawn');
|
||||
this.updateButtons();
|
||||
this.showMovablePawnHints();
|
||||
return;
|
||||
}
|
||||
if (this.gs.phase === 'roll') {
|
||||
|
|
@ -962,6 +979,31 @@ export default class ParchisiGame extends Phaser.Scene {
|
|||
this.selectedPawnIdx = null;
|
||||
}
|
||||
|
||||
showMovablePawnHints() {
|
||||
const moves = getValidMoves(this.gs);
|
||||
const movable = [...new Set(moves.map((m) => m.pawnIdx))];
|
||||
for (const pawnIdx of movable) {
|
||||
const obj = this.pawnObjs['red'][pawnIdx];
|
||||
const px = obj.x;
|
||||
const py = obj.y;
|
||||
|
||||
// Static glow ring — pulses alpha
|
||||
const glow = this.add.graphics().setDepth(DEPTH.highlight);
|
||||
glow.lineStyle(2.5, 0x00e5b0, 0.9);
|
||||
glow.strokeCircle(px, py, PAWN_R + 6);
|
||||
this.tweens.add({ targets: glow, alpha: { from: 0.9, to: 0.25 }, duration: 550, yoyo: true, repeat: -1, ease: 'Sine.easeInOut' });
|
||||
|
||||
// Expanding ripple ring
|
||||
const ripple = this.add.graphics().setDepth(DEPTH.highlight);
|
||||
ripple.setPosition(px, py);
|
||||
ripple.lineStyle(2, 0x00e5b0, 0.75);
|
||||
ripple.strokeCircle(0, 0, PAWN_R + 6);
|
||||
this.tweens.add({ targets: ripple, scaleX: 2.0, scaleY: 2.0, alpha: 0, duration: 950, repeat: -1, ease: 'Cubic.easeOut' });
|
||||
|
||||
this.highlightObjs.push(glow, ripple);
|
||||
}
|
||||
}
|
||||
|
||||
// ── UI updates ────────────────────────────────────────────────────────────
|
||||
updateButtons() {
|
||||
const canRoll = !this.animating
|
||||
|
|
|
|||
Loading…
Reference in New Issue