refine ui rendering and gem token layout
- Add `regMaskedImg` helper for rounded-rectangle image masking - Update card, noble, and deck rendering to layer masks and borders correctly - Adjust gem token sizing, spacing, and positioning for improved alignment - Standardize visual constants and improve overall UI polish
This commit is contained in:
parent
e2b28b6c0e
commit
5eb5c71ebf
|
|
@ -121,6 +121,16 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
// ── render: rebuild all dynamic visuals from this.gs ─────────────────────────
|
// ── render: rebuild all dynamic visuals from this.gs ─────────────────────────
|
||||||
reg(obj) { this.dyn.push(obj); return obj; }
|
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() {
|
clearDyn() {
|
||||||
this.clearPreview();
|
this.clearPreview();
|
||||||
for (const o of this.dyn) { try { o.destroy(); } catch { /* noop */ } }
|
for (const o of this.dyn) { try { o.destroy(); } catch { /* noop */ } }
|
||||||
|
|
@ -153,12 +163,16 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
if (!this.hasArt) {
|
if (!this.hasArt) {
|
||||||
g.fillStyle(tint, 0.9).fillRoundedRect(px, py, pw, 65, { tl: 10, tr: 10, bl: 0, br: 0 });
|
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) {
|
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))
|
rp(this.add.image(px + pw / 2, py + ph / 2, 'splendor-cards', cardFrame(card))
|
||||||
.setDisplaySize(pw, ph).setDepth(DEPTH.popup));
|
.setDisplaySize(pw, ph).setMask(maskG.createGeometryMask()).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
|
// Points
|
||||||
if (card.points > 0) {
|
if (card.points > 0) {
|
||||||
|
|
@ -206,12 +220,13 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
this.gs.nobles.forEach((n, i) => {
|
this.gs.nobles.forEach((n, i) => {
|
||||||
const x = MARKET_X + i * (NOBLE + 14);
|
const x = MARKET_X + i * (NOBLE + 14);
|
||||||
const g = this.reg(this.add.graphics().setDepth(DEPTH.card));
|
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.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) {
|
if (this.hasArt) {
|
||||||
this.reg(this.add.image(x + NOBLE / 2, NOBLE_Y + NOBLE / 2, 'splendor-cards', nobleFrame(n))
|
this.regMaskedImg(x, NOBLE_Y, NOBLE, NOBLE, 'splendor-cards', nobleFrame(n), DEPTH.card + 1);
|
||||||
.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)
|
// prestige (always drawn over art)
|
||||||
this.reg(this.add.text(x + 8, NOBLE_Y + 4, '3', {
|
this.reg(this.add.text(x + 8, NOBLE_Y + 4, '3', {
|
||||||
fontFamily: 'Righteous', fontSize: '22px', color: COLORS.goldHex,
|
fontFamily: 'Righteous', fontSize: '22px', color: COLORS.goldHex,
|
||||||
|
|
@ -238,13 +253,15 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
// deck pile
|
// deck pile
|
||||||
const deckN = this.gs.decks[tier].length;
|
const deckN = this.gs.decks[tier].length;
|
||||||
const dg = this.reg(this.add.graphics().setDepth(DEPTH.card));
|
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)
|
dg.fillStyle(tier === 3 ? 0x2a3550 : tier === 2 ? 0x3a4a2a : 0x4a3a2a, 1)
|
||||||
.fillRoundedRect(MARKET_X, y, DECK_W, CH, 10);
|
.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) {
|
if (deckN > 0 && this.hasArt) {
|
||||||
this.reg(this.add.image(MARKET_X + DECK_W / 2, y + CH / 2, 'splendor-cards', deckBackFrame(tier))
|
this.regMaskedImg(MARKET_X, y, DECK_W, CH, 'splendor-cards', deckBackFrame(tier), DEPTH.card + 1);
|
||||||
.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), {
|
this.reg(this.add.text(MARKET_X + DECK_W / 2, y + 30, '★'.repeat(tier), {
|
||||||
fontFamily: 'Righteous', fontSize: '20px', color: COLORS.goldHex,
|
fontFamily: 'Righteous', fontSize: '20px', color: COLORS.goldHex,
|
||||||
}).setOrigin(0.5).setDepth(DEPTH.card + 2));
|
}).setOrigin(0.5).setDepth(DEPTH.card + 2));
|
||||||
|
|
@ -274,18 +291,20 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
// Card background — always draw the tinted rect (provides border shape and colour identity)
|
// 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 g = this.reg(this.add.graphics().setDepth(DEPTH.card + 1));
|
||||||
const tint = GEM_HEX[card.bonus];
|
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(0x14110b, 1).fillRoundedRect(x, y, CW, CH, 10);
|
||||||
g.fillStyle(tint, 0.20).fillRoundedRect(x, y, CW, CH, 10);
|
g.fillStyle(tint, 0.20).fillRoundedRect(x, y, CW, CH, 10);
|
||||||
if (!this.hasArt) {
|
if (!this.hasArt) {
|
||||||
g.fillStyle(tint, 0.9).fillRoundedRect(x, y, CW, 30, { tl: 10, tr: 10, bl: 0, br: 0 });
|
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 background art drawn over the tinted rect when available
|
// Spritesheet art clipped to rounded rect
|
||||||
if (this.hasArt) {
|
if (this.hasArt) {
|
||||||
this.reg(this.add.image(x + CW / 2, y + CH / 2, 'splendor-cards', cardFrame(card))
|
this.regMaskedImg(x, y, CW, CH, 'splendor-cards', cardFrame(card), DEPTH.card + 1);
|
||||||
.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
|
// Vector overlays — always drawn on top of whatever background is present
|
||||||
if (card.points > 0) {
|
if (card.points > 0) {
|
||||||
|
|
@ -399,9 +418,10 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
|
|
||||||
// gem cells: cards (bonus) over tokens — compact, right-aligned
|
// gem cells: cards (bonus) over tokens — compact, right-aligned
|
||||||
const order = [...GEMS, GOLD];
|
const order = [...GEMS, GOLD];
|
||||||
const gemR = 14;
|
const gemR = 10;
|
||||||
|
const totalR = 14;
|
||||||
const gemSpacing = 36;
|
const gemSpacing = 36;
|
||||||
const gemStartX = x + w - 16 - gemR - (order.length - 1) * gemSpacing;
|
const gemStartX = x + w - 16 - totalR - (order.length - 1) * gemSpacing;
|
||||||
order.forEach((color, ci) => {
|
order.forEach((color, ci) => {
|
||||||
const ccx = gemStartX + ci * gemSpacing;
|
const ccx = gemStartX + ci * gemSpacing;
|
||||||
const ccy = y + 70;
|
const ccy = y + 70;
|
||||||
|
|
@ -409,14 +429,14 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
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, gemR);
|
cg.fillStyle(GEM_HEX[color], 1).fillCircle(ccx, ccy, gemR);
|
||||||
cg.lineStyle(2, GEM_EDGE[color], 1).strokeCircle(ccx, ccy, gemR);
|
cg.lineStyle(1.5, 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: '14px',
|
fontFamily: 'Righteous', fontSize: '12px',
|
||||||
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 + 22, `${toks}`, {
|
||||||
fontFamily: '"Julius Sans One"', fontSize: '14px', 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)
|
||||||
|
|
@ -435,15 +455,15 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
const totalCY = ccy + 53;
|
const totalCY = ccy + 53;
|
||||||
const divG = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1));
|
const divG = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1));
|
||||||
divG.lineStyle(1, COLORS.accent, 0.35)
|
divG.lineStyle(1, COLORS.accent, 0.35)
|
||||||
.lineBetween(gemStartX - gemR - 2, divY, gemStartX + (order.length - 1) * gemSpacing + gemR + 2, divY);
|
.lineBetween(gemStartX - totalR - 2, divY, gemStartX + (order.length - 1) * gemSpacing + totalR + 2, divY);
|
||||||
order.forEach((color, ci) => {
|
order.forEach((color, ci) => {
|
||||||
const ccx = gemStartX + ci * gemSpacing;
|
const ccx = gemStartX + ci * gemSpacing;
|
||||||
const total = (color === GOLD ? 0 : (p.bonuses[color] ?? 0)) + (p.tokens[color] ?? 0);
|
const total = (color === GOLD ? 0 : (p.bonuses[color] ?? 0)) + (p.tokens[color] ?? 0);
|
||||||
const tg = this.reg(this.add.graphics().setDepth(DEPTH.ui + 1));
|
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.fillStyle(GEM_HEX[color], total > 0 ? 0.8 : 0.2).fillCircle(ccx, totalCY, totalR);
|
||||||
tg.lineStyle(1.5, GEM_EDGE[color], 1).strokeCircle(ccx, totalCY, 10);
|
tg.lineStyle(2, GEM_EDGE[color], 1).strokeCircle(ccx, totalCY, totalR);
|
||||||
this.reg(this.add.text(ccx, totalCY, String(total), {
|
this.reg(this.add.text(ccx, totalCY, String(total), {
|
||||||
fontFamily: 'Righteous', fontSize: '12px',
|
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));
|
||||||
});
|
});
|
||||||
|
|
@ -586,9 +606,11 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
const gap = 16;
|
const gap = 16;
|
||||||
const h = Math.min(220, Math.floor((GAME_HEIGHT - 90 - gap * (n - 1)) / n));
|
const h = Math.min(220, Math.floor((GAME_HEIGHT - 90 - gap * (n - 1)) / n));
|
||||||
const order = [...GEMS, GOLD];
|
const order = [...GEMS, GOLD];
|
||||||
const cellW = (PANEL_W - 32) / order.length;
|
const totalR = 14;
|
||||||
|
const gemSpacing = 36;
|
||||||
|
const gemStartX = PANEL_X + PANEL_W - 16 - totalR - (order.length - 1) * gemSpacing;
|
||||||
const ci = order.indexOf(color);
|
const ci = order.indexOf(color);
|
||||||
return { x: PANEL_X + 16 + cellW * ci + cellW / 2, y: 76 + seat * (h + gap) + 70 };
|
return { x: gemStartX + ci * gemSpacing, y: 76 + seat * (h + gap) + 70 };
|
||||||
}
|
}
|
||||||
|
|
||||||
humanGemPos(color) { return this.playerGemPos(this.humanSeat, color); }
|
humanGemPos(color) { return this.playerGemPos(this.humanSeat, color); }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue