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() {
|
cleanup() {
|
||||||
this.input?.removeAllListeners();
|
this.input?.removeAllListeners();
|
||||||
this.clearHumanControls();
|
this.clearHumanControls();
|
||||||
|
this._flyCard?.destroy();
|
||||||
|
this._flyCard = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Table backdrop ──────────────────────────────────────────────────────────
|
// ── Table backdrop ──────────────────────────────────────────────────────────
|
||||||
|
|
@ -405,28 +407,86 @@ export default class KiitosGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
applyMove(move, playerIdx) {
|
applyMove(move, playerIdx) {
|
||||||
let evt;
|
if (move.type === 'pass') {
|
||||||
if (move.type === 'play') {
|
const evt = this.logic.applyPass(playerIdx);
|
||||||
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 (evt.resolved) {
|
if (evt.resolved) {
|
||||||
const owner = this.logic.players[evt.resolved.ownerIdx];
|
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);
|
this.flashCenter(`${owner.name === 'You' ? 'Your' : owner.name + "'s"} word fizzled — ${evt.resolved.cards} cards to penalty`, THEME.negativeHex);
|
||||||
playSound(this, SFX.CARD_SHUFFLE);
|
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();
|
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) {
|
if (evt?.completed) {
|
||||||
const owner = this.logic.players[evt.ownerIdx];
|
const owner = this.logic.players[evt.ownerIdx];
|
||||||
this.kiitosBubble(owner, evt.word);
|
this.kiitosBubble(owner, evt.word);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (evt?.roundOver) { this.time.delayedCall(700, () => this.endRound()); return; }
|
if (evt?.roundOver) { this.time.delayedCall(700, () => this.endRound()); return; }
|
||||||
this.time.delayedCall(evt?.completed ? 1100 : 360, () => this.nextTurn());
|
this.time.delayedCall(evt?.completed ? 1100 : 360, () => this.nextTurn());
|
||||||
}
|
}
|
||||||
|
|
@ -503,12 +563,10 @@ export default class KiitosGame extends Phaser.Scene {
|
||||||
|
|
||||||
if (this._humanForced) {
|
if (this._humanForced) {
|
||||||
if (letter !== this._humanForced || insertIndex !== built.length) { this.snapBack(card); return; }
|
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._humanActive = false;
|
||||||
this.lockDrag();
|
this.lockDrag();
|
||||||
this.clearSlots();
|
this.clearSlots();
|
||||||
this.tweenCardToSlot(card, slotX);
|
this.applyMove({ type: 'play', letter, forced: true }, 0);
|
||||||
this.time.delayedCall(170, () => this.applyMove({ type: 'play', letter, forced: true }, 0));
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const newBuilt = built.slice(0, insertIndex) + letter + built.slice(insertIndex);
|
const newBuilt = built.slice(0, insertIndex) + letter + built.slice(insertIndex);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue