diff --git a/public/src/games/splendor/SplendorGame.js b/public/src/games/splendor/SplendorGame.js index 2910825..2e5973f 100644 --- a/public/src/games/splendor/SplendorGame.js +++ b/public/src/games/splendor/SplendorGame.js @@ -13,6 +13,7 @@ import { createInitialState, legalActions, applyAction, applyDiscard, defaultDiscards, canAfford, tokenTotal, isGameOver, finalRanking, currentPlayer, } from './SplendorLogic.js'; +import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js'; import { HAND_LIMIT } from './SplendorData.js'; import { chooseAction, nextThinkDelay } from './SplendorAI.js'; @@ -48,6 +49,7 @@ export default class SplendorGame extends Phaser.Scene { 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 } create() { @@ -71,10 +73,32 @@ export default class SplendorGame extends Phaser.Scene { } this.gs = createInitialState({ playerCount, names }); + this.buildPortraits(); this.render(); this.advance(); } + // ── portraits (created once, not part of dyn) ──────────────────────────────── + buildPortraits() { + const n = this.gs.players.length; + const gap = 16; + const h = Math.min(220, Math.floor((GAME_HEIGHT - 90 - gap * (n - 1)) / n)); + const portraitR = 34; + + this.gs.players.forEach((_, idx) => { + const panelY = 76 + idx * (h + gap); + const px = PANEL_X + 16 + portraitR; + const py = panelY + 38 + portraitR; // below 22px name + small gap + + if (idx === this.humanSeat) { + 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, { playIntro: false }); + } + }); + } + // ── static background / chrome ────────────────────────────────────────────── buildBackground() { const pf = this.playfield; @@ -358,7 +382,7 @@ export default class SplendorGame extends Phaser.Scene { const x = PANEL_X, y = 76 + idx * (h + gap), w = PANEL_W; const isCurrent = idx === this.gs.current && !isGameOver(this.gs); const g = this.reg(this.add.graphics().setDepth(DEPTH.ui)); - g.fillStyle(0x000000, 0.34).fillRoundedRect(x, y, w, h, 12); + g.fillStyle(0x000000, 0.62).fillRoundedRect(x, y, w, h, 12); g.lineStyle(isCurrent ? 3 : 1, isCurrent ? COLORS.gold : COLORS.accent, isCurrent ? 1 : 0.4) .strokeRoundedRect(x, y, w, h, 12); @@ -373,45 +397,67 @@ export default class SplendorGame extends Phaser.Scene { fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex, }).setOrigin(1, 0).setDepth(DEPTH.ui + 1)); - // gem cells: cards (bonus) over tokens + // gem cells: cards (bonus) over tokens — compact, right-aligned const order = [...GEMS, GOLD]; - const cellW = (w - 32) / order.length; + const gemR = 14; + const gemSpacing = 36; + const gemStartX = x + w - 16 - gemR - (order.length - 1) * gemSpacing; order.forEach((color, ci) => { - const ccx = x + 16 + cellW * ci + cellW / 2; + const ccx = gemStartX + ci * gemSpacing; const ccy = y + 70; const cards = color === GOLD ? 0 : (p.bonuses[color] ?? 0); 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, 16); - cg.lineStyle(2, GEM_EDGE[color], 1).strokeCircle(ccx, ccy, 16); + cg.fillStyle(GEM_HEX[color], 1).fillCircle(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: '16px', + fontFamily: 'Righteous', fontSize: '14px', color: color === 'white' ? '#222' : '#fff', }).setOrigin(0.5).setDepth(DEPTH.ui + 2)); } this.reg(this.add.text(ccx, ccy + 24, `${toks}`, { - fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.textHex, + fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.textHex, }).setOrigin(0.5).setDepth(DEPTH.ui + 2)); // discard interactivity (human, over limit) const discardable = this.gs.phase === 'discard' && this.gs.current === this.humanSeat && idx === this.humanSeat && toks > 0; if (discardable) { - const z = this.reg(this.add.zone(ccx, ccy + 8, cellW, 56) + const z = this.reg(this.add.zone(ccx, ccy + 8, gemSpacing, 50) .setInteractive({ useHandCursor: true }).setDepth(DEPTH.ui + 3)); z.on('pointerdown', () => this.onDiscardClick(color)); } }); + // divider + total row (card bonuses + tokens) + const ccy = y + 70; + const divY = ccy + 38; + const totalCY = ccy + 53; + const divG = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1)); + divG.lineStyle(1, COLORS.accent, 0.35) + .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, 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: '12px', + color: color === 'white' ? '#222' : '#fff', + }).setOrigin(0.5).setDepth(DEPTH.ui + 2)); + }); + // reserved cards const rN = p.reserved.length; this.reg(this.add.text(x + 16, y + h - 30, `Reserved: ${rN}/${MAX_RESERVED}`, { fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex, }).setDepth(DEPTH.ui + 1)); - if (idx === this.humanSeat && rN > 0) { + if (rN > 0) { + const isHuman = idx === this.humanSeat; p.reserved.forEach((card, ri) => { const rx = x + 150 + ri * 116, ry = y + h - 40; - const canBuy = this.isHumanTurn() && canAfford(p, card); + const canBuy = isHuman && this.isHumanTurn() && canAfford(p, card); const mg = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1)); mg.fillStyle(GEM_HEX[card.bonus], 0.85).fillRoundedRect(rx, ry, 104, 30, 6); mg.lineStyle(2, canBuy ? 0x57c46a : GEM_EDGE[card.bonus], canBuy ? 1 : 0.8) @@ -422,11 +468,11 @@ export default class SplendorGame extends Phaser.Scene { color: card.bonus === 'white' ? '#222' : '#fff', }).setOrigin(0.5).setDepth(DEPTH.ui + 2)); const z = this.reg(this.add.zone(rx + 52, ry + 15, 104, 30) - .setInteractive(this.isHumanTurn() ? { useHandCursor: true } : {}) + .setInteractive({ useHandCursor: isHuman && this.isHumanTurn() }) .setDepth(DEPTH.ui + 3)); z.on('pointerover', () => this.showCardPreview(card, rx + 52, ry + 30)); z.on('pointerout', () => this.clearPreview()); - if (this.isHumanTurn()) { + if (isHuman && this.isHumanTurn()) { z.on('pointerdown', () => this.onCardClick(card, 'reserve')); } });