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 };
|
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);
|
const hexStr = (n) => '#' + (n >>> 0).toString(16).padStart(6, '0').slice(-6);
|
||||||
|
|
||||||
export default class SplendorGame extends Phaser.Scene {
|
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.dyn = []; // dynamic objects rebuilt every render
|
||||||
this.preview = []; // hover card preview — cleared separately
|
this.preview = []; // hover card preview — cleared separately
|
||||||
this.portraits = []; // one per seat, created once
|
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() {
|
create() {
|
||||||
|
|
@ -142,6 +156,40 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
this.preview = [];
|
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) {
|
showCardPreview(card, barCenterX, barBottomY) {
|
||||||
this.clearPreview();
|
this.clearPreview();
|
||||||
const pw = 270, ph = 390;
|
const pw = 270, ph = 390;
|
||||||
|
|
@ -468,6 +516,30 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
}).setOrigin(0.5).setDepth(DEPTH.ui + 2));
|
}).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
|
// reserved cards
|
||||||
const rN = p.reserved.length;
|
const rN = p.reserved.length;
|
||||||
this.reg(this.add.text(x + 16, y + h - 30, `Reserved: ${rN}/${MAX_RESERVED}`, {
|
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) {
|
if (rN > 0) {
|
||||||
const isHuman = idx === this.humanSeat;
|
const isHuman = idx === this.humanSeat;
|
||||||
p.reserved.forEach((card, ri) => {
|
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 rx = x + 150 + ri * 116, ry = y + h - 40;
|
||||||
const canBuy = isHuman && this.isHumanTurn() && canAfford(p, card);
|
const canBuy = isHuman && this.isHumanTurn() && canAfford(p, card);
|
||||||
const mg = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1));
|
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 ─────────────────────────────────────────────────────────────
|
// ── human input ─────────────────────────────────────────────────────────────
|
||||||
onTokenClick(color) {
|
onTokenClick(color) {
|
||||||
if (!this.isHumanTurn()) return;
|
if (!this.isHumanTurn()) return;
|
||||||
|
|
@ -800,10 +1098,74 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
applyHuman(action) {
|
applyHuman(action) {
|
||||||
this.selectedCard = null;
|
this.selectedCard = null;
|
||||||
this.selection = [];
|
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.gs = applyAction(this.gs, action);
|
||||||
this.playActionSound(action);
|
this.playActionSound(action);
|
||||||
if (this.gs.phase === 'discard' && this.gs.current === this.humanSeat) {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
this.render();
|
this.render();
|
||||||
|
|
@ -841,6 +1203,67 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
return;
|
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.gs = applyAction(this.gs, action);
|
||||||
this.playActionSound(action);
|
this.playActionSound(action);
|
||||||
if (this.gs.phase === 'discard') {
|
if (this.gs.phase === 'discard') {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue