refactor: fix card visibility and animation sequence in GoFish

- Pass live sprites from `animateAsk` to `animatePairedCards` to prevent premature destruction.
- Update `animateAsk` to fan collected cards into the asker's show zone instead of hiding them.
- Update `animatePairedCards` to accept pre-existing sprites for seamless pair animation.
- Remove `canAsk` check in `applyAsk` to ensure turn advances correctly after any ask.
This commit is contained in:
Brian Fertig 2026-05-18 23:56:59 -06:00
parent b63bf1b651
commit 975ee6ea25
2 changed files with 56 additions and 25 deletions

View File

@ -651,7 +651,7 @@ export default class GoFishGame extends Phaser.Scene {
if (after === before) { this.animating = false; return; }
const last = after.lastAsk;
this.animateAsk(askerSeat, targetSeat, rank, last, before, () => {
this.animateAsk(askerSeat, targetSeat, rank, last, before, (liveSprites) => {
// Fish with pool cards remaining — player must pick interactively.
if (after.phase === 'pick') {
this.gs = after;
@ -678,16 +678,17 @@ export default class GoFishGame extends Phaser.Scene {
}
if (last.newPairs > 0 && last.pairedCards?.length >= 2) {
this.time.delayedCall(600, () => {
this.hideBanner();
// Cards from animateAsk are already visible in position.
// Do NOT call renderAll() here — it would destroy the liveSprites.
// renderAll() is deferred to after the pair animation completes.
this.hideBanner();
this.animatePairedCards(askerSeat, last.pairedCards, () => {
this.renderAll();
this.updateStatus();
this.animatePairedCards(askerSeat, last.pairedCards, () => {
this.animating = false;
if (isGameOver(this.gs)) { this.endGame(); return; }
this.maybeStartAITurn();
});
});
this.animating = false;
if (isGameOver(this.gs)) { this.endGame(); return; }
this.maybeStartAITurn();
}, liveSprites ?? null);
} else {
this.time.delayedCall(900, () => {
this.renderAll();
@ -754,24 +755,31 @@ export default class GoFishGame extends Phaser.Scene {
return { sprite, tx, ty };
});
// After target cards are shown, fan all sprites into a holding position
// at the asker's show zone — they stay visible for the pair animation.
const waitMs = 250 + (n - 1) * 80 + 380;
this.time.delayedCall(waitMs, () => {
if (askerSprite.active) {
this.tweens.add({
targets: askerSprite, alpha: 0, duration: 200,
onComplete: () => { if (askerSprite.active) askerSprite.destroy(); },
});
}
for (const { sprite } of targetSprites) {
const FGAP = 8;
const nTotal = 1 + targetSprites.length;
const isAskerVert = askerSlot === 'left' || askerSlot === 'right';
const allSprites = [
askerSprite,
...targetSprites.map(({ sprite }) => sprite),
].map((sprite, i) => {
const x = isAskerVert
? askerShow.x
: askerShow.x + (i - (nTotal - 1) / 2) * (CARD_W + FGAP);
const y = isAskerVert
? askerShow.y + (i - (nTotal - 1) / 2) * (CARD_H + FGAP)
: askerShow.y;
return { sprite, x, y };
});
for (const { sprite, x, y } of allSprites) {
if (!sprite.active) continue;
this.tweens.add({
targets: sprite,
x: askerLayout.handCenter.x, y: askerLayout.handCenter.y,
alpha: 0, duration: 380, ease: 'Cubic.easeIn',
onComplete: () => { if (sprite.active) sprite.destroy(); },
});
this.tweens.add({ targets: sprite, x, y, duration: 280, ease: 'Cubic.easeOut' });
}
this.time.delayedCall(420, onComplete);
this.time.delayedCall(320, () => onComplete(allSprites));
});
} else {
// Fish — flip card back face-down and return it to hand.
@ -822,7 +830,30 @@ export default class GoFishGame extends Phaser.Scene {
});
}
animatePairedCards(askerSeat, pairedCards, onComplete) {
animatePairedCards(askerSeat, pairedCards, onComplete, preSprites = null) {
if (preSprites) {
// Cards are already face-up and in position from animateAsk — skip sprite creation.
const sprites = preSprites.map(({ sprite, x, y }) => ({ sprite, targetX: x, targetY: y }));
playSound(this, SFX.CARD_PLACE);
this.time.delayedCall(200, () => {
const cx = sprites.reduce((s, { targetX }) => s + targetX, 0) / sprites.length;
const cy = sprites.reduce((s, { targetY }) => s + targetY, 0) / sprites.length;
this.spawnFireworks(cx, cy);
this.time.delayedCall(350, () => this.spawnFireworks(cx, cy));
});
this.time.delayedCall(1200, () => {
for (const { sprite } of sprites) {
if (!sprite.active) continue;
this.tweens.add({
targets: sprite, x: CX, y: CY, alpha: 0, duration: 500, ease: 'Cubic.easeIn',
onComplete: () => { if (sprite.active) sprite.destroy(); },
});
}
});
this.time.delayedCall(1800, onComplete);
return;
}
const slot = this.slotForSeat[askerSeat];
const layout = slotLayout(slot);
const n = pairedCards.length;

View File

@ -193,7 +193,7 @@ export function applyAsk(state, targetSeat, rank) {
pairedCards: [],
};
ensureHasCards(next, askerSeat);
if (!canAsk(next, askerSeat)) advanceTurn(next);
advanceTurn(next);
checkGameOver(next);
return next;
}