Compare commits
No commits in common. "25ccdb5da9ad913b0c04c85825f8c8551b3d09ec" and "e2b28b6c0e473fc5adfcc66e1283716cafd05eb5" have entirely different histories.
25ccdb5da9
...
e2b28b6c0e
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
|
@ -24,7 +24,6 @@ const MARKET_X = 80; // left edge of decks
|
|||
const CARDS_X = MARKET_X + DECK_W + 24; // first face-up card
|
||||
const TIER_Y = { 3: 168, 2: 168 + CH + 26, 1: 168 + (CH + 26) * 2 };
|
||||
const NOBLE = 96, NOBLE_Y = 56;
|
||||
const NOBLE_THUMB = 34, NOBLE_THUMB_GAP = 4; // square noble thumbnails in player panels
|
||||
|
||||
const BANK_X = 830, BANK_Y0 = 176, BANK_STEP = 88, TOKEN_R = 36;
|
||||
|
||||
|
|
@ -33,17 +32,6 @@ 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 {
|
||||
|
|
@ -59,15 +47,9 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
this.busy = false;
|
||||
this.selection = []; // colours chosen for a take action
|
||||
this.selectedCard = null; // { card, source } chosen to buy/reserve
|
||||
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
|
||||
this._thumbHoverTimer = null; // pending 500 ms intent timer for thumbnail preview
|
||||
this.animatingNoble = null; // { seat, nobleId } suppresses noble thumbnail during animation
|
||||
this._pendingNoble = null; // { seat, noble, srcX, srcY } queued noble animation
|
||||
this.dyn = []; // dynamic objects rebuilt every render
|
||||
this.preview = []; // hover card preview — cleared separately
|
||||
this.portraits = []; // one per seat, created once
|
||||
}
|
||||
|
||||
create() {
|
||||
|
|
@ -112,7 +94,7 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
this.portraits[idx] = createPlayerPortrait(this, px, py, portraitR, DEPTH.ui + 1, 'SplendorGame');
|
||||
} else {
|
||||
const opp = this.opponents[idx - 1];
|
||||
this.portraits[idx] = createOpponentPortrait(this, opp, px, py, portraitR, DEPTH.ui + 1);
|
||||
this.portraits[idx] = createOpponentPortrait(this, opp, px, py, portraitR, DEPTH.ui + 1, { playIntro: false });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
@ -132,26 +114,15 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
fontFamily: 'Righteous', fontSize: '40px', color: COLORS.textHex,
|
||||
}).setOrigin(0.5, 0).setDepth(DEPTH.ui);
|
||||
|
||||
new Button(this, GAME_WIDTH - 96, GAME_HEIGHT - 36, 'Leave', () => this.scene.start('GameMenu'),
|
||||
new Button(this, GAME_WIDTH - 96, 40, 'Leave', () => this.scene.start('GameMenu'),
|
||||
{ variant: 'ghost', width: 140, height: 42, fontSize: 18 }).setDepth(DEPTH.ui);
|
||||
}
|
||||
|
||||
// ── render: rebuild all dynamic visuals from this.gs ─────────────────────────
|
||||
reg(obj) { this.dyn.push(obj); return obj; }
|
||||
|
||||
regMaskedImg(x, y, w, h, key, frame, depth, r = 10) {
|
||||
const maskG = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
maskG.fillStyle(0xffffff).fillRoundedRect(x, y, w, h, r);
|
||||
this.dyn.push({ destroy: () => maskG.destroy() });
|
||||
return this.reg(
|
||||
this.add.image(x + w / 2, y + h / 2, key, frame)
|
||||
.setDisplaySize(w, h).setMask(maskG.createGeometryMask()).setDepth(depth)
|
||||
);
|
||||
}
|
||||
|
||||
clearDyn() {
|
||||
this.clearPreview();
|
||||
if (this._thumbHoverTimer) { this._thumbHoverTimer.remove(); this._thumbHoverTimer = null; }
|
||||
for (const o of this.dyn) { try { o.destroy(); } catch { /* noop */ } }
|
||||
this.dyn = [];
|
||||
}
|
||||
|
|
@ -161,76 +132,6 @@ 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;
|
||||
}
|
||||
|
||||
// Wraps applyAction and detects if a noble was earned this turn.
|
||||
_applyAction(action) {
|
||||
const prevNobles = this.gs.nobles.slice();
|
||||
const prevCounts = this.gs.players.map((p) => p.nobles.length);
|
||||
const next = applyAction(this.gs, action);
|
||||
this.gs = next;
|
||||
for (let seat = 0; seat < this.gs.players.length; seat++) {
|
||||
if (this.gs.players[seat].nobles.length > prevCounts[seat]) {
|
||||
const claimed = prevNobles.find((pn) => !this.gs.nobles.some((n) => n.id === pn.id));
|
||||
if (claimed) {
|
||||
const si = prevNobles.indexOf(claimed);
|
||||
this._pendingNoble = {
|
||||
seat,
|
||||
noble: claimed,
|
||||
srcX: MARKET_X + si * (NOBLE + 14) + NOBLE / 2,
|
||||
srcY: NOBLE_Y + NOBLE / 2,
|
||||
};
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
nobleThumbPos(seat, nobleIdx) {
|
||||
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 totalCY = panelY + 70 + 57;
|
||||
const nobleCount = this.gs.players[seat].nobles.length;
|
||||
const groupW = nobleCount * NOBLE_THUMB + Math.max(0, nobleCount - 1) * NOBLE_THUMB_GAP;
|
||||
const tx = PANEL_X + PANEL_W - 16 - groupW + nobleIdx * (NOBLE_THUMB + NOBLE_THUMB_GAP);
|
||||
const ty = totalCY + 14 + 3;
|
||||
return { cx: tx + NOBLE_THUMB / 2, cy: ty + NOBLE_THUMB / 2 };
|
||||
}
|
||||
|
||||
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;
|
||||
|
|
@ -252,16 +153,12 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
if (!this.hasArt) {
|
||||
g.fillStyle(tint, 0.9).fillRoundedRect(px, py, pw, 65, { tl: 10, tr: 10, bl: 0, br: 0 });
|
||||
}
|
||||
g.lineStyle(3, GEM_EDGE[card.bonus], 1).strokeRoundedRect(px, py, pw, ph, 10);
|
||||
|
||||
if (this.hasArt) {
|
||||
const maskG = this.make.graphics({ x: 0, y: 0, add: false });
|
||||
maskG.fillStyle(0xffffff).fillRoundedRect(px, py, pw, ph, 10);
|
||||
this.preview.push({ destroy: () => maskG.destroy() });
|
||||
rp(this.add.image(px + pw / 2, py + ph / 2, 'splendor-cards', cardFrame(card))
|
||||
.setDisplaySize(pw, ph).setMask(maskG.createGeometryMask()).setDepth(DEPTH.popup));
|
||||
.setDisplaySize(pw, ph).setDepth(DEPTH.popup));
|
||||
}
|
||||
// Border drawn above art
|
||||
rp(this.add.graphics().setDepth(DEPTH.popup + 1))
|
||||
.lineStyle(3, GEM_EDGE[card.bonus], 1).strokeRoundedRect(px, py, pw, ph, 10);
|
||||
|
||||
// Points
|
||||
if (card.points > 0) {
|
||||
|
|
@ -309,13 +206,12 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
this.gs.nobles.forEach((n, i) => {
|
||||
const x = MARKET_X + i * (NOBLE + 14);
|
||||
const g = this.reg(this.add.graphics().setDepth(DEPTH.card));
|
||||
g.fillStyle(0x000000, 0.45).fillRoundedRect(x + 3, NOBLE_Y + 4, NOBLE, NOBLE, 10);
|
||||
g.fillStyle(0x2b2620, 1).fillRoundedRect(x, NOBLE_Y, NOBLE, NOBLE, 10);
|
||||
g.lineStyle(2, COLORS.accent, 0.8).strokeRoundedRect(x, NOBLE_Y, NOBLE, NOBLE, 10);
|
||||
if (this.hasArt) {
|
||||
this.regMaskedImg(x, NOBLE_Y, NOBLE, NOBLE, 'splendor-cards', nobleFrame(n), DEPTH.card + 1);
|
||||
this.reg(this.add.image(x + NOBLE / 2, NOBLE_Y + NOBLE / 2, 'splendor-cards', nobleFrame(n))
|
||||
.setDisplaySize(NOBLE, NOBLE).setDepth(DEPTH.card + 1));
|
||||
}
|
||||
this.reg(this.add.graphics().setDepth(DEPTH.card + 2))
|
||||
.lineStyle(1.5, COLORS.accent, 0.8).strokeRoundedRect(x, NOBLE_Y, NOBLE, NOBLE, 10);
|
||||
// prestige (always drawn over art)
|
||||
this.reg(this.add.text(x + 8, NOBLE_Y + 4, '3', {
|
||||
fontFamily: 'Righteous', fontSize: '22px', color: COLORS.goldHex,
|
||||
|
|
@ -342,15 +238,13 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
// deck pile
|
||||
const deckN = this.gs.decks[tier].length;
|
||||
const dg = this.reg(this.add.graphics().setDepth(DEPTH.card));
|
||||
dg.fillStyle(0x000000, 0.45).fillRoundedRect(MARKET_X + 3, y + 4, DECK_W, CH, 10);
|
||||
dg.fillStyle(tier === 3 ? 0x2a3550 : tier === 2 ? 0x3a4a2a : 0x4a3a2a, 1)
|
||||
.fillRoundedRect(MARKET_X, y, DECK_W, CH, 10);
|
||||
dg.lineStyle(2, 0x000000, 0.6).strokeRoundedRect(MARKET_X, y, DECK_W, CH, 10);
|
||||
if (deckN > 0 && this.hasArt) {
|
||||
this.regMaskedImg(MARKET_X, y, DECK_W, CH, 'splendor-cards', deckBackFrame(tier), DEPTH.card + 1);
|
||||
this.reg(this.add.image(MARKET_X + DECK_W / 2, y + CH / 2, 'splendor-cards', deckBackFrame(tier))
|
||||
.setDisplaySize(DECK_W, CH).setDepth(DEPTH.card + 1));
|
||||
}
|
||||
this.reg(this.add.graphics().setDepth(DEPTH.card + 2))
|
||||
.lineStyle(1.5, tier === 3 ? 0x4a6090 : tier === 2 ? 0x5a7040 : 0x705030, 0.8)
|
||||
.strokeRoundedRect(MARKET_X, y, DECK_W, CH, 10);
|
||||
this.reg(this.add.text(MARKET_X + DECK_W / 2, y + 30, '★'.repeat(tier), {
|
||||
fontFamily: 'Righteous', fontSize: '20px', color: COLORS.goldHex,
|
||||
}).setOrigin(0.5).setDepth(DEPTH.card + 2));
|
||||
|
|
@ -380,20 +274,18 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
// Card background — always draw the tinted rect (provides border shape and colour identity)
|
||||
const g = this.reg(this.add.graphics().setDepth(DEPTH.card + 1));
|
||||
const tint = GEM_HEX[card.bonus];
|
||||
g.fillStyle(0x000000, 0.45).fillRoundedRect(x + 3, y + 4, CW, CH, 10);
|
||||
g.fillStyle(0x14110b, 1).fillRoundedRect(x, y, CW, CH, 10);
|
||||
g.fillStyle(tint, 0.20).fillRoundedRect(x, y, CW, CH, 10);
|
||||
if (!this.hasArt) {
|
||||
g.fillStyle(tint, 0.9).fillRoundedRect(x, y, CW, 30, { tl: 10, tr: 10, bl: 0, br: 0 });
|
||||
}
|
||||
g.lineStyle(2, GEM_EDGE[card.bonus], 1).strokeRoundedRect(x, y, CW, CH, 10);
|
||||
|
||||
// Spritesheet art clipped to rounded rect
|
||||
// Spritesheet background art drawn over the tinted rect when available
|
||||
if (this.hasArt) {
|
||||
this.regMaskedImg(x, y, CW, CH, 'splendor-cards', cardFrame(card), DEPTH.card + 1);
|
||||
this.reg(this.add.image(x + CW / 2, y + CH / 2, 'splendor-cards', cardFrame(card))
|
||||
.setDisplaySize(CW, CH).setDepth(DEPTH.card + 1));
|
||||
}
|
||||
// Border drawn on top of art
|
||||
this.reg(this.add.graphics().setDepth(DEPTH.card + 2))
|
||||
.lineStyle(1.5, GEM_EDGE[card.bonus], 1).strokeRoundedRect(x, y, CW, CH, 10);
|
||||
|
||||
// Vector overlays — always drawn on top of whatever background is present
|
||||
if (card.points > 0) {
|
||||
|
|
@ -474,13 +366,8 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
}).setOrigin(1, 0).setDepth(DEPTH.card + 3));
|
||||
}
|
||||
if (this.isHumanTurn() && color !== GOLD && n > 0) {
|
||||
const ring = this.reg(this.add.graphics().setDepth(DEPTH.card + 3).setAlpha(0));
|
||||
ring.lineStyle(4, 0xffffff, 1).strokeCircle(cx, cy, TOKEN_R + 7);
|
||||
|
||||
const zone = this.reg(this.add.zone(cx, cy, TOKEN_R * 2, TOKEN_R * 2)
|
||||
.setInteractive({ useHandCursor: true }).setDepth(DEPTH.card + 4));
|
||||
zone.on('pointerover', () => ring.setAlpha(1));
|
||||
zone.on('pointerout', () => ring.setAlpha(0));
|
||||
.setInteractive({ useHandCursor: true }).setDepth(DEPTH.card + 3));
|
||||
zone.on('pointerdown', () => this.onTokenClick(color));
|
||||
}
|
||||
});
|
||||
|
|
@ -512,10 +399,9 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
|
||||
// gem cells: cards (bonus) over tokens — compact, right-aligned
|
||||
const order = [...GEMS, GOLD];
|
||||
const gemR = 10;
|
||||
const totalR = 14;
|
||||
const gemR = 14;
|
||||
const gemSpacing = 36;
|
||||
const gemStartX = x + w - 16 - totalR - (order.length - 1) * gemSpacing;
|
||||
const gemStartX = x + w - 16 - gemR - (order.length - 1) * gemSpacing;
|
||||
order.forEach((color, ci) => {
|
||||
const ccx = gemStartX + ci * gemSpacing;
|
||||
const ccy = y + 70;
|
||||
|
|
@ -523,14 +409,14 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
const toks = p.tokens[color] ?? 0;
|
||||
const cg = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1));
|
||||
cg.fillStyle(GEM_HEX[color], 1).fillCircle(ccx, ccy, gemR);
|
||||
cg.lineStyle(1.5, GEM_EDGE[color], 1).strokeCircle(ccx, ccy, gemR);
|
||||
cg.lineStyle(2, GEM_EDGE[color], 1).strokeCircle(ccx, ccy, gemR);
|
||||
if (color !== GOLD) {
|
||||
this.reg(this.add.text(ccx, ccy, String(cards), {
|
||||
fontFamily: 'Righteous', fontSize: '12px',
|
||||
fontFamily: 'Righteous', fontSize: '14px',
|
||||
color: color === 'white' ? '#222' : '#fff',
|
||||
}).setOrigin(0.5).setDepth(DEPTH.ui + 2));
|
||||
}
|
||||
this.reg(this.add.text(ccx, ccy + 22, `${toks}`, {
|
||||
this.reg(this.add.text(ccx, ccy + 24, `${toks}`, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.textHex,
|
||||
}).setOrigin(0.5).setDepth(DEPTH.ui + 2));
|
||||
// discard interactivity (human, over limit)
|
||||
|
|
@ -546,85 +432,22 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
// divider + total row (card bonuses + tokens)
|
||||
const ccy = y + 70;
|
||||
const divY = ccy + 38;
|
||||
const totalCY = ccy + 57;
|
||||
const totalCY = ccy + 53;
|
||||
const divG = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1));
|
||||
divG.lineStyle(1, COLORS.accent, 0.35)
|
||||
.lineBetween(gemStartX - totalR - 2, divY, gemStartX + (order.length - 1) * gemSpacing + totalR + 2, divY);
|
||||
.lineBetween(gemStartX - gemR - 2, divY, gemStartX + (order.length - 1) * gemSpacing + gemR + 2, divY);
|
||||
order.forEach((color, ci) => {
|
||||
const ccx = gemStartX + ci * gemSpacing;
|
||||
const total = (color === GOLD ? 0 : (p.bonuses[color] ?? 0)) + (p.tokens[color] ?? 0);
|
||||
const tg = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1));
|
||||
tg.fillStyle(GEM_HEX[color], total > 0 ? 0.8 : 0.2).fillCircle(ccx, totalCY, totalR);
|
||||
tg.lineStyle(2, GEM_EDGE[color], 1).strokeCircle(ccx, totalCY, totalR);
|
||||
tg.fillStyle(GEM_HEX[color], total > 0 ? 0.8 : 0.2).fillCircle(ccx, totalCY, 10);
|
||||
tg.lineStyle(1.5, GEM_EDGE[color], 1).strokeCircle(ccx, totalCY, 10);
|
||||
this.reg(this.add.text(ccx, totalCY, String(total), {
|
||||
fontFamily: 'Righteous', fontSize: '14px',
|
||||
fontFamily: 'Righteous', fontSize: '12px',
|
||||
color: color === 'white' ? '#222' : '#fff',
|
||||
}).setOrigin(0.5).setDepth(DEPTH.ui + 2));
|
||||
});
|
||||
|
||||
// noble thumbnails — right-aligned below the total circles row
|
||||
if (p.nobles.length > 0) {
|
||||
const nobleCount = p.nobles.length;
|
||||
const nobleGroupW = nobleCount * NOBLE_THUMB + (nobleCount - 1) * NOBLE_THUMB_GAP;
|
||||
const nobleGroupLeft = x + w - 16 - nobleGroupW;
|
||||
const nTY = totalCY + 14 + 3;
|
||||
p.nobles.forEach((noble, ni) => {
|
||||
if (this.animatingNoble?.seat === idx && this.animatingNoble?.nobleId === noble.id) return;
|
||||
const nTX = nobleGroupLeft + ni * (NOBLE_THUMB + NOBLE_THUMB_GAP);
|
||||
const ng = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1));
|
||||
ng.fillStyle(0x000000, 0.32).fillRoundedRect(nTX + 2, nTY + 2, NOBLE_THUMB, NOBLE_THUMB, 4);
|
||||
ng.fillStyle(0x2b2620, 1).fillRoundedRect(nTX, nTY, NOBLE_THUMB, NOBLE_THUMB, 4);
|
||||
if (this.hasArt) {
|
||||
this.regMaskedImg(nTX, nTY, NOBLE_THUMB, NOBLE_THUMB, 'splendor-cards', nobleFrame(noble), DEPTH.ui + 2, 4);
|
||||
}
|
||||
this.reg(this.add.graphics().setDepth(DEPTH.ui + 3))
|
||||
.lineStyle(1.5, COLORS.accent, 0.85).strokeRoundedRect(nTX, nTY, NOBLE_THUMB, NOBLE_THUMB, 4);
|
||||
this.reg(this.add.text(nTX + 3, nTY + 1, '3', {
|
||||
fontFamily: 'Righteous', fontSize: '10px', color: COLORS.goldHex,
|
||||
}).setDepth(DEPTH.ui + 4));
|
||||
});
|
||||
}
|
||||
|
||||
// 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));
|
||||
}
|
||||
// 500 ms hover-intent preview
|
||||
const tz = this.reg(
|
||||
this.add.zone(tx + THUMB_W / 2, ty + THUMB_H / 2, THUMB_W, THUMB_H)
|
||||
.setInteractive().setDepth(DEPTH.ui + 5)
|
||||
);
|
||||
tz.on('pointerover', () => {
|
||||
this._thumbHoverTimer = this.time.delayedCall(500, () => {
|
||||
for (const p of this.portraits) p?.hide();
|
||||
this.showCardPreview(bc, tx + THUMB_W / 2, ty + THUMB_H);
|
||||
});
|
||||
});
|
||||
tz.on('pointerout', () => {
|
||||
if (this._thumbHoverTimer) { this._thumbHoverTimer.remove(); this._thumbHoverTimer = null; }
|
||||
this.clearPreview();
|
||||
for (const p of this.portraits) p?.show();
|
||||
});
|
||||
});
|
||||
|
||||
// reserved cards
|
||||
const rN = p.reserved.length;
|
||||
this.reg(this.add.text(x + 16, y + h - 30, `Reserved: ${rN}/${MAX_RESERVED}`, {
|
||||
|
|
@ -633,7 +456,6 @@ 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));
|
||||
|
|
@ -660,13 +482,9 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
|
||||
// ── bottom control bar ──────────────────────────────────────────────────────
|
||||
drawControlBar() {
|
||||
const n = this.gs?.players?.length ?? 4;
|
||||
const panelGap = 16;
|
||||
const panelH = Math.min(220, Math.floor((GAME_HEIGHT - 90 - panelGap * (n - 1)) / n));
|
||||
const lastPanelBottom = 76 + (n - 1) * (panelH + panelGap) + panelH;
|
||||
const x = MARKET_X, y = CONTROL_Y, w = 920, h = Math.max(120, lastPanelBottom - y);
|
||||
const x = MARKET_X, y = CONTROL_Y, w = 920, h = GAME_HEIGHT - y - 24;
|
||||
const g = this.reg(this.add.graphics().setDepth(DEPTH.ui));
|
||||
g.fillStyle(0x000000, 0.55).fillRoundedRect(x, y, w, h, 12);
|
||||
g.fillStyle(0x000000, 0.3).fillRoundedRect(x, y, w, h, 12);
|
||||
g.lineStyle(1, COLORS.accent, 0.4).strokeRoundedRect(x, y, w, h, 12);
|
||||
|
||||
if (isGameOver(this.gs)) return;
|
||||
|
|
@ -706,10 +524,10 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
bx += 170;
|
||||
}
|
||||
if (source === 'board' && human.reserved.length < MAX_RESERVED) {
|
||||
this.addButton(bx + 115, y + 70, 'Reserve (+1 gold)',
|
||||
this.addButton(bx + 80, y + 70, 'Reserve (+1 gold)',
|
||||
() => this.applyHuman({ type: 'reserve', cardId: card.id, tier: card.tier }),
|
||||
{ width: 230, height: 46, variant: 'ghost' });
|
||||
bx += 285;
|
||||
bx += 250;
|
||||
}
|
||||
this.addButton(bx + 70, y + 70, 'Cancel', () => { this.selectedCard = null; this.render(); },
|
||||
{ width: 130, height: 46, variant: 'ghost' });
|
||||
|
|
@ -748,9 +566,12 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
|
||||
drawTurnLabel() {
|
||||
const txt = isGameOver(this.gs) ? 'Game over' : `${this.pname(this.gs.current)}'s turn`;
|
||||
this.reg(this.add.text(BANK_X, GAME_HEIGHT - 48, txt, {
|
||||
this.reg(this.add.text(BANK_X, GAME_HEIGHT - 70, txt, {
|
||||
fontFamily: 'Righteous', fontSize: '22px', color: COLORS.textHex,
|
||||
}).setOrigin(0, 0.5).setDepth(DEPTH.ui + 1));
|
||||
this.reg(this.add.text(BANK_X, GAME_HEIGHT - 40, `First to ${WIN_POINTS} prestige wins`, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex,
|
||||
}).setOrigin(0, 0.5).setDepth(DEPTH.ui + 1));
|
||||
}
|
||||
|
||||
// ── helpers ─────────────────────────────────────────────────────────────────
|
||||
|
|
@ -765,11 +586,9 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
const gap = 16;
|
||||
const h = Math.min(220, Math.floor((GAME_HEIGHT - 90 - gap * (n - 1)) / n));
|
||||
const order = [...GEMS, GOLD];
|
||||
const totalR = 14;
|
||||
const gemSpacing = 36;
|
||||
const gemStartX = PANEL_X + PANEL_W - 16 - totalR - (order.length - 1) * gemSpacing;
|
||||
const cellW = (PANEL_W - 32) / order.length;
|
||||
const ci = order.indexOf(color);
|
||||
return { x: gemStartX + ci * gemSpacing, y: 76 + seat * (h + gap) + 70 };
|
||||
return { x: PANEL_X + 16 + cellW * ci + cellW / 2, y: 76 + seat * (h + gap) + 70 };
|
||||
}
|
||||
|
||||
humanGemPos(color) { return this.playerGemPos(this.humanSeat, color); }
|
||||
|
|
@ -781,7 +600,7 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
: action.colors;
|
||||
|
||||
const preTokens = { ...this.gs.players[seat].tokens };
|
||||
this._applyAction(action);
|
||||
this.gs = applyAction(this.gs, action);
|
||||
this.playActionSound(action);
|
||||
if (this.gs.phase === 'discard') {
|
||||
this.gs = applyDiscard(this.gs, defaultDiscards(this.gs));
|
||||
|
|
@ -807,12 +626,6 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
duration: 380,
|
||||
delay: i * 90,
|
||||
ease: 'Cubic.easeOut',
|
||||
onStart: () => {
|
||||
try {
|
||||
const a = new Audio(`/assets/fx/gem-0${(i % 2) + 1}.mp3`);
|
||||
a.volume = 0.7; a.play();
|
||||
} catch { /* */ }
|
||||
},
|
||||
onComplete: () => {
|
||||
gem.destroy();
|
||||
if (--pending === 0) {
|
||||
|
|
@ -840,311 +653,10 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
displayWidth: 32, displayHeight: 32,
|
||||
duration: 300,
|
||||
ease: 'Cubic.easeOut',
|
||||
onStart: () => {
|
||||
try {
|
||||
const a = new Audio(`/assets/fx/gem-0${(destIdx % 2) + 1}.mp3`);
|
||||
a.volume = 0.7; a.play();
|
||||
} catch { /* */ }
|
||||
},
|
||||
onComplete: () => gem.destroy(),
|
||||
});
|
||||
}
|
||||
|
||||
// ── noble animation ──────────────────────────────────────────────────────────
|
||||
animNoble(seat, noble, srcX, srcY, onDone) {
|
||||
const DISP = 270;
|
||||
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.add.container(srcX, srcY).setDepth(DEPTH.popup + 1);
|
||||
const bg = this.add.graphics();
|
||||
bg.fillStyle(0x2b2620, 1).fillRoundedRect(-NOBLE / 2, -NOBLE / 2, NOBLE, NOBLE, 10);
|
||||
container.add(bg);
|
||||
if (this.hasArt) {
|
||||
container.add(
|
||||
this.add.image(0, 0, 'splendor-cards', nobleFrame(noble)).setDisplaySize(NOBLE, NOBLE)
|
||||
);
|
||||
}
|
||||
const border = this.add.graphics();
|
||||
border.lineStyle(2, COLORS.accent, 0.9).strokeRoundedRect(-NOBLE / 2, -NOBLE / 2, NOBLE, NOBLE, 10);
|
||||
container.add(border);
|
||||
container.add(this.add.text(-NOBLE / 2 + 8, -NOBLE / 2 + 4, '3', {
|
||||
fontFamily: 'Righteous', fontSize: '22px', color: COLORS.goldHex,
|
||||
}));
|
||||
|
||||
this.tweens.add({
|
||||
targets: container,
|
||||
x: cx, y: cy,
|
||||
scale: DISP / NOBLE,
|
||||
duration: 420,
|
||||
ease: 'Cubic.easeOut',
|
||||
onComplete: () => {
|
||||
this._playReserveFireworks(cx, cy, 'white');
|
||||
try { const a = new Audio('/assets/fx/firework.mp3'); a.volume = 0.7; a.play(); } catch { /* */ }
|
||||
|
||||
this.time.delayedCall(1200, () => {
|
||||
const ni = this.gs.players[seat].nobles.findIndex((n) => n.id === noble.id);
|
||||
const { cx: thumbCX, cy: thumbCY } = this.nobleThumbPos(seat, ni >= 0 ? ni : 0);
|
||||
|
||||
this.tweens.add({ targets: overlay, alpha: 0, duration: 300,
|
||||
onComplete: () => { try { overlay.destroy(); } catch { /* */ } } });
|
||||
try { const a = new Audio('/assets/fx/ui-attach.mp3'); a.volume = 0.8; a.play(); } catch { /* */ }
|
||||
|
||||
this.tweens.add({
|
||||
targets: container,
|
||||
x: thumbCX, y: thumbCY,
|
||||
scale: NOBLE_THUMB / NOBLE,
|
||||
alpha: 0,
|
||||
duration: 360,
|
||||
ease: 'Cubic.easeIn',
|
||||
onComplete: () => {
|
||||
for (const p of this.portraits) p?.show();
|
||||
container.removeAll(true);
|
||||
container.destroy();
|
||||
onDone();
|
||||
},
|
||||
});
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
// 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 { /* */ } } });
|
||||
|
||||
try { const a = new Audio('/assets/fx/ui-attach.mp3'); a.volume = 0.8; a.play(); } 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, () => {
|
||||
try { const fw = new Audio('/assets/fx/firework.mp3'); fw.volume = 0.7; fw.play(); } catch { /* */ }
|
||||
|
||||
// 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 { /* */ } } });
|
||||
|
||||
try { const a = new Audio('/assets/fx/ui-attach.mp3'); a.volume = 0.8; a.play(); } 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;
|
||||
|
|
@ -1186,7 +698,7 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
|
||||
// Save token state before and after applying the action
|
||||
const preTokens = { ...this.gs.players[this.humanSeat].tokens };
|
||||
this._applyAction(action);
|
||||
this.gs = applyAction(this.gs, action);
|
||||
this.playActionSound(action);
|
||||
const postTokens = this.gs.players[this.humanSeat].tokens;
|
||||
|
||||
|
|
@ -1216,12 +728,6 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
duration: 380,
|
||||
delay: i * 90,
|
||||
ease: 'Cubic.easeOut',
|
||||
onStart: () => {
|
||||
try {
|
||||
const a = new Audio(`/assets/fx/gem-0${(i % 2) + 1}.mp3`);
|
||||
a.volume = 0.7; a.play();
|
||||
} catch { /* */ }
|
||||
},
|
||||
onComplete: () => {
|
||||
gem.destroy();
|
||||
if (--pending === 0) {
|
||||
|
|
@ -1272,74 +778,10 @@ 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._applyAction(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._applyAction(action);
|
||||
this.playActionSound(action);
|
||||
if (this.gs.phase === 'discard' && this.gs.current === this.humanSeat) {
|
||||
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._applyAction(action);
|
||||
this.gs = applyAction(this.gs, action);
|
||||
this.playActionSound(action);
|
||||
if (this.gs.phase === 'discard' && this.gs.current === this.humanSeat) {
|
||||
this.render();
|
||||
this.render(); // hand over to the discard UI
|
||||
return;
|
||||
}
|
||||
this.render();
|
||||
|
|
@ -1348,20 +790,6 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
|
||||
// ── turn loop ───────────────────────────────────────────────────────────────
|
||||
advance() {
|
||||
if (this._pendingNoble) {
|
||||
const { seat, noble, srcX, srcY } = this._pendingNoble;
|
||||
this._pendingNoble = null;
|
||||
this.animatingNoble = { seat, nobleId: noble.id };
|
||||
this.busy = true;
|
||||
this.render();
|
||||
this.animNoble(seat, noble, srcX, srcY, () => {
|
||||
this.animatingNoble = null;
|
||||
this.busy = false;
|
||||
this.render();
|
||||
this.advance();
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (isGameOver(this.gs)) { this.render(); this.onGameOver(); return; }
|
||||
if (this.gs.phase === 'discard') {
|
||||
// Only the human ever parks here interactively; AI auto-discards inline.
|
||||
|
|
@ -1391,73 +819,7 @@ 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._applyAction(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._applyAction(action);
|
||||
this.playActionSound(action);
|
||||
if (this.gs.phase === 'discard') {
|
||||
this.gs = applyDiscard(this.gs, defaultDiscards(this.gs));
|
||||
}
|
||||
|
||||
if (card.points > 0) this.portraits[seat]?.playEmotion('happy');
|
||||
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;
|
||||
}
|
||||
|
||||
if (action.type === 'buy') {
|
||||
const card = this.findCard(action.cardId);
|
||||
if (card?.points > 0) this.portraits[seat]?.playEmotion('happy');
|
||||
}
|
||||
this._applyAction(action);
|
||||
this.gs = applyAction(this.gs, action);
|
||||
this.playActionSound(action);
|
||||
if (this.gs.phase === 'discard') {
|
||||
this.gs = applyDiscard(this.gs, defaultDiscards(this.gs));
|
||||
|
|
@ -1472,7 +834,7 @@ export default class SplendorGame extends Phaser.Scene {
|
|||
if (!action) return;
|
||||
if (action.type === 'buy') playSound(this, SFX.PURCHASE);
|
||||
else if (action.type === 'reserve' || action.type === 'reserveDeck') playSound(this, SFX.CARD_DEAL);
|
||||
// gem sounds played per-gem in animAITake / confirmTake
|
||||
else if (action.type === 'take2' || action.type === 'take3') playSound(this, SFX.COINS);
|
||||
}
|
||||
|
||||
showTurnBanner(text) {
|
||||
|
|
|
|||
|
|
@ -139,7 +139,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
'selectedPlayfield', 'playfieldTiles', (pf) => this.selectPlayfield(pf));
|
||||
}
|
||||
|
||||
if (this.gameDef.cardGame && this.gameDef.slug !== 'splendor') {
|
||||
if (this.gameDef.cardGame) {
|
||||
this.buildOptionSection('Card Back', 798, this.cache.json.get('card-backs')?.cardBacks ?? [],
|
||||
'selectedCardBack', 'cardBackTiles', (cb) => this.selectCardBack(cb),
|
||||
CARD_TILE_W, CARD_TILE_H, CARD_TILE_GAP);
|
||||
|
|
@ -150,7 +150,7 @@ export default class OpponentSelectScene extends Phaser.Scene {
|
|||
const pfd = this.cache.json.get('playfields') ?? {};
|
||||
this.applyDefault('playfields', pfd.default, 'selectedPlayfield', 'playfieldTiles');
|
||||
}
|
||||
if (this.gameDef.cardGame && this.gameDef.slug !== 'splendor') {
|
||||
if (this.gameDef.cardGame) {
|
||||
const cardBacks = this.cache.json.get('card-backs')?.cardBacks ?? [];
|
||||
if (cardBacks.length > 0) {
|
||||
const randomCb = cardBacks[Math.floor(Math.random() * cardBacks.length)];
|
||||
|
|
|
|||
Loading…
Reference in New Issue