feat(blokus/splendor): refine tray UI and upgrade splendor spritesheet rendering
- Blokus: Adjust tray coordinates and add a background panel for improved layout separation. - Splendor: Integrate a 28-frame spritesheet for development cards, nobles, and deck backs. - Refactor frame mapping to calculate indices dynamically based on tier and bonus color. - Unify card rendering pipeline: draw tinted background, overlay spritesheet art (when available), then render vector overlays. - Add hover preview panel for reserved cards. - Document spritesheet layout and update preload scene asset comments.
This commit is contained in:
parent
04a32cebcd
commit
e60387ec7c
Binary file not shown.
|
After Width: | Height: | Size: 1022 KiB |
Binary file not shown.
|
|
@ -17,16 +17,16 @@ import {
|
||||||
} from './BlokusLogic.js';
|
} from './BlokusLogic.js';
|
||||||
import { chooseMove, nextThinkDelay } from './BlokusAI.js';
|
import { chooseMove, nextThinkDelay } from './BlokusAI.js';
|
||||||
|
|
||||||
const DEPTH = { felt: -1, grid: 0, corner: 1, piece: 10, ghost: 20, hud: 30, button: 35, banner: 50, modal: 80 };
|
const DEPTH = { felt: -1, grid: 0, corner: 1, trayBg: 2, piece: 10, ghost: 20, hud: 30, button: 35, banner: 50, modal: 80 };
|
||||||
|
|
||||||
// Right-hand panel layout.
|
// Right-hand panel layout.
|
||||||
const TRAY_X = 1035;
|
const TRAY_X = 1020;
|
||||||
const TRAY_Y = 96;
|
const TRAY_Y = 96;
|
||||||
const TRAY_COLS = 3;
|
const TRAY_COLS = 3;
|
||||||
const TRAY_SLOT_W = 160;
|
const TRAY_SLOT_W = 160;
|
||||||
const TRAY_SLOT_H = 92;
|
const TRAY_SLOT_H = 92;
|
||||||
const TRAY_CELL = 15;
|
const TRAY_CELL = 15;
|
||||||
const SIDE_X = 1710; // centre of the button / score column
|
const SIDE_X = 1730; // centre of the button / score column
|
||||||
|
|
||||||
export default class BlokusGame extends Phaser.Scene {
|
export default class BlokusGame extends Phaser.Scene {
|
||||||
constructor() { super('BlokusGame'); }
|
constructor() { super('BlokusGame'); }
|
||||||
|
|
@ -61,6 +61,7 @@ export default class BlokusGame extends Phaser.Scene {
|
||||||
}
|
}
|
||||||
|
|
||||||
this.buildBoard();
|
this.buildBoard();
|
||||||
|
this.buildTrayBackground();
|
||||||
this.pieceGfx = this.add.graphics().setDepth(DEPTH.piece);
|
this.pieceGfx = this.add.graphics().setDepth(DEPTH.piece);
|
||||||
this.ghostGfx = this.add.graphics().setDepth(DEPTH.ghost);
|
this.ghostGfx = this.add.graphics().setDepth(DEPTH.ghost);
|
||||||
|
|
||||||
|
|
@ -105,6 +106,18 @@ export default class BlokusGame extends Phaser.Scene {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buildTrayBackground() {
|
||||||
|
const pad = 12;
|
||||||
|
const trayW = TRAY_COLS * TRAY_SLOT_W;
|
||||||
|
// Bottom edge of the Pass button (center y=898, height=48)
|
||||||
|
const panelBottom = 922;
|
||||||
|
const g = this.add.graphics().setDepth(DEPTH.trayBg);
|
||||||
|
g.fillStyle(COLORS.panel, 0.88);
|
||||||
|
g.fillRoundedRect(TRAY_X - pad, TRAY_Y - pad, trayW + pad * 2, panelBottom - TRAY_Y + pad * 2, 14);
|
||||||
|
g.lineStyle(2, COLORS.accent, 0.9);
|
||||||
|
g.strokeRoundedRect(TRAY_X - pad, TRAY_Y - pad, trayW + pad * 2, panelBottom - TRAY_Y + pad * 2, 14);
|
||||||
|
}
|
||||||
|
|
||||||
buildBoardInput() {
|
buildBoardInput() {
|
||||||
const zone = this.add.zone(GRID_X, GRID_Y, BOARD_PX, BOARD_PX).setOrigin(0)
|
const zone = this.add.zone(GRID_X, GRID_Y, BOARD_PX, BOARD_PX).setOrigin(0)
|
||||||
.setInteractive({ useHandCursor: false });
|
.setInteractive({ useHandCursor: false });
|
||||||
|
|
|
||||||
|
|
@ -130,10 +130,14 @@ export const NOBLES = NOBLE_REQS.map((requires, i) => ({
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// ── Spritesheet frame mapping (270×390 cells, row-major) ─────────────────────
|
// ── Spritesheet frame mapping (270×390 cells, row-major) ─────────────────────
|
||||||
// Layout authored later: 0–89 = development cards (catalog order),
|
// 28 frames total:
|
||||||
// 90–99 = nobles, 100–102 = tier deck backs. Code falls back to vector drawing
|
// 0–14 = card backgrounds: (tier-1)*5 + GEMS.indexOf(bonus)
|
||||||
// when the 'splendor-cards' texture is absent, so these are advisory until art
|
// tier1: white(0) blue(1) green(2) red(3) black(4)
|
||||||
// exists.
|
// tier2: white(5) blue(6) green(7) red(8) black(9)
|
||||||
export function cardFrame(card) { return card.frame; }
|
// tier3: white(10) blue(11) green(12) red(13) black(14)
|
||||||
export function nobleFrame(noble) { return 90 + noble.frame; }
|
// 15–24 = nobles 0–9 (unique art each)
|
||||||
export function deckBackFrame(tier) { return 100 + (tier - 1); }
|
// 25–27 = deck backs for tier 1, 2, 3
|
||||||
|
// Vectors (cost pips, points, bonus badge) are always drawn on top of the sprite.
|
||||||
|
export function cardFrame(card) { return (card.tier - 1) * 5 + GEMS.indexOf(card.bonus); }
|
||||||
|
export function nobleFrame(noble) { return 15 + noble.frame; }
|
||||||
|
export function deckBackFrame(tier) { return 25 + (tier - 1); }
|
||||||
|
|
|
||||||
|
|
@ -6,7 +6,7 @@ import { api } from '../../services/api.js';
|
||||||
import { playSound, SFX } from '../../ui/Sounds.js';
|
import { playSound, SFX } from '../../ui/Sounds.js';
|
||||||
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
import { MusicPlayer } from '../../ui/MusicPlayer.js';
|
||||||
import {
|
import {
|
||||||
GEMS, GOLD, GEM_HEX, GEM_EDGE, GEM_NAME, cardFrame, nobleFrame,
|
GEMS, GOLD, GEM_HEX, GEM_EDGE, GEM_NAME, cardFrame, nobleFrame, deckBackFrame,
|
||||||
WIN_POINTS, MAX_RESERVED, TAKE_SAME_MIN,
|
WIN_POINTS, MAX_RESERVED, TAKE_SAME_MIN,
|
||||||
} from './SplendorData.js';
|
} from './SplendorData.js';
|
||||||
import {
|
import {
|
||||||
|
|
@ -47,6 +47,7 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
this.selection = []; // colours chosen for a take action
|
this.selection = []; // colours chosen for a take action
|
||||||
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
|
||||||
}
|
}
|
||||||
|
|
||||||
create() {
|
create() {
|
||||||
|
|
@ -96,10 +97,71 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
reg(obj) { this.dyn.push(obj); return obj; }
|
reg(obj) { this.dyn.push(obj); return obj; }
|
||||||
|
|
||||||
clearDyn() {
|
clearDyn() {
|
||||||
|
this.clearPreview();
|
||||||
for (const o of this.dyn) { try { o.destroy(); } catch { /* noop */ } }
|
for (const o of this.dyn) { try { o.destroy(); } catch { /* noop */ } }
|
||||||
this.dyn = [];
|
this.dyn = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
clearPreview() {
|
||||||
|
for (const o of this.preview) { try { o.destroy(); } catch { /* noop */ } }
|
||||||
|
this.preview = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
showCardPreview(card, barCenterX, barBottomY) {
|
||||||
|
this.clearPreview();
|
||||||
|
const pw = 270, ph = 390;
|
||||||
|
const rp = (obj) => { this.preview.push(obj); return obj; };
|
||||||
|
|
||||||
|
const px = Math.min(Math.max(barCenterX - pw / 2, 8), GAME_WIDTH - pw - 8);
|
||||||
|
const pyBelow = barBottomY + 4;
|
||||||
|
const py = pyBelow + ph > GAME_HEIGHT - 8 ? barBottomY - ph - 4 : pyBelow;
|
||||||
|
|
||||||
|
// Drop shadow panel
|
||||||
|
rp(this.add.graphics().setDepth(DEPTH.popup))
|
||||||
|
.fillStyle(0x000000, 0.65).fillRoundedRect(px - 7, py - 7, pw + 14, ph + 14, 14);
|
||||||
|
|
||||||
|
// Card background (same tinted-rect approach as drawCard)
|
||||||
|
const g = rp(this.add.graphics().setDepth(DEPTH.popup));
|
||||||
|
const tint = GEM_HEX[card.bonus];
|
||||||
|
g.fillStyle(0x14110b, 1).fillRoundedRect(px, py, pw, ph, 10);
|
||||||
|
g.fillStyle(tint, 0.20).fillRoundedRect(px, py, pw, ph, 10);
|
||||||
|
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) {
|
||||||
|
rp(this.add.image(px + pw / 2, py + ph / 2, 'splendor-cards', cardFrame(card))
|
||||||
|
.setDisplaySize(pw, ph).setDepth(DEPTH.popup));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Points
|
||||||
|
if (card.points > 0) {
|
||||||
|
rp(this.add.text(px + 18, py + 8, String(card.points), {
|
||||||
|
fontFamily: 'Righteous', fontSize: '54px',
|
||||||
|
color: card.bonus === 'white' ? '#222' : '#fff',
|
||||||
|
}).setDepth(DEPTH.popup + 1));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bonus gem badge top-right
|
||||||
|
const bg = rp(this.add.graphics().setDepth(DEPTH.popup + 1));
|
||||||
|
bg.fillStyle(GEM_HEX[card.bonus], 1).fillCircle(px + pw - 40, py + 36, 26);
|
||||||
|
bg.lineStyle(3, GEM_EDGE[card.bonus], 1).strokeCircle(px + pw - 40, py + 36, 26);
|
||||||
|
|
||||||
|
// Cost pips bottom-left stacked
|
||||||
|
const costs = GEMS.filter((c) => (card.cost[c] ?? 0) > 0);
|
||||||
|
costs.forEach((c, i) => {
|
||||||
|
const cy = py + ph - 44 - i * 62;
|
||||||
|
const pg = rp(this.add.graphics().setDepth(DEPTH.popup + 1));
|
||||||
|
pg.fillStyle(GEM_HEX[c], 1).fillCircle(px + 40, cy, 26);
|
||||||
|
pg.lineStyle(3, GEM_EDGE[c], 1).strokeCircle(px + 40, cy, 26);
|
||||||
|
rp(this.add.text(px + 40, cy, String(card.cost[c]), {
|
||||||
|
fontFamily: 'Righteous', fontSize: '28px',
|
||||||
|
color: c === 'white' ? '#222' : '#fff',
|
||||||
|
}).setOrigin(0.5).setDepth(DEPTH.popup + 2));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
this.clearDyn();
|
this.clearDyn();
|
||||||
this.drawNobles();
|
this.drawNobles();
|
||||||
|
|
@ -121,11 +183,11 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
this.reg(this.add.image(x + NOBLE / 2, NOBLE_Y + NOBLE / 2, 'splendor-cards', nobleFrame(n))
|
this.reg(this.add.image(x + NOBLE / 2, NOBLE_Y + NOBLE / 2, 'splendor-cards', nobleFrame(n))
|
||||||
.setDisplaySize(NOBLE, NOBLE).setDepth(DEPTH.card + 1));
|
.setDisplaySize(NOBLE, NOBLE).setDepth(DEPTH.card + 1));
|
||||||
}
|
}
|
||||||
// prestige
|
// 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,
|
||||||
}).setDepth(DEPTH.card + 2));
|
}).setDepth(DEPTH.card + 2));
|
||||||
// requirement chips along the bottom
|
// requirement chips along the bottom (always drawn over art)
|
||||||
const reqs = Object.entries(n.requires);
|
const reqs = Object.entries(n.requires);
|
||||||
reqs.forEach(([color, req], j) => {
|
reqs.forEach(([color, req], j) => {
|
||||||
const cx = x + 16 + j * 30, cy = NOBLE_Y + NOBLE - 18;
|
const cx = x + 16 + j * 30, cy = NOBLE_Y + NOBLE - 18;
|
||||||
|
|
@ -150,12 +212,16 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
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);
|
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.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 + 1));
|
}).setOrigin(0.5).setDepth(DEPTH.card + 2));
|
||||||
this.reg(this.add.text(MARKET_X + DECK_W / 2, y + CH - 26, `${deckN}`, {
|
this.reg(this.add.text(MARKET_X + DECK_W / 2, y + CH - 26, `${deckN}`, {
|
||||||
fontFamily: 'Righteous', fontSize: '26px', color: COLORS.textHex,
|
fontFamily: 'Righteous', fontSize: '26px', color: COLORS.textHex,
|
||||||
}).setOrigin(0.5).setDepth(DEPTH.card + 1));
|
}).setOrigin(0.5).setDepth(DEPTH.card + 2));
|
||||||
if (deckN > 0 && this.isHumanTurn()) {
|
if (deckN > 0 && this.isHumanTurn()) {
|
||||||
const zone = this.reg(this.add.zone(MARKET_X + DECK_W / 2, y + CH / 2, DECK_W, CH)
|
const zone = this.reg(this.add.zone(MARKET_X + DECK_W / 2, y + CH / 2, DECK_W, CH)
|
||||||
.setInteractive({ useHandCursor: true }).setDepth(DEPTH.card + 2));
|
.setInteractive({ useHandCursor: true }).setDepth(DEPTH.card + 2));
|
||||||
|
|
@ -176,41 +242,46 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
const affordable = this.isHumanTurn() && canAfford(human, card);
|
const affordable = this.isHumanTurn() && canAfford(human, card);
|
||||||
const selected = this.selectedCard && this.selectedCard.card.id === card.id;
|
const selected = this.selectedCard && this.selectedCard.card.id === card.id;
|
||||||
|
|
||||||
|
// 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(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
|
||||||
if (this.hasArt) {
|
if (this.hasArt) {
|
||||||
this.reg(this.add.image(x + CW / 2, y + CH / 2, 'splendor-cards', cardFrame(card))
|
this.reg(this.add.image(x + CW / 2, y + CH / 2, 'splendor-cards', cardFrame(card))
|
||||||
.setDisplaySize(CW, CH).setDepth(DEPTH.card + 1));
|
.setDisplaySize(CW, CH).setDepth(DEPTH.card + 1));
|
||||||
} else {
|
|
||||||
const g = this.reg(this.add.graphics().setDepth(DEPTH.card + 1));
|
|
||||||
const tint = GEM_HEX[card.bonus];
|
|
||||||
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.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);
|
|
||||||
// points
|
|
||||||
if (card.points > 0) {
|
|
||||||
this.reg(this.add.text(x + 10, y + 2, String(card.points), {
|
|
||||||
fontFamily: 'Righteous', fontSize: '26px',
|
|
||||||
color: card.bonus === 'white' ? '#222' : '#fff',
|
|
||||||
}).setDepth(DEPTH.card + 2));
|
|
||||||
}
|
|
||||||
// bonus gem badge (top-right)
|
|
||||||
const bg = this.reg(this.add.graphics().setDepth(DEPTH.card + 2));
|
|
||||||
bg.fillStyle(GEM_HEX[card.bonus], 1).fillCircle(x + CW - 22, y + 15, 12);
|
|
||||||
bg.lineStyle(2, GEM_EDGE[card.bonus], 1).strokeCircle(x + CW - 22, y + 15, 12);
|
|
||||||
// cost pips, bottom-left stacked
|
|
||||||
const costs = GEMS.filter((c) => (card.cost[c] ?? 0) > 0);
|
|
||||||
costs.forEach((c, i) => {
|
|
||||||
const cy = y + CH - 22 - i * 30;
|
|
||||||
const pg = this.reg(this.add.graphics().setDepth(DEPTH.card + 2));
|
|
||||||
pg.fillStyle(GEM_HEX[c], 1).fillCircle(x + 20, cy, 12);
|
|
||||||
pg.lineStyle(2, GEM_EDGE[c], 1).strokeCircle(x + 20, cy, 12);
|
|
||||||
this.reg(this.add.text(x + 20, cy, String(card.cost[c]), {
|
|
||||||
fontFamily: 'Righteous', fontSize: '15px',
|
|
||||||
color: c === 'white' ? '#222' : '#fff',
|
|
||||||
}).setOrigin(0.5).setDepth(DEPTH.card + 3));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Vector overlays — always drawn on top of whatever background is present
|
||||||
|
if (card.points > 0) {
|
||||||
|
this.reg(this.add.text(x + 10, y + 2, String(card.points), {
|
||||||
|
fontFamily: 'Righteous', fontSize: '26px',
|
||||||
|
color: card.bonus === 'white' ? '#222' : '#fff',
|
||||||
|
}).setDepth(DEPTH.card + 2));
|
||||||
|
}
|
||||||
|
// bonus gem badge (top-right)
|
||||||
|
const bg = this.reg(this.add.graphics().setDepth(DEPTH.card + 2));
|
||||||
|
bg.fillStyle(GEM_HEX[card.bonus], 1).fillCircle(x + CW - 22, y + 15, 12);
|
||||||
|
bg.lineStyle(2, GEM_EDGE[card.bonus], 1).strokeCircle(x + CW - 22, y + 15, 12);
|
||||||
|
// cost pips, bottom-left stacked
|
||||||
|
const costs = GEMS.filter((c) => (card.cost[c] ?? 0) > 0);
|
||||||
|
costs.forEach((c, i) => {
|
||||||
|
const cy = y + CH - 22 - i * 30;
|
||||||
|
const pg = this.reg(this.add.graphics().setDepth(DEPTH.card + 2));
|
||||||
|
pg.fillStyle(GEM_HEX[c], 1).fillCircle(x + 20, cy, 12);
|
||||||
|
pg.lineStyle(2, GEM_EDGE[c], 1).strokeCircle(x + 20, cy, 12);
|
||||||
|
this.reg(this.add.text(x + 20, cy, String(card.cost[c]), {
|
||||||
|
fontFamily: 'Righteous', fontSize: '15px',
|
||||||
|
color: c === 'white' ? '#222' : '#fff',
|
||||||
|
}).setOrigin(0.5).setDepth(DEPTH.card + 3));
|
||||||
|
});
|
||||||
|
|
||||||
// affordability / selection ring
|
// affordability / selection ring
|
||||||
if (selected || affordable) {
|
if (selected || affordable) {
|
||||||
const ring = this.reg(this.add.graphics().setDepth(DEPTH.card + 4));
|
const ring = this.reg(this.add.graphics().setDepth(DEPTH.card + 4));
|
||||||
|
|
@ -328,9 +399,12 @@ export default class SplendorGame extends Phaser.Scene {
|
||||||
fontFamily: 'Righteous', fontSize: '14px',
|
fontFamily: 'Righteous', fontSize: '14px',
|
||||||
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)
|
||||||
|
.setInteractive(this.isHumanTurn() ? { useHandCursor: true } : {})
|
||||||
|
.setDepth(DEPTH.ui + 3));
|
||||||
|
z.on('pointerover', () => this.showCardPreview(card, rx + 52, ry + 30));
|
||||||
|
z.on('pointerout', () => this.clearPreview());
|
||||||
if (this.isHumanTurn()) {
|
if (this.isHumanTurn()) {
|
||||||
const z = this.reg(this.add.zone(rx + 52, ry + 15, 104, 30)
|
|
||||||
.setInteractive({ useHandCursor: true }).setDepth(DEPTH.ui + 3));
|
|
||||||
z.on('pointerdown', () => this.onCardClick(card, 'reserve'));
|
z.on('pointerdown', () => this.onCardClick(card, 'reserve'));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,62 @@
|
||||||
|
# Splendor Spritesheet Spec
|
||||||
|
|
||||||
|
**File:** `public/assets/images/splendor-cards.png`
|
||||||
|
**Cell size:** 270 × 390 px
|
||||||
|
**Total frames:** 28, row-major order
|
||||||
|
|
||||||
|
Vectors (cost pips, point value, bonus badge, requirement chips) are always drawn on top of the sprite by the game engine — art only needs to provide the background illustration.
|
||||||
|
|
||||||
|
## Development Card Backgrounds (frames 0–14)
|
||||||
|
|
||||||
|
One background per tier × bonus color. All cards sharing the same tier and bonus color use the same frame.
|
||||||
|
|
||||||
|
| Frame | Tier | Bonus Color |
|
||||||
|
|-------|------|-------------|
|
||||||
|
| 0 | 1 | White (Diamond) |
|
||||||
|
| 1 | 1 | Blue (Sapphire) |
|
||||||
|
| 2 | 1 | Green (Emerald) |
|
||||||
|
| 3 | 1 | Red (Ruby) |
|
||||||
|
| 4 | 1 | Black (Onyx) |
|
||||||
|
| 5 | 2 | White (Diamond) |
|
||||||
|
| 6 | 2 | Blue (Sapphire) |
|
||||||
|
| 7 | 2 | Green (Emerald) |
|
||||||
|
| 8 | 2 | Red (Ruby) |
|
||||||
|
| 9 | 2 | Black (Onyx) |
|
||||||
|
| 10 | 3 | White (Diamond) |
|
||||||
|
| 11 | 3 | Blue (Sapphire) |
|
||||||
|
| 12 | 3 | Green (Emerald) |
|
||||||
|
| 13 | 3 | Red (Ruby) |
|
||||||
|
| 14 | 3 | Black (Onyx) |
|
||||||
|
|
||||||
|
## Nobles (frames 15–24)
|
||||||
|
|
||||||
|
Each noble has unique art. Nobles are displayed at 96 × 96 px (square), so design the main subject centered within the 270 × 390 cell.
|
||||||
|
|
||||||
|
| Frame | Requirements |
|
||||||
|
|-------|-------------|
|
||||||
|
| 15 | White ×4, Blue ×4 |
|
||||||
|
| 16 | Blue ×4, Green ×4 |
|
||||||
|
| 17 | Green ×4, Red ×4 |
|
||||||
|
| 18 | Red ×4, Black ×4 |
|
||||||
|
| 19 | Black ×4, White ×4 |
|
||||||
|
| 20 | White ×3, Blue ×3, Green ×3 |
|
||||||
|
| 21 | Blue ×3, Green ×3, Red ×3 |
|
||||||
|
| 22 | Green ×3, Red ×3, Black ×3 |
|
||||||
|
| 23 | Red ×3, Black ×3, White ×3 |
|
||||||
|
| 24 | Black ×3, White ×3, Blue ×3 |
|
||||||
|
|
||||||
|
## Deck Backs (frames 25–27)
|
||||||
|
|
||||||
|
| Frame | Tier |
|
||||||
|
|-------|------|
|
||||||
|
| 25 | Tier 1 |
|
||||||
|
| 26 | Tier 2 |
|
||||||
|
| 27 | Tier 3 |
|
||||||
|
|
||||||
|
## Display Sizes (for reference)
|
||||||
|
|
||||||
|
| Element | Rendered size |
|
||||||
|
|---------|--------------|
|
||||||
|
| Development cards | 128 × 176 px |
|
||||||
|
| Nobles | 96 × 96 px |
|
||||||
|
| Deck backs | 110 × 176 px |
|
||||||
|
|
@ -98,8 +98,8 @@ export default class PreloadScene extends Phaser.Scene {
|
||||||
this.load.spritesheet('dominion-prosperity', '/assets/images/dominion-prosperity.png', { frameWidth: 270, frameHeight: 390 });
|
this.load.spritesheet('dominion-prosperity', '/assets/images/dominion-prosperity.png', { frameWidth: 270, frameHeight: 390 });
|
||||||
// Prosperity token sprites (1 VP, 5 VP, Gold) — 150×150 each.
|
// Prosperity token sprites (1 VP, 5 VP, Gold) — 150×150 each.
|
||||||
this.load.spritesheet('dominion-tokens', '/assets/images/dominion-tokens.png', { frameWidth: 150, frameHeight: 150 });
|
this.load.spritesheet('dominion-tokens', '/assets/images/dominion-tokens.png', { frameWidth: 150, frameHeight: 150 });
|
||||||
// Splendor development cards + nobles. 270×390 cells; frame order documented
|
// Splendor: 28 frames at 270×390. 0-14 = tier×bonus backgrounds, 15-24 = nobles, 25-27 = deck backs.
|
||||||
// in SplendorData.js. Optional — the scene draws vector cards when absent.
|
// Frame order documented in SplendorData.js. Optional — vectors are drawn when absent.
|
||||||
this.load.spritesheet('splendor-cards', '/assets/images/splendor-cards.png', { frameWidth: 270, frameHeight: 390 });
|
this.load.spritesheet('splendor-cards', '/assets/images/splendor-cards.png', { frameWidth: 270, frameHeight: 390 });
|
||||||
this.load.spritesheet('ttr-cards', '/assets/images/tickettoride-cards.png', { frameWidth: 270, frameHeight: 390 });
|
this.load.spritesheet('ttr-cards', '/assets/images/tickettoride-cards.png', { frameWidth: 270, frameHeight: 390 });
|
||||||
this.load.spritesheet('gofish-cards', '/assets/images/gofish-cards.png', { frameWidth: 270, frameHeight: 390 });
|
this.load.spritesheet('gofish-cards', '/assets/images/gofish-cards.png', { frameWidth: 270, frameHeight: 390 });
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue