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,
|
createInitialState, legalActions, applyAction, applyDiscard, defaultDiscards,
|
||||||
canAfford, tokenTotal, isGameOver, finalRanking, currentPlayer,
|
canAfford, tokenTotal, isGameOver, finalRanking, currentPlayer,
|
||||||
} from './SplendorLogic.js';
|
} from './SplendorLogic.js';
|
||||||
|
import { createOpponentPortrait, createPlayerPortrait } from '../../ui/Portrait.js';
|
||||||
import { HAND_LIMIT } from './SplendorData.js';
|
import { HAND_LIMIT } from './SplendorData.js';
|
||||||
import { chooseAction, nextThinkDelay } from './SplendorAI.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.selectedCard = null; // { card, source } chosen to buy/reserve
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -71,10 +73,32 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.gs = createInitialState({ playerCount, names });
|
this.gs = createInitialState({ playerCount, names });
|
||||||
|
this.buildPortraits();
|
||||||
this.render();
|
this.render();
|
||||||
this.advance();
|
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 ──────────────────────────────────────────────
|
// ── static background / chrome ──────────────────────────────────────────────
|
||||||
buildBackground() {
|
buildBackground() {
|
||||||
const pf = this.playfield;
|
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 x = PANEL_X, y = 76 + idx * (h + gap), w = PANEL_W;
|
||||||
const isCurrent = idx === this.gs.current && !isGameOver(this.gs);
|
const isCurrent = idx === this.gs.current && !isGameOver(this.gs);
|
||||||
const g = this.reg(this.add.graphics().setDepth(DEPTH.ui));
|
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)
|
g.lineStyle(isCurrent ? 3 : 1, isCurrent ? COLORS.gold : COLORS.accent, isCurrent ? 1 : 0.4)
|
||||||
.strokeRoundedRect(x, y, w, h, 12);
|
.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,
|
fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex,
|
||||||
}).setOrigin(1, 0).setDepth(DEPTH.ui + 1));
|
}).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 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) => {
|
order.forEach((color, ci) => {
|
||||||
const ccx = x + 16 + cellW * ci + cellW / 2;
|
const ccx = gemStartX + ci * gemSpacing;
|
||||||
const ccy = y + 70;
|
const ccy = y + 70;
|
||||||
const cards = color === GOLD ? 0 : (p.bonuses[color] ?? 0);
|
const cards = color === GOLD ? 0 : (p.bonuses[color] ?? 0);
|
||||||
const toks = p.tokens[color] ?? 0;
|
const toks = p.tokens[color] ?? 0;
|
||||||
const cg = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1));
|
const cg = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1));
|
||||||
cg.fillStyle(GEM_HEX[color], 1).fillCircle(ccx, ccy, 16);
|
cg.fillStyle(GEM_HEX[color], 1).fillCircle(ccx, ccy, gemR);
|
||||||
cg.lineStyle(2, GEM_EDGE[color], 1).strokeCircle(ccx, ccy, 16);
|
cg.lineStyle(2, GEM_EDGE[color], 1).strokeCircle(ccx, ccy, gemR);
|
||||||
if (color !== GOLD) {
|
if (color !== GOLD) {
|
||||||
this.reg(this.add.text(ccx, ccy, String(cards), {
|
this.reg(this.add.text(ccx, ccy, String(cards), {
|
||||||
fontFamily: 'Righteous', fontSize: '16px',
|
fontFamily: 'Righteous', fontSize: '14px',
|
||||||
color: color === 'white' ? '#222' : '#fff',
|
color: color === 'white' ? '#222' : '#fff',
|
||||||
}).setOrigin(0.5).setDepth(DEPTH.ui + 2));
|
}).setOrigin(0.5).setDepth(DEPTH.ui + 2));
|
||||||
}
|
}
|
||||||
this.reg(this.add.text(ccx, ccy + 24, `${toks}`, {
|
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));
|
}).setOrigin(0.5).setDepth(DEPTH.ui + 2));
|
||||||
// discard interactivity (human, over limit)
|
// discard interactivity (human, over limit)
|
||||||
const discardable = this.gs.phase === 'discard' && this.gs.current === this.humanSeat
|
const discardable = this.gs.phase === 'discard' && this.gs.current === this.humanSeat
|
||||||
&& idx === this.humanSeat && toks > 0;
|
&& idx === this.humanSeat && toks > 0;
|
||||||
if (discardable) {
|
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));
|
.setInteractive({ useHandCursor: true }).setDepth(DEPTH.ui + 3));
|
||||||
z.on('pointerdown', () => this.onDiscardClick(color));
|
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
|
// 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}`, {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex,
|
fontFamily: '"Julius Sans One"', fontSize: '15px', color: COLORS.mutedHex,
|
||||||
}).setDepth(DEPTH.ui + 1));
|
}).setDepth(DEPTH.ui + 1));
|
||||||
if (idx === this.humanSeat && rN > 0) {
|
if (rN > 0) {
|
||||||
|
const isHuman = idx === this.humanSeat;
|
||||||
p.reserved.forEach((card, ri) => {
|
p.reserved.forEach((card, ri) => {
|
||||||
const rx = x + 150 + ri * 116, ry = y + h - 40;
|
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));
|
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.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)
|
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',
|
color: card.bonus === 'white' ? '#222' : '#fff',
|
||||||
}).setOrigin(0.5).setDepth(DEPTH.ui + 2));
|
}).setOrigin(0.5).setDepth(DEPTH.ui + 2));
|
||||||
const z = this.reg(this.add.zone(rx + 52, ry + 15, 104, 30)
|
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));
|
.setDepth(DEPTH.ui + 3));
|
||||||
z.on('pointerover', () => this.showCardPreview(card, rx + 52, ry + 30));
|
z.on('pointerover', () => this.showCardPreview(card, rx + 52, ry + 30));
|
||||||
z.on('pointerout', () => this.clearPreview());
|
z.on('pointerout', () => this.clearPreview());
|
||||||
if (this.isHumanTurn()) {
|
if (isHuman && this.isHumanTurn()) {
|
||||||
z.on('pointerdown', () => this.onCardClick(card, 'reserve'));
|
z.on('pointerdown', () => this.onCardClick(card, 'reserve'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue