feat(splendor): add buy/reserve card animations and bought-card thumbnails
- Implement card fly-to-center animation with fireworks for buy and reserve actions - Add persistent bought-card thumbnails in player panels with automatic wrapping - Build full card containers (costs, points, gem badges) for smooth scaling transitions - Suppress reserve pills and buy animations in UI until transitions complete - Apply animation triggers consistently for both human and AI turns
This commit is contained in:
parent
5eb5c71ebf
commit
6abcbfa32b
|
|
@ -32,6 +32,17 @@ const CONTROL_Y = 794; // bottom context bar
|
|||
|
||||
const DEPTH = { bg: 0, board: 10, card: 14, ui: 40, popup: 60, banner: 90 };
|
||||
|
||||
// ── bought-card thumbnails in player panels ───────────────────────────────────
|
||||
const THUMB_W = 44;
|
||||
const THUMB_H = Math.round(THUMB_W * CH / CW); // ≈ 60 — maintains card aspect ratio
|
||||
const THUMB_GAP = 4; // horizontal gap between cards
|
||||
const THUMB_ROW_GAP = 3; // vertical gap between rows
|
||||
const PORTRAIT_R = 34; // must match buildPortraits
|
||||
const THUMB_AREA_REL_X = PORTRAIT_R * 2 + 16 + 8; // portrait right-edge + 8 px left padding
|
||||
const THUMB_GEM_REL_X = PANEL_W - 16 - 14 - 5 * 36 - 14; // gem-row left edge, panel-relative
|
||||
const THUMB_PER_ROW = Math.floor((THUMB_GEM_REL_X - THUMB_AREA_REL_X) / (THUMB_W + THUMB_GAP));
|
||||
const THUMB_ROW_TOP_REL = 38; // panel-relative top (aligns with portrait top)
|
||||
|
||||
const hexStr = (n) => '#' + (n >>> 0).toString(16).padStart(6, '0').slice(-6);
|
||||
|
||||
export default class SplendorGame extends Phaser.Scene {
|
||||
|
|
@ -50,6 +61,9 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
this.dyn = []; // dynamic objects rebuilt every render
|
||||
this.preview = []; // hover card preview — cleared separately
|
||||
this.portraits = []; // one per seat, created once
|
||||
this.animatingReserve = null; // { seat, cardId } suppresses that pill during animation
|
||||
this.animatingBuy = null; // { seat, cardId } suppresses thumbnail during animation
|
||||
this.boughtCards = {}; // seat → card[] — persists purchased cards for display
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
@ -142,6 +156,40 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
this.preview = [];
|
||||
}
|
||||
|
||||
findCardBoardPos(cardId) {
|
||||
for (const tier of [1, 2, 3]) {
|
||||
const col = this.gs.board[tier].findIndex((c) => c?.id === cardId);
|
||||
if (col !== -1) {
|
||||
return { x: CARDS_X + col * (CW + CARD_GAP) + CW / 2, y: TIER_Y[tier] + CH / 2 };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
findCard(cardId) {
|
||||
for (const tier of [1, 2, 3]) {
|
||||
const c = this.gs.board[tier].find((c) => c?.id === cardId);
|
||||
if (c) return c;
|
||||
}
|
||||
for (const p of this.gs.players) {
|
||||
const c = p.reserved.find((c) => c?.id === cardId);
|
||||
if (c) return c;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
boughtCardThumbPos(seat, idx) {
|
||||
const n = this.gs.players.length;
|
||||
const gap = 16;
|
||||
const h = Math.min(220, Math.floor((GAME_HEIGHT - 90 - gap * (n - 1)) / n));
|
||||
const panelY = 76 + seat * (h + gap);
|
||||
const col = idx % THUMB_PER_ROW;
|
||||
const row = Math.floor(idx / THUMB_PER_ROW);
|
||||
const tx = PANEL_X + THUMB_AREA_REL_X + col * (THUMB_W + THUMB_GAP);
|
||||
const ty = panelY + THUMB_ROW_TOP_REL + row * (THUMB_H + THUMB_ROW_GAP);
|
||||
return { cx: tx + THUMB_W / 2, cy: ty + THUMB_H / 2 };
|
||||
}
|
||||
|
||||
showCardPreview(card, barCenterX, barBottomY) {
|
||||
this.clearPreview();
|
||||
const pw = 270, ph = 390;
|
||||
|
|
@ -468,6 +516,30 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
}).setOrigin(0.5).setDepth(DEPTH.ui + 2));
|
||||
});
|
||||
|
||||
// bought card thumbnails (right of portrait, left of gems, wrapping into rows)
|
||||
(this.boughtCards[idx] ?? []).forEach((bc, bi) => {
|
||||
if (this.animatingBuy?.seat === idx && this.animatingBuy?.cardId === bc.id) return;
|
||||
const col = bi % THUMB_PER_ROW;
|
||||
const row = Math.floor(bi / THUMB_PER_ROW);
|
||||
const tx = x + THUMB_AREA_REL_X + col * (THUMB_W + THUMB_GAP);
|
||||
const ty = y + THUMB_ROW_TOP_REL + row * (THUMB_H + THUMB_ROW_GAP);
|
||||
const tg = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1));
|
||||
tg.fillStyle(0x000000, 0.32).fillRoundedRect(tx + 2, ty + 2, THUMB_W, THUMB_H, 4);
|
||||
tg.fillStyle(0x14110b, 1).fillRoundedRect(tx, ty, THUMB_W, THUMB_H, 4);
|
||||
tg.fillStyle(GEM_HEX[bc.bonus], 0.22).fillRoundedRect(tx, ty, THUMB_W, THUMB_H, 4);
|
||||
if (this.hasArt) {
|
||||
this.regMaskedImg(tx, ty, THUMB_W, THUMB_H, 'splendor-cards', cardFrame(bc), DEPTH.ui + 2, 4);
|
||||
}
|
||||
this.reg(this.add.graphics().setDepth(DEPTH.ui + 3))
|
||||
.lineStyle(1, GEM_EDGE[bc.bonus], 0.9).strokeRoundedRect(tx, ty, THUMB_W, THUMB_H, 4);
|
||||
if (bc.points > 0) {
|
||||
this.reg(this.add.text(tx + 3, ty + 1, String(bc.points), {
|
||||
fontFamily: 'Righteous', fontSize: '10px',
|
||||
color: bc.bonus === 'white' ? '#333' : '#fff',
|
||||
}).setDepth(DEPTH.ui + 4));
|
||||
}
|
||||
});
|
||||
|
||||
// reserved cards
|
||||
const rN = p.reserved.length;
|
||||
this.reg(this.add.text(x + 16, y + h - 30, `Reserved: ${rN}/${MAX_RESERVED}`, {
|
||||
|
|
@ -476,6 +548,7 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
if (rN > 0) {
|
||||
const isHuman = idx === this.humanSeat;
|
||||
p.reserved.forEach((card, ri) => {
|
||||
if (this.animatingReserve?.seat === idx && this.animatingReserve?.cardId === card.id) return;
|
||||
const rx = x + 150 + ri * 116, ry = y + h - 40;
|
||||
const canBuy = isHuman && this.isHumanTurn() && canAfford(p, card);
|
||||
const mg = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1));
|
||||
|
|
@ -679,6 +752,231 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
});
|
||||
}
|
||||
|
||||
// Builds a card container centred at (x,y) in world space with full overlay art.
|
||||
// All draw coords are in container-local space (0,0 = card centre).
|
||||
_buildCardContainer(x, y, card) {
|
||||
const container = this.add.container(x, y).setDepth(DEPTH.popup + 1);
|
||||
const tint = GEM_HEX[card.bonus];
|
||||
|
||||
// Background
|
||||
const bg = this.add.graphics();
|
||||
bg.fillStyle(0x14110b, 1).fillRoundedRect(-CW / 2, -CH / 2, CW, CH, 10);
|
||||
bg.fillStyle(tint, 0.22).fillRoundedRect(-CW / 2, -CH / 2, CW, CH, 10);
|
||||
container.add(bg);
|
||||
|
||||
// Art
|
||||
if (this.hasArt) {
|
||||
container.add(
|
||||
this.add.image(0, 0, 'splendor-cards', cardFrame(card)).setDisplaySize(CW, CH)
|
||||
);
|
||||
}
|
||||
|
||||
// Border above art
|
||||
const border = this.add.graphics();
|
||||
border.lineStyle(1.5, GEM_EDGE[card.bonus], 1).strokeRoundedRect(-CW / 2, -CH / 2, CW, CH, 10);
|
||||
container.add(border);
|
||||
|
||||
// Points — top-left
|
||||
if (card.points > 0) {
|
||||
container.add(this.add.text(-CW / 2 + 10, -CH / 2 + 2, String(card.points), {
|
||||
fontFamily: 'Righteous', fontSize: '26px',
|
||||
color: card.bonus === 'white' ? '#222' : '#fff',
|
||||
}));
|
||||
}
|
||||
|
||||
// Gem bonus badge — top-right
|
||||
const badgeG = this.add.graphics();
|
||||
badgeG.fillStyle(GEM_HEX[card.bonus], 1).fillCircle(CW / 2 - 22, -CH / 2 + 15, 12);
|
||||
badgeG.lineStyle(2, GEM_EDGE[card.bonus], 1).strokeCircle(CW / 2 - 22, -CH / 2 + 15, 12);
|
||||
container.add(badgeG);
|
||||
if (this.hasGems) {
|
||||
container.add(
|
||||
this.add.image(CW / 2 - 22, -CH / 2 + 15, 'splendor-gems', gemFrame(card.bonus))
|
||||
.setDisplaySize(18, 18)
|
||||
);
|
||||
}
|
||||
|
||||
// Cost pips — lower-left, stacked
|
||||
const costs = GEMS.filter((c) => (card.cost[c] ?? 0) > 0);
|
||||
costs.forEach((c, i) => {
|
||||
const pipY = CH / 2 - 22 - i * 30;
|
||||
const pipG = this.add.graphics();
|
||||
pipG.fillStyle(GEM_HEX[c], 1).fillCircle(-CW / 2 + 20, pipY, 12);
|
||||
pipG.lineStyle(2, GEM_EDGE[c], 1).strokeCircle(-CW / 2 + 20, pipY, 12);
|
||||
container.add(pipG);
|
||||
container.add(this.add.text(-CW / 2 + 20, pipY, String(card.cost[c]), {
|
||||
fontFamily: 'Righteous', fontSize: '15px',
|
||||
color: c === 'white' ? '#222' : '#fff',
|
||||
}).setOrigin(0.5));
|
||||
});
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
// ── reserve animation ───────────────────────────────────────────────────────
|
||||
animReserve(seat, card, srcX, srcY, onDone) {
|
||||
const PW = 270, PH = 390;
|
||||
const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2;
|
||||
|
||||
// DOM video portraits render above the canvas; hide them for the duration
|
||||
for (const p of this.portraits) p?.hide();
|
||||
|
||||
const overlay = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000)
|
||||
.setAlpha(0).setDepth(DEPTH.popup);
|
||||
this.tweens.add({ targets: overlay, alpha: 0.52, duration: 280 });
|
||||
|
||||
const container = this._buildCardContainer(srcX, srcY, card);
|
||||
|
||||
// Phase 1: fly to centre and grow
|
||||
this.tweens.add({
|
||||
targets: container,
|
||||
x: cx, y: cy,
|
||||
scaleX: PW / CW, scaleY: PH / CH,
|
||||
duration: 420,
|
||||
ease: 'Cubic.easeOut',
|
||||
onComplete: () => {
|
||||
this._playReserveFireworks(cx, cy, card.bonus);
|
||||
|
||||
// Phase 2: hold 1.2 s, then shrink to reserve-pill slot
|
||||
this.time.delayedCall(1200, () => {
|
||||
const n = this.gs.players.length;
|
||||
const gap = 16;
|
||||
const h = Math.min(220, Math.floor((GAME_HEIGHT - 90 - gap * (n - 1)) / n));
|
||||
const panelY = 76 + seat * (h + gap);
|
||||
const ri = this.gs.players[seat].reserved.findIndex((c) => c.id === card.id);
|
||||
const pillCX = PANEL_X + 150 + Math.max(0, ri) * 116 + 52;
|
||||
const pillCY = panelY + h - 40 + 15;
|
||||
|
||||
this.tweens.add({ targets: overlay, alpha: 0, duration: 300,
|
||||
onComplete: () => { try { overlay.destroy(); } catch { /* */ } } });
|
||||
|
||||
this.tweens.add({
|
||||
targets: container,
|
||||
x: pillCX, y: pillCY,
|
||||
scale: 0.08,
|
||||
alpha: 0,
|
||||
duration: 360,
|
||||
ease: 'Cubic.easeIn',
|
||||
onComplete: () => {
|
||||
for (const p of this.portraits) p?.show();
|
||||
container.removeAll(true);
|
||||
container.destroy();
|
||||
onDone();
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
_playReserveFireworks(cx, cy, bonus) {
|
||||
// 5 distinct bursts staggered in time, each at a random position around the card
|
||||
const tint = GEM_HEX[bonus];
|
||||
const edge = GEM_EDGE[bonus];
|
||||
const palettes = [
|
||||
[tint, edge, 0xffffff],
|
||||
[0xffd700, 0xffbb00, 0xffffff],
|
||||
[tint, 0xffffff, edge],
|
||||
[0xff6644, 0xffaa00, 0xffd700],
|
||||
[edge, tint, 0xaaffdd],
|
||||
];
|
||||
|
||||
const BURSTS = 5;
|
||||
// Card at center is 270×390; place bursts in a ring around it
|
||||
const RX = 195, RY = 240; // semi-axes of the ring (just outside card edges)
|
||||
|
||||
for (let b = 0; b < BURSTS; b++) {
|
||||
const delay = b * 190 + Math.random() * 70;
|
||||
// Spread bursts evenly around the card with some jitter
|
||||
const angle = (b / BURSTS) * Math.PI * 2 + (Math.random() - 0.5) * 0.7;
|
||||
const rScale = 1.05 + Math.random() * 0.55;
|
||||
const bx = cx + Math.cos(angle) * RX * rScale;
|
||||
const by = cy + Math.sin(angle) * RY * rScale;
|
||||
const palette = palettes[b % palettes.length];
|
||||
const sparkCount = 16 + Math.floor(Math.random() * 8);
|
||||
|
||||
this.time.delayedCall(delay, () => {
|
||||
// Central flash — expands and fades
|
||||
const flash = this.add.circle(bx, by, 7, palette[0], 1).setDepth(DEPTH.popup + 2);
|
||||
this.tweens.add({
|
||||
targets: flash, scaleX: 4.5, scaleY: 4.5, alpha: 0,
|
||||
duration: 260, ease: 'Cubic.easeOut',
|
||||
onComplete: () => { try { flash.destroy(); } catch { /* */ } },
|
||||
});
|
||||
|
||||
// Sparks radiate outward from the burst point
|
||||
for (let i = 0; i < sparkCount; i++) {
|
||||
const sparkAngle = (i / sparkCount) * Math.PI * 2 + (Math.random() - 0.5) * 0.3;
|
||||
const speed = 90 + Math.random() * 170;
|
||||
const size = 2.5 + Math.random() * 5;
|
||||
const color = palette[Math.floor(Math.random() * palette.length)];
|
||||
const spark = this.add.circle(bx, by, size, color, 1).setDepth(DEPTH.popup + 2);
|
||||
this.tweens.add({
|
||||
targets: spark,
|
||||
x: bx + Math.cos(sparkAngle) * speed,
|
||||
y: by + Math.sin(sparkAngle) * speed + speed * 0.28, // slight gravity droop
|
||||
scaleX: 0.12, scaleY: 0.12,
|
||||
alpha: 0,
|
||||
duration: 600 + Math.random() * 480,
|
||||
ease: 'Cubic.easeOut',
|
||||
onComplete: () => { try { spark.destroy(); } catch { /* */ } },
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ── buy animation ────────────────────────────────────────────────────────────
|
||||
animBuy(seat, card, srcX, srcY, onDone) {
|
||||
if (!this.hasArt) { onDone(); return; }
|
||||
|
||||
const PW = 270, PH = 390;
|
||||
const cx = GAME_WIDTH / 2, cy = GAME_HEIGHT / 2;
|
||||
|
||||
for (const p of this.portraits) p?.hide();
|
||||
|
||||
const overlay = this.add.rectangle(cx, cy, GAME_WIDTH, GAME_HEIGHT, 0x000000)
|
||||
.setAlpha(0).setDepth(DEPTH.popup);
|
||||
this.tweens.add({ targets: overlay, alpha: 0.52, duration: 280 });
|
||||
|
||||
const container = this._buildCardContainer(srcX, srcY, card);
|
||||
|
||||
this.tweens.add({
|
||||
targets: container,
|
||||
x: cx, y: cy,
|
||||
scaleX: PW / CW, scaleY: PH / CH,
|
||||
duration: 420,
|
||||
ease: 'Cubic.easeOut',
|
||||
onComplete: () => {
|
||||
this._playReserveFireworks(cx, cy, card.bonus);
|
||||
|
||||
this.time.delayedCall(1200, () => {
|
||||
const bought = this.boughtCards[seat] ?? [];
|
||||
const bi = bought.findIndex((c) => c.id === card.id);
|
||||
const { cx: thumbCX, cy: thumbCY } = this.boughtCardThumbPos(seat, bi >= 0 ? bi : bought.length - 1);
|
||||
|
||||
this.tweens.add({ targets: overlay, alpha: 0, duration: 300,
|
||||
onComplete: () => { try { overlay.destroy(); } catch { /* */ } } });
|
||||
|
||||
this.tweens.add({
|
||||
targets: container,
|
||||
x: thumbCX, y: thumbCY,
|
||||
scaleX: THUMB_W / CW, scaleY: THUMB_H / CH,
|
||||
alpha: 0,
|
||||
duration: 360,
|
||||
ease: 'Cubic.easeIn',
|
||||
onComplete: () => {
|
||||
for (const p of this.portraits) p?.show();
|
||||
container.removeAll(true);
|
||||
container.destroy();
|
||||
onDone();
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// ── human input ─────────────────────────────────────────────────────────────
|
||||
onTokenClick(color) {
|
||||
if (!this.isHumanTurn()) return;
|
||||
|
|
@ -800,10 +1098,74 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
applyHuman(action) {
|
||||
this.selectedCard = null;
|
||||
this.selection = [];
|
||||
|
||||
if (this.hasArt && action.type === 'buy') {
|
||||
const seat = this.humanSeat;
|
||||
const card = this.findCard(action.cardId);
|
||||
const boardPos = this.findCardBoardPos(action.cardId);
|
||||
let srcX, srcY;
|
||||
if (boardPos) {
|
||||
srcX = boardPos.x; srcY = boardPos.y;
|
||||
} else {
|
||||
const n = this.gs.players.length, gap = 16;
|
||||
const h = Math.min(220, Math.floor((GAME_HEIGHT - 90 - gap * (n - 1)) / n));
|
||||
const panelY = 76 + seat * (h + gap);
|
||||
const ri = Math.max(0, this.gs.players[seat].reserved.findIndex((c) => c?.id === action.cardId));
|
||||
srcX = PANEL_X + 150 + ri * 116 + 52; srcY = panelY + h - 40 + 15;
|
||||
}
|
||||
|
||||
this.gs = applyAction(this.gs, action);
|
||||
this.playActionSound(action);
|
||||
if (this.gs.phase === 'discard' && this.gs.current === seat) { this.render(); return; }
|
||||
|
||||
if (!this.boughtCards[seat]) this.boughtCards[seat] = [];
|
||||
this.boughtCards[seat].push(card);
|
||||
this.animatingBuy = { seat, cardId: card.id };
|
||||
this.busy = true;
|
||||
this.render();
|
||||
|
||||
this.animBuy(seat, card, srcX, srcY, () => {
|
||||
this.animatingBuy = null;
|
||||
this.busy = false;
|
||||
this.render();
|
||||
this.advance();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hasArt && (action.type === 'reserve' || action.type === 'reserveDeck')) {
|
||||
const srcPos = action.type === 'reserve'
|
||||
? this.findCardBoardPos(action.cardId)
|
||||
: { x: MARKET_X + DECK_W / 2, y: TIER_Y[action.tier] + CH / 2 };
|
||||
const srcX = srcPos?.x ?? GAME_WIDTH / 2;
|
||||
const srcY = srcPos?.y ?? GAME_HEIGHT / 2;
|
||||
|
||||
this.gs = applyAction(this.gs, action);
|
||||
this.playActionSound(action);
|
||||
if (this.gs.phase === 'discard' && this.gs.current === this.humanSeat) {
|
||||
this.render(); // hand over to the discard UI
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
|
||||
const reserved = this.gs.players[this.humanSeat].reserved;
|
||||
const card = reserved[reserved.length - 1];
|
||||
this.animatingReserve = { seat: this.humanSeat, cardId: card.id };
|
||||
this.busy = true;
|
||||
this.render();
|
||||
|
||||
this.animReserve(this.humanSeat, card, srcX, srcY, () => {
|
||||
this.animatingReserve = null;
|
||||
this.busy = false;
|
||||
this.render();
|
||||
this.advance();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.gs = applyAction(this.gs, action);
|
||||
this.playActionSound(action);
|
||||
if (this.gs.phase === 'discard' && this.gs.current === this.humanSeat) {
|
||||
this.render();
|
||||
return;
|
||||
}
|
||||
this.render();
|
||||
|
|
@ -841,6 +1203,67 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
return;
|
||||
}
|
||||
|
||||
if (this.hasArt && (action.type === 'reserve' || action.type === 'reserveDeck')) {
|
||||
const srcPos = action.type === 'reserve'
|
||||
? this.findCardBoardPos(action.cardId)
|
||||
: { x: MARKET_X + DECK_W / 2, y: TIER_Y[action.tier] + CH / 2 };
|
||||
const srcX = srcPos?.x ?? GAME_WIDTH / 2;
|
||||
const srcY = srcPos?.y ?? GAME_HEIGHT / 2;
|
||||
|
||||
this.gs = applyAction(this.gs, action);
|
||||
this.playActionSound(action);
|
||||
if (this.gs.phase === 'discard') {
|
||||
this.gs = applyDiscard(this.gs, defaultDiscards(this.gs));
|
||||
}
|
||||
|
||||
const reserved = this.gs.players[seat].reserved;
|
||||
const card = reserved[reserved.length - 1];
|
||||
this.animatingReserve = { seat, cardId: card.id };
|
||||
this.render();
|
||||
|
||||
this.animReserve(seat, card, srcX, srcY, () => {
|
||||
this.animatingReserve = null;
|
||||
this.busy = false;
|
||||
this.render();
|
||||
this.advance();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hasArt && action.type === 'buy') {
|
||||
const card = this.findCard(action.cardId);
|
||||
const boardPos = this.findCardBoardPos(action.cardId);
|
||||
let srcX, srcY;
|
||||
if (boardPos) {
|
||||
srcX = boardPos.x; srcY = boardPos.y;
|
||||
} else {
|
||||
const n = this.gs.players.length, gap = 16;
|
||||
const h = Math.min(220, Math.floor((GAME_HEIGHT - 90 - gap * (n - 1)) / n));
|
||||
const panelY = 76 + seat * (h + gap);
|
||||
const ri = Math.max(0, this.gs.players[seat].reserved.findIndex((c) => c?.id === action.cardId));
|
||||
srcX = PANEL_X + 150 + ri * 116 + 52; srcY = panelY + h - 40 + 15;
|
||||
}
|
||||
|
||||
this.gs = applyAction(this.gs, action);
|
||||
this.playActionSound(action);
|
||||
if (this.gs.phase === 'discard') {
|
||||
this.gs = applyDiscard(this.gs, defaultDiscards(this.gs));
|
||||
}
|
||||
|
||||
if (!this.boughtCards[seat]) this.boughtCards[seat] = [];
|
||||
this.boughtCards[seat].push(card);
|
||||
this.animatingBuy = { seat, cardId: card.id };
|
||||
this.render();
|
||||
|
||||
this.animBuy(seat, card, srcX, srcY, () => {
|
||||
this.animatingBuy = null;
|
||||
this.busy = false;
|
||||
this.render();
|
||||
this.advance();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
this.gs = applyAction(this.gs, action);
|
||||
this.playActionSound(action);
|
||||
if (this.gs.phase === 'discard') {
|
||||
|
|
|
|||
Loading…
Reference in New Issue