feat(splendor): add player portraits and refine panel UI
- Introduce portrait rendering for players and opponents via `buildPortraits`. - Compact and right-align gem cells with updated sizing, spacing, and smaller fonts. - Add divider and total row displaying combined bonus and token counts. - Increase panel background opacity and fix reserved card interactivity checks.
This commit is contained in:
parent
9b7bbe55c4
commit
e2b28b6c0e
|
|
@ -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'));
|
||||
}
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue