diff --git a/public/src/games/gofish/GoFishGame.js b/public/src/games/gofish/GoFishGame.js index d9f85bd..2f07cd7 100644 --- a/public/src/games/gofish/GoFishGame.js +++ b/public/src/games/gofish/GoFishGame.js @@ -601,39 +601,164 @@ export default class GoFishGame extends Phaser.Scene { const after = applyAsk(before, targetSeat, rank); if (after === before) { this.animating = false; return; } const last = after.lastAsk; - this.showBanner(this.formatAskBanner(last)); - playSound(this, last.result === 'catch' ? SFX.CARD_PLACE : SFX.CARD_DEAL); - this.gs = after; - this.selectedRank = null; + this.animateAsk(askerSeat, targetSeat, rank, last, before, () => { + this.showBanner(this.formatAskBanner(last)); + playSound(this, last.result === 'catch' ? SFX.CARD_PLACE : SFX.CARD_DEAL); - for (let s = 0; s < this.gs.players.length; s++) { - observeLog(this.aiMemory[s], this.gs, s); - } + this.gs = after; + this.selectedRank = null; - if (last.newPairs > 0 && last.pairedCards?.length >= 2) { - this.time.delayedCall(600, () => { - this.hideBanner(); - this.renderAll(); - this.updateStatus(); - this.animatePairedCards(askerSeat, last.pairedCards, () => { + for (let s = 0; s < this.gs.players.length; s++) { + observeLog(this.aiMemory[s], this.gs, s); + } + + if (last.newPairs > 0 && last.pairedCards?.length >= 2) { + this.time.delayedCall(600, () => { + this.hideBanner(); + this.renderAll(); + this.updateStatus(); + this.animatePairedCards(askerSeat, last.pairedCards, () => { + this.animating = false; + if (isGameOver(this.gs)) { this.endGame(); return; } + this.maybeStartAITurn(); + }); + }); + } else { + this.time.delayedCall(900, () => { + this.renderAll(); + this.updateStatus(); this.animating = false; if (isGameOver(this.gs)) { this.endGame(); return; } + this.hideBanner(); this.maybeStartAITurn(); }); + } + }); + } + + animateAsk(askerSeat, targetSeat, rank, last, beforeState, onComplete) { + const askerSlot = this.slotForSeat[askerSeat]; + const askerLayout = slotLayout(askerSlot); + const askerShow = this.slotShowPos(askerSlot, askerLayout); + + const askerCard = beforeState.players[askerSeat].hand.find(c => c.rank === rank); + if (!askerCard) { onComplete(); return; } + + const askerSprite = this.makeCardSprite(askerCard, askerLayout.handCenter.x, askerLayout.handCenter.y, { + faceUp: false, rotation: 0, + }); + askerSprite.setDepth(D.banner - 4); + this.transientObjs.push(askerSprite); + + this.tweens.add({ + targets: askerSprite, + x: askerShow.x, y: askerShow.y, + duration: 280, + ease: 'Cubic.easeOut', + onComplete: () => this.flipCardFaceUp(askerSprite, askerCard), + }); + + if (last.result === 'catch') { + const targetSlot = this.slotForSeat[targetSeat]; + const targetLayout = slotLayout(targetSlot); + const targetShow = this.slotShowPos(targetSlot, targetLayout); + const n = last.cardsTransferred.length; + + this.time.delayedCall(640, () => { + const isTargetVert = targetSlot === 'left' || targetSlot === 'right'; + const targetSprites = last.cardsTransferred.map((card, i) => { + const tx = isTargetVert ? targetShow.x + : targetShow.x + (i - (n - 1) / 2) * (CARD_W + 8); + const ty = isTargetVert ? targetShow.y + (i - (n - 1) / 2) * (CARD_H + 8) + : targetShow.y; + + const sprite = this.makeCardSprite(card, targetLayout.handCenter.x, targetLayout.handCenter.y, { + faceUp: false, rotation: 0, + }); + sprite.setDepth(D.banner - 4); + this.transientObjs.push(sprite); + + this.tweens.add({ + targets: sprite, + x: tx, y: ty, + duration: 250, + delay: i * 80, + ease: 'Cubic.easeOut', + onComplete: () => this.flipCardFaceUp(sprite, card), + }); + + return { sprite, tx, ty }; + }); + + 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) { + 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.time.delayedCall(420, onComplete); + }); }); } else { - this.time.delayedCall(900, () => { - this.renderAll(); - this.updateStatus(); - this.animating = false; - if (isGameOver(this.gs)) { this.endGame(); return; } - this.hideBanner(); - this.maybeStartAITurn(); + this.time.delayedCall(880, () => { + this.flipCardFaceDown(askerSprite); + this.time.delayedCall(340, () => { + this.tweens.add({ + targets: askerSprite, + x: askerLayout.handCenter.x, y: askerLayout.handCenter.y, + duration: 280, ease: 'Cubic.easeIn', + onComplete: () => { if (askerSprite.active) askerSprite.destroy(); }, + }); + this.time.delayedCall(320, onComplete); + }); }); } } + slotShowPos(slot, layout) { + const GAP = 10; + switch (slot) { + case 'bottom': return { x: layout.handCenter.x, y: layout.handCenter.y - CARD_H / 2 - GAP - CARD_H / 2 }; + case 'top': return { x: layout.handCenter.x, y: layout.handCenter.y + CARD_H / 2 + GAP + CARD_H / 2 }; + case 'left': return { x: layout.handCenter.x + CARD_H / 2 + GAP + CARD_W / 2, y: layout.handCenter.y }; + case 'right': return { x: layout.handCenter.x - CARD_H / 2 - GAP - CARD_W / 2, y: layout.handCenter.y }; + default: return { x: CX, y: CY }; + } + } + + flipCardFaceUp(container, card) { + this.tweens.add({ + targets: container, scaleX: 0, duration: 150, ease: 'Linear', + onComplete: () => { + container.setRotation(0); + this.renderCardFace(container, card, true); + this.tweens.add({ targets: container, scaleX: 1, duration: 150, ease: 'Linear' }); + }, + }); + } + + flipCardFaceDown(container) { + this.tweens.add({ + targets: container, scaleX: 0, duration: 150, ease: 'Linear', + onComplete: () => { + this.renderCardFace(container, container.card, false); + this.tweens.add({ targets: container, scaleX: 1, duration: 150, ease: 'Linear' }); + }, + }); + } + animatePairedCards(askerSeat, pairedCards, onComplete) { const slot = this.slotForSeat[askerSeat]; const layout = slotLayout(slot);