diff --git a/public/src/games/gofish/GoFishGame.js b/public/src/games/gofish/GoFishGame.js index 8136e7b..d7e7bb8 100644 --- a/public/src/games/gofish/GoFishGame.js +++ b/public/src/games/gofish/GoFishGame.js @@ -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; diff --git a/public/src/games/gofish/GoFishLogic.js b/public/src/games/gofish/GoFishLogic.js index d2b09b5..4170e4b 100644 --- a/public/src/games/gofish/GoFishLogic.js +++ b/public/src/games/gofish/GoFishLogic.js @@ -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; }