diff --git a/public/src/games/splendor/SplendorGame.js b/public/src/games/splendor/SplendorGame.js index 2e5973f..0d3ba2f 100644 --- a/public/src/games/splendor/SplendorGame.js +++ b/public/src/games/splendor/SplendorGame.js @@ -121,6 +121,16 @@ export default class SplendorGame extends Phaser.Scene { // ── 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(); 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) { 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).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 if (card.points > 0) { @@ -206,12 +220,13 @@ 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.reg(this.add.image(x + NOBLE / 2, NOBLE_Y + NOBLE / 2, 'splendor-cards', nobleFrame(n)) - .setDisplaySize(NOBLE, NOBLE).setDepth(DEPTH.card + 1)); + this.regMaskedImg(x, NOBLE_Y, NOBLE, NOBLE, 'splendor-cards', nobleFrame(n), 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, @@ -238,13 +253,15 @@ 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.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.regMaskedImg(MARKET_X, y, DECK_W, CH, 'splendor-cards', deckBackFrame(tier), 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)); @@ -274,18 +291,20 @@ 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 background art drawn over the tinted rect when available + // Spritesheet art clipped to rounded rect if (this.hasArt) { - this.reg(this.add.image(x + CW / 2, y + CH / 2, 'splendor-cards', cardFrame(card)) - .setDisplaySize(CW, CH).setDepth(DEPTH.card + 1)); + this.regMaskedImg(x, y, CW, CH, 'splendor-cards', cardFrame(card), 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) { @@ -399,9 +418,10 @@ export default class SplendorGame extends Phaser.Scene { // gem cells: cards (bonus) over tokens — compact, right-aligned const order = [...GEMS, GOLD]; - const gemR = 14; + const gemR = 10; + const totalR = 14; 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) => { const ccx = gemStartX + ci * gemSpacing; const ccy = y + 70; @@ -409,14 +429,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(2, GEM_EDGE[color], 1).strokeCircle(ccx, ccy, gemR); + cg.lineStyle(1.5, GEM_EDGE[color], 1).strokeCircle(ccx, ccy, gemR); if (color !== GOLD) { this.reg(this.add.text(ccx, ccy, String(cards), { - fontFamily: 'Righteous', fontSize: '14px', + fontFamily: 'Righteous', fontSize: '12px', color: color === 'white' ? '#222' : '#fff', }).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, }).setOrigin(0.5).setDepth(DEPTH.ui + 2)); // discard interactivity (human, over limit) @@ -435,15 +455,15 @@ export default class SplendorGame extends Phaser.Scene { 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); + .lineBetween(gemStartX - totalR - 2, divY, gemStartX + (order.length - 1) * gemSpacing + totalR + 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); + 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); this.reg(this.add.text(ccx, totalCY, String(total), { - fontFamily: 'Righteous', fontSize: '12px', + fontFamily: 'Righteous', fontSize: '14px', color: color === 'white' ? '#222' : '#fff', }).setOrigin(0.5).setDepth(DEPTH.ui + 2)); }); @@ -586,9 +606,11 @@ 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 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); - 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); }