feat: add market card sliding and refill animations to Ticket to Ride
- Implement `animateMarketSlideUp` to slide remaining cards up when one is drawn. - Add `animateRefillMarketSlot` to animate a new card appearing from the deck. - Update human and AI draw handlers to orchestrate card draw, slide, and refill animations. - Introduce `marketSlotObjs` to track UI objects per slot for targeted tweens. - Create `renderAllExceptMarket` to render the rest of the game while market animations play. - Refactor `renderMarket` to group objects by slot index.
This commit is contained in:
parent
ae6c69e25c
commit
97befdb502
|
|
@ -54,6 +54,7 @@ export default class TicketToRideGame extends Phaser.Scene {
|
|||
this.oppScoreTexts = {};
|
||||
this.oppMiniHandGfx = [];
|
||||
this.oppMiniHandCount = [];
|
||||
this.marketSlotObjs = [];
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
@ -356,16 +357,24 @@ export default class TicketToRideGame extends Phaser.Scene {
|
|||
const srcX = MK_X;
|
||||
const srcY = MK_Y0 + i * (CARD_W + 10);
|
||||
const color = this.gs.faceUp[i];
|
||||
const origFaceUpLength = this.gs.faceUp.length;
|
||||
const newGs = L.drawTrainCard(this.gs, 0, 'faceUp', i);
|
||||
const refillHappened = newGs.faceUp.length === origFaceUpLength;
|
||||
const refillSlot = origFaceUpLength - 1;
|
||||
const refillColor = refillHappened ? newGs.faceUp[refillSlot] : undefined;
|
||||
this.busy = true;
|
||||
this.gs = newGs;
|
||||
this.renderAll();
|
||||
this.marketSlotObjs[i]?.forEach((o) => o.setVisible(false));
|
||||
this.renderAllExceptMarket();
|
||||
playSound(this, SFX.CARD_DEAL);
|
||||
const dest = this.handDestForColor(color);
|
||||
await this.animateCardDraw({
|
||||
srcX, srcY, srcAngle: -90, cardFrame: TTR_CARD_FRAME[color],
|
||||
shouldFlip: false, destX: dest.x, destY: dest.y, destW: 97, destH: 140,
|
||||
});
|
||||
await this.animateMarketSlideUp(i, origFaceUpLength - 1);
|
||||
if (refillColor !== undefined) await this.animateRefillMarketSlot(refillSlot, refillColor);
|
||||
this.renderAll();
|
||||
this.busy = false;
|
||||
this.advance();
|
||||
}
|
||||
|
|
@ -434,13 +443,18 @@ export default class TicketToRideGame extends Phaser.Scene {
|
|||
const fanX = dX - OPP_R;
|
||||
const fanY = dY + OPP_R + 20;
|
||||
// First draw
|
||||
const faceUpColor1 = a.source === 'faceUp' ? this.gs.faceUp[a.index] : null;
|
||||
const srcX1 = a.source === 'faceUp' ? MK_X : PILE_X;
|
||||
const srcY1 = a.source === 'faceUp' ? MK_Y0 + a.index * (CARD_W + 10) : MK_Y0;
|
||||
const firstFaceUp = a.source === 'faceUp';
|
||||
const faceUpColor1 = firstFaceUp ? this.gs.faceUp[a.index] : null;
|
||||
const srcX1 = firstFaceUp ? MK_X : PILE_X;
|
||||
const srcY1 = firstFaceUp ? MK_Y0 + a.index * (CARD_W + 10) : MK_Y0;
|
||||
const origFaceUpLen1 = firstFaceUp ? this.gs.faceUp.length : 0;
|
||||
if (firstFaceUp) this.marketSlotObjs[a.index]?.forEach((o) => o.setVisible(false));
|
||||
this.gs = L.drawTrainCard(this.gs, seat, a.source, a.index);
|
||||
const slot1 = Math.min(L.handCount(this.gs.players[seat]) - 1, 4);
|
||||
const refillHappened1 = firstFaceUp && this.gs.faceUp.length === origFaceUpLen1;
|
||||
const refillColor1 = refillHappened1 ? this.gs.faceUp[origFaceUpLen1 - 1] : undefined;
|
||||
playSound(this, SFX.CARD_DEAL);
|
||||
this.renderAll();
|
||||
if (firstFaceUp) this.renderAllExceptMarket(); else this.renderAll();
|
||||
await this.animateCardDraw({
|
||||
srcX: srcX1, srcY: srcY1, srcAngle: -90,
|
||||
cardFrame: faceUpColor1 ? TTR_CARD_FRAME[faceUpColor1] : TTR_CARD_FRAME.back,
|
||||
|
|
@ -448,18 +462,28 @@ export default class TicketToRideGame extends Phaser.Scene {
|
|||
destX: fanX + slot1 * MINI_STEP + MINI_W / 2, destY: fanY,
|
||||
destW: MINI_W, destH: MINI_H,
|
||||
});
|
||||
if (firstFaceUp) {
|
||||
await this.animateMarketSlideUp(a.index, origFaceUpLen1 - 1);
|
||||
if (refillColor1 !== undefined) await this.animateRefillMarketSlot(origFaceUpLen1 - 1, refillColor1);
|
||||
this.renderAll();
|
||||
}
|
||||
// Second draw if the turn is still in progress.
|
||||
if ((this.gs.phase === 'turn' || this.gs.phase === 'lastRound')
|
||||
&& this.gs.currentPlayer === seat && this.gs.drawnThisTurn === 1 && !this.gs.pendingTickets) {
|
||||
await this.delay(150);
|
||||
const b = AI.chooseSecondDraw(this.gs, seat);
|
||||
const faceUpColor2 = b.source === 'faceUp' ? this.gs.faceUp[b.index] : null;
|
||||
const srcX2 = b.source === 'faceUp' ? MK_X : PILE_X;
|
||||
const srcY2 = b.source === 'faceUp' ? MK_Y0 + b.index * (CARD_W + 10) : MK_Y0;
|
||||
const secondFaceUp = b.source === 'faceUp';
|
||||
const faceUpColor2 = secondFaceUp ? this.gs.faceUp[b.index] : null;
|
||||
const srcX2 = secondFaceUp ? MK_X : PILE_X;
|
||||
const srcY2 = secondFaceUp ? MK_Y0 + b.index * (CARD_W + 10) : MK_Y0;
|
||||
const origFaceUpLen2 = secondFaceUp ? this.gs.faceUp.length : 0;
|
||||
if (secondFaceUp) this.marketSlotObjs[b.index]?.forEach((o) => o.setVisible(false));
|
||||
this.gs = L.drawTrainCard(this.gs, seat, b.source, b.index);
|
||||
const slot2 = Math.min(L.handCount(this.gs.players[seat]) - 1, 4);
|
||||
const refillHappened2 = secondFaceUp && this.gs.faceUp.length === origFaceUpLen2;
|
||||
const refillColor2 = refillHappened2 ? this.gs.faceUp[origFaceUpLen2 - 1] : undefined;
|
||||
playSound(this, SFX.CARD_DEAL);
|
||||
this.renderAll();
|
||||
if (secondFaceUp) this.renderAllExceptMarket(); else this.renderAll();
|
||||
await this.animateCardDraw({
|
||||
srcX: srcX2, srcY: srcY2, srcAngle: -90,
|
||||
cardFrame: faceUpColor2 ? TTR_CARD_FRAME[faceUpColor2] : TTR_CARD_FRAME.back,
|
||||
|
|
@ -467,6 +491,11 @@ export default class TicketToRideGame extends Phaser.Scene {
|
|||
destX: fanX + slot2 * MINI_STEP + MINI_W / 2, destY: fanY,
|
||||
destW: MINI_W, destH: MINI_H,
|
||||
});
|
||||
if (secondFaceUp) {
|
||||
await this.animateMarketSlideUp(b.index, origFaceUpLen2 - 1);
|
||||
if (refillColor2 !== undefined) await this.animateRefillMarketSlot(origFaceUpLen2 - 1, refillColor2);
|
||||
this.renderAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -539,6 +568,7 @@ export default class TicketToRideGame extends Phaser.Scene {
|
|||
renderMarket() {
|
||||
this.marketObjs.forEach((o) => o.destroy());
|
||||
this.marketObjs = [];
|
||||
this.marketSlotObjs = [];
|
||||
const myTurn = this.canHumanAct();
|
||||
const mkStep = CARD_W + 10; // rotated visual height + padding
|
||||
for (let i = 0; i < 5; i++) {
|
||||
|
|
@ -548,16 +578,17 @@ export default class TicketToRideGame extends Phaser.Scene {
|
|||
const g = this.add.graphics().setDepth(D.market);
|
||||
g.lineStyle(2, COLORS.mutedHex, 0.5);
|
||||
g.strokeRoundedRect(MK_X - CARD_H / 2, y - CARD_W / 2, CARD_H, CARD_W, 8);
|
||||
this.marketSlotObjs[i] = [g];
|
||||
this.marketObjs.push(g);
|
||||
continue;
|
||||
}
|
||||
const objs = this.makeCardFace(MK_X, y, CARD_W, CARD_H, color, D.market, '');
|
||||
objs.forEach((o) => o.setAngle(-90));
|
||||
this.marketObjs.push(...objs);
|
||||
const z = this.add.zone(MK_X, y, CARD_H, CARD_W).setDepth(D.market + 2);
|
||||
if (myTurn) z.setInteractive({ useHandCursor: true });
|
||||
z.on('pointerdown', () => this.onMarketClick(i));
|
||||
this.marketObjs.push(z);
|
||||
this.marketSlotObjs[i] = [...objs, z];
|
||||
this.marketObjs.push(...objs, z);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -882,11 +913,61 @@ export default class TicketToRideGame extends Phaser.Scene {
|
|||
|
||||
delay(ms) { return new Promise((res) => this.time.delayedCall(ms, res)); }
|
||||
|
||||
renderAllExceptMarket() {
|
||||
if (!this.gs) return;
|
||||
this.renderRoutes();
|
||||
this.renderHover();
|
||||
this.renderHand();
|
||||
this.renderPiles();
|
||||
this.renderPanels();
|
||||
this.renderStatus();
|
||||
}
|
||||
|
||||
handDestForColor(colorKey) {
|
||||
const i = HAND_ORDER.indexOf(colorKey);
|
||||
return { x: 400 + i * 105, y: 1000 };
|
||||
}
|
||||
|
||||
async animateMarketSlideUp(fromSlot, lastSlot) {
|
||||
const mkStep = CARD_W + 10;
|
||||
const targets = [];
|
||||
for (let j = fromSlot + 1; j <= lastSlot; j++) {
|
||||
if (this.marketSlotObjs[j]) targets.push(...this.marketSlotObjs[j]);
|
||||
}
|
||||
if (targets.length === 0) return;
|
||||
await new Promise((res) => this.tweens.add({
|
||||
targets, y: `-=${mkStep}`, duration: 200, ease: 'Cubic.easeInOut', onComplete: res,
|
||||
}));
|
||||
}
|
||||
|
||||
async animateRefillMarketSlot(slotIndex, color) {
|
||||
const ANIM_DEPTH = D.banner + 10;
|
||||
const destX = MK_X;
|
||||
const destY = MK_Y0 + slotIndex * (CARD_W + 10);
|
||||
const origScaleX = CARD_W / 270;
|
||||
|
||||
const card = this.add.image(PILE_X, MK_Y0, 'ttr-cards', TTR_CARD_FRAME.back)
|
||||
.setDisplaySize(CARD_W, CARD_H)
|
||||
.setAngle(-90)
|
||||
.setDepth(ANIM_DEPTH);
|
||||
|
||||
// Slide from deck to market slot
|
||||
await new Promise((res) => this.tweens.add({
|
||||
targets: card, x: destX, y: destY, duration: 350, ease: 'Cubic.easeOut', onComplete: res,
|
||||
}));
|
||||
|
||||
// Flip at slot: collapse scaleX (height at -90°), swap frame, restore
|
||||
await new Promise((res) => this.tweens.add({
|
||||
targets: card, scaleX: 0, duration: 150, ease: 'Linear', onComplete: res,
|
||||
}));
|
||||
card.setFrame(TTR_CARD_FRAME[color] ?? TTR_CARD_FRAME.back);
|
||||
await new Promise((res) => this.tweens.add({
|
||||
targets: card, scaleX: origScaleX, duration: 150, ease: 'Linear', onComplete: res,
|
||||
}));
|
||||
|
||||
card.destroy();
|
||||
}
|
||||
|
||||
async animateCardDraw({ srcX, srcY, srcAngle = 0, cardFrame, shouldFlip = false, destX, destY, destW, destH }) {
|
||||
const ANIM_DEPTH = D.banner + 10;
|
||||
const CX = GAME_WIDTH / 2;
|
||||
|
|
|
|||
Loading…
Reference in New Issue