feat: add card flying animation to Kiitos game
- Animate cards flying from player's panel to center slot on play/deviate moves - Use tweens with Cubic.easeOut for smooth card placement (1200ms) - Refactor applyMove to handle pass as early return and compute animation destination upfront - Extract _postMoveEvents helper for post-move event handling - Simplify forced move flow by removing manual delay and slot position capture - Clean up flyCard reference on scene cleanup to prevent memory leaks
This commit is contained in:
parent
4597b31f5c
commit
f3939435c6
|
|
@ -72,6 +72,8 @@ export default class KiitosGame extends Phaser.Scene {
|
|||
cleanup() {
|
||||
this.input?.removeAllListeners();
|
||||
this.clearHumanControls();
|
||||
this._flyCard?.destroy();
|
||||
this._flyCard = null;
|
||||
}
|
||||
|
||||
// ── Table backdrop ──────────────────────────────────────────────────────────
|
||||
|
|
@ -405,28 +407,86 @@ export default class KiitosGame extends Phaser.Scene {
|
|||
}
|
||||
|
||||
applyMove(move, playerIdx) {
|
||||
let evt;
|
||||
if (move.type === 'play') {
|
||||
evt = this.logic.applyForced(playerIdx);
|
||||
playSound(this, SFX.CARD_PLACE);
|
||||
} else if (move.type === 'deviate') {
|
||||
evt = this.logic.applyDeviate(playerIdx, move.letter, move.prefix ?? move.newBuilt, move.word);
|
||||
playSound(this, SFX.CARD_PLACE);
|
||||
} else {
|
||||
evt = this.logic.applyPass(playerIdx);
|
||||
if (move.type === 'pass') {
|
||||
const evt = this.logic.applyPass(playerIdx);
|
||||
if (evt.resolved) {
|
||||
const owner = this.logic.players[evt.resolved.ownerIdx];
|
||||
this.flashCenter(`${owner.name === 'You' ? 'Your' : owner.name + "'s"} word fizzled — ${evt.resolved.cards} cards to penalty`, THEME.negativeHex);
|
||||
playSound(this, SFX.CARD_SHUFFLE);
|
||||
}
|
||||
this.renderState();
|
||||
this._postMoveEvents(evt);
|
||||
return;
|
||||
}
|
||||
|
||||
// Compute animation destination before applying logic (center may be cleared on completion).
|
||||
const oldBuilt = this.logic.center.built;
|
||||
let cardIndex, displayWord;
|
||||
if (move.type === 'play') {
|
||||
cardIndex = oldBuilt.length;
|
||||
displayWord = this.logic.center.targetWord ?? (oldBuilt + move.letter);
|
||||
} else {
|
||||
const newBuilt = move.prefix ?? move.newBuilt;
|
||||
const inserts = insertionsFromTo(oldBuilt, newBuilt);
|
||||
cardIndex = inserts[0]?.index ?? Math.max(0, newBuilt.length - 1);
|
||||
displayWord = move.word;
|
||||
}
|
||||
const totW = displayWord.length * CARD_W + (displayWord.length - 1) * CARD_GAP;
|
||||
const rowStartX = GAME_WIDTH / 2 - totW / 2 + CARD_W / 2;
|
||||
const destX = rowStartX + cardIndex * (CARD_W + CARD_GAP);
|
||||
|
||||
// Apply logic.
|
||||
let evt;
|
||||
if (move.type === 'play') {
|
||||
evt = this.logic.applyForced(playerIdx);
|
||||
} else {
|
||||
evt = this.logic.applyDeviate(playerIdx, move.letter, move.prefix ?? move.newBuilt, move.word);
|
||||
}
|
||||
playSound(this, SFX.CARD_PLACE);
|
||||
|
||||
// Render post-move state. Hide the newly placed card until the animation lands.
|
||||
this._deviateView = false;
|
||||
this.renderState();
|
||||
|
||||
let hiddenCard = null;
|
||||
if (!evt?.completed && this.centerLayer?.list) {
|
||||
hiddenCard = this.centerLayer.list[cardIndex] ?? null;
|
||||
if (hiddenCard?.active) hiddenCard.setAlpha(0);
|
||||
}
|
||||
|
||||
// Fly the card from the playing seat's panel to its slot in the centre row.
|
||||
const seatId = this.seats[playerIdx].id;
|
||||
const panel = this.seatPanels?.[seatId];
|
||||
const startX = panel?.x ?? GAME_WIDTH / 2;
|
||||
const startY = panel?.y ?? CENTER_Y;
|
||||
|
||||
this._flyCard?.destroy();
|
||||
this._flyCard = this.makeCard(startX, startY, CARD_W, CARD_H, move.letter);
|
||||
this._flyCard.setDepth(D.bubble);
|
||||
this._flyCard.setScale(0.4);
|
||||
|
||||
this.tweens.add({
|
||||
targets: this._flyCard,
|
||||
x: destX, y: CENTER_Y,
|
||||
scaleX: 1, scaleY: 1,
|
||||
duration: 1200,
|
||||
ease: 'Cubic.easeOut',
|
||||
onComplete: () => {
|
||||
this._flyCard?.destroy();
|
||||
this._flyCard = null;
|
||||
if (hiddenCard?.active) {
|
||||
this.tweens.add({ targets: hiddenCard, alpha: 1, duration: 150, ease: 'Sine.easeIn' });
|
||||
}
|
||||
this._postMoveEvents(evt);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_postMoveEvents(evt) {
|
||||
if (evt?.completed) {
|
||||
const owner = this.logic.players[evt.ownerIdx];
|
||||
this.kiitosBubble(owner, evt.word);
|
||||
}
|
||||
|
||||
if (evt?.roundOver) { this.time.delayedCall(700, () => this.endRound()); return; }
|
||||
this.time.delayedCall(evt?.completed ? 1100 : 360, () => this.nextTurn());
|
||||
}
|
||||
|
|
@ -503,12 +563,10 @@ export default class KiitosGame extends Phaser.Scene {
|
|||
|
||||
if (this._humanForced) {
|
||||
if (letter !== this._humanForced || insertIndex !== built.length) { this.snapBack(card); return; }
|
||||
const slotX = zone.getData('mx'); // capture before clearSlots() destroys the zone
|
||||
this._humanActive = false;
|
||||
this.lockDrag();
|
||||
this.clearSlots();
|
||||
this.tweenCardToSlot(card, slotX);
|
||||
this.time.delayedCall(170, () => this.applyMove({ type: 'play', letter, forced: true }, 0));
|
||||
this.applyMove({ type: 'play', letter, forced: true }, 0);
|
||||
return;
|
||||
}
|
||||
const newBuilt = built.slice(0, insertIndex) + letter + built.slice(insertIndex);
|
||||
|
|
|
|||
Loading…
Reference in New Issue