feat(catan): overhaul visuals and opponent UI
- Replace static sea background with a radial gradient for a richer look. - Redesign settlement and city graphics with drop shadows and white halos. - Enhance road rendering with a white core for better visibility. - Add VP badges and card count fans to opponent panels. - Support card back sprites via `data.cardBack` for accurate card rendering. - Mark Catan as a card game in the registry.
This commit is contained in:
parent
7439945416
commit
29fb4a9b2f
Binary file not shown.
|
Before Width: | Height: | Size: 1.7 MiB |
Binary file not shown.
|
Before Width: | Height: | Size: 2.3 MiB After Width: | Height: | Size: 2.2 MiB |
|
|
@ -22,6 +22,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
this.gameDef = data.game;
|
||||
this.opponents = data.opponents ?? [];
|
||||
this.playfield = data.playfield ?? null;
|
||||
this.cardBack = data.cardBack ?? null;
|
||||
this.gs = null;
|
||||
this.busy = false;
|
||||
this.highlights = [];
|
||||
|
|
@ -70,12 +71,25 @@ export default class CatanGame extends Phaser.Scene {
|
|||
} else {
|
||||
this.add.rectangle(GAME_WIDTH / 2, GAME_HEIGHT / 2, GAME_WIDTH, GAME_HEIGHT, 0x14506b).setDepth(D.board - 2);
|
||||
}
|
||||
// Sea backdrop ring under the island.
|
||||
// Sea backdrop — radial gradient via concentric circles, dark outer to lighter inner.
|
||||
const SEA_X = 1000, SEA_Y = 470, SEA_R = 470;
|
||||
const sea = this.add.graphics().setDepth(D.board - 1);
|
||||
sea.fillStyle(0x0d3a52, 1);
|
||||
sea.fillCircle(1000, 470, 470);
|
||||
sea.lineStyle(6, 0x0a2c40, 1);
|
||||
sea.strokeCircle(1000, 470, 470);
|
||||
const STEPS = 20;
|
||||
for (let i = STEPS; i >= 0; i--) {
|
||||
const t = i / STEPS;
|
||||
const r = Math.round(SEA_R * (i / STEPS));
|
||||
// Interpolate from outer dark (0x07243a) to inner lighter (0x1a5e80)
|
||||
const ro = 0x07, go = 0x24, bo = 0x3a;
|
||||
const ri = 0x1a, gi = 0x5e, bi = 0x80;
|
||||
const red = Math.round(ro + (ri - ro) * (1 - t));
|
||||
const green = Math.round(go + (gi - go) * (1 - t));
|
||||
const blue = Math.round(bo + (bi - bo) * (1 - t));
|
||||
const color = (red << 16) | (green << 8) | blue;
|
||||
sea.fillStyle(color, 1);
|
||||
sea.fillCircle(SEA_X, SEA_Y, r);
|
||||
}
|
||||
sea.lineStyle(4, 0x041820, 0.9);
|
||||
sea.strokeCircle(SEA_X, SEA_Y, SEA_R);
|
||||
}
|
||||
|
||||
buildBoardStatic() {
|
||||
|
|
@ -585,7 +599,7 @@ export default class CatanGame extends Phaser.Scene {
|
|||
const info = this.add.text(x, y + 96, '', {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '14px', color: COLORS.mutedHex, align: 'center',
|
||||
}).setOrigin(0.5, 0).setDepth(D.hud);
|
||||
this.oppPanels.push({ seat, info, x, y });
|
||||
this.oppPanels.push({ seat, info, x, y, cardFan: [], vpBadge: null });
|
||||
});
|
||||
this.updateOpponentPanels();
|
||||
}
|
||||
|
|
@ -599,9 +613,59 @@ export default class CatanGame extends Phaser.Scene {
|
|||
if (this.gs.longestRoad.owner === panel.seat) badges.push('LR');
|
||||
if (this.gs.largestArmy.owner === panel.seat) badges.push('LA');
|
||||
panel.info.setText(
|
||||
`${L.publicVictoryPoints(this.gs, panel.seat)} VP ${cards} cards\n` +
|
||||
`${dev} dev ${p.knightsPlayed} knights` + (badges.length ? `\n[${badges.join(' ')}]` : '')
|
||||
);
|
||||
|
||||
// VP badge circle above the portrait
|
||||
if (panel.vpBadge) { panel.vpBadge.destroy(); panel.vpBadge = null; }
|
||||
const vp = L.publicVictoryPoints(this.gs, panel.seat);
|
||||
const col = PLAYER_COLORS[p.colorIndex];
|
||||
const badgeContainer = this.add.container(panel.x, panel.y - 74).setDepth(D.hud + 5);
|
||||
const bg = this.add.graphics();
|
||||
bg.fillStyle(col.hexDark, 1); bg.fillCircle(0, 0, 18);
|
||||
bg.lineStyle(2.5, col.hex, 0.9); bg.strokeCircle(0, 0, 18);
|
||||
const vpText = this.add.text(0, 0, String(vp), {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '15px', color: col.key === 'white' ? '#000000' : '#ffffff', fontStyle: 'bold',
|
||||
}).setOrigin(0.5);
|
||||
badgeContainer.add([bg, vpText]);
|
||||
panel.vpBadge = badgeContainer;
|
||||
|
||||
// Rebuild face-down card fan to the right of the portrait
|
||||
if (!panel.cardFan) panel.cardFan = [];
|
||||
panel.cardFan.forEach(o => o.destroy());
|
||||
panel.cardFan = [];
|
||||
|
||||
if (cards > 0) {
|
||||
const frameIdx = this.cardBack?.spriteIndex ?? 0;
|
||||
const cardW = 32, cardH = 45;
|
||||
const maxShow = 7;
|
||||
const show = Math.min(cards, maxShow);
|
||||
const step = 16;
|
||||
const fanStartX = panel.x + 74;
|
||||
|
||||
for (let i = 0; i < show; i++) {
|
||||
const img = this.add.image(fanStartX + i * step, panel.y, 'cardbacks', frameIdx)
|
||||
.setDisplaySize(cardW, cardH)
|
||||
.setDepth(D.hud + i + 1);
|
||||
panel.cardFan.push(img);
|
||||
}
|
||||
|
||||
if (cards > maxShow) {
|
||||
const countX = fanStartX + (maxShow - 1) * step + cardW / 2 + 6;
|
||||
const badge = this.add.text(countX, panel.y, `×${cards}`, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '16px', color: '#ffd700',
|
||||
stroke: '#000000', strokeThickness: 3,
|
||||
}).setOrigin(0, 0.5).setDepth(D.hud + 20);
|
||||
panel.cardFan.push(badge);
|
||||
} else {
|
||||
const countX = fanStartX + (show - 1) * step + cardW / 2 + 6;
|
||||
const badge = this.add.text(countX, panel.y, `${cards}`, {
|
||||
fontFamily: '"Julius Sans One"', fontSize: '16px', color: COLORS.mutedHex,
|
||||
stroke: '#000000', strokeThickness: 2,
|
||||
}).setOrigin(0, 0.5).setDepth(D.hud + 20);
|
||||
panel.cardFan.push(badge);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -626,8 +690,10 @@ export default class CatanGame extends Phaser.Scene {
|
|||
for (const eid of p.roads) {
|
||||
const [a, b] = EDGES[eid].nodes;
|
||||
const g = this.add.graphics().setDepth(D.road);
|
||||
g.lineStyle(12, col.hexDark, 1); g.lineBetween(NODES[a].x, NODES[a].y, NODES[b].x, NODES[b].y);
|
||||
g.lineStyle(7, col.hex, 1); g.lineBetween(NODES[a].x, NODES[a].y, NODES[b].x, NODES[b].y);
|
||||
const ax = NODES[a].x, ay = NODES[a].y, bx = NODES[b].x, by = NODES[b].y;
|
||||
g.lineStyle(16, 0xffffff, 0.9); g.lineBetween(ax, ay, bx, by);
|
||||
g.lineStyle(12, col.hexDark, 1); g.lineBetween(ax, ay, bx, by);
|
||||
g.lineStyle(7, col.hex, 1); g.lineBetween(ax, ay, bx, by);
|
||||
this.pieceObjs.push(g);
|
||||
}
|
||||
}
|
||||
|
|
@ -641,24 +707,38 @@ export default class CatanGame extends Phaser.Scene {
|
|||
|
||||
makeSettlement(x, y, col) {
|
||||
const g = this.add.graphics().setDepth(D.building);
|
||||
g.fillStyle(0x000000, 0.25); g.fillRoundedRect(x - 11, y - 6, 24, 18, 3);
|
||||
// Drop shadow
|
||||
g.fillStyle(0x000000, 0.55); g.fillRoundedRect(x - 13, y - 1, 34, 24, 4);
|
||||
// White halo (3px border around the scaled-up shape)
|
||||
g.fillStyle(0xffffff, 1);
|
||||
g.fillRect(x - 16, y - 4, 32, 20);
|
||||
g.fillTriangle(x - 19, y - 4, x + 19, y - 4, x, y - 21);
|
||||
// Player color fill (~30% larger than original)
|
||||
g.fillStyle(col.hex, 1);
|
||||
g.fillRect(x - 10, y - 3, 20, 13);
|
||||
g.fillTriangle(x - 12, y - 3, x + 12, y - 3, x, y - 14);
|
||||
g.lineStyle(2, col.hexDark, 1);
|
||||
g.strokeRect(x - 10, y - 3, 20, 13);
|
||||
g.fillRect(x - 13, y - 4, 26, 17);
|
||||
g.fillTriangle(x - 16, y - 4, x + 16, y - 4, x, y - 18);
|
||||
// Dark outline
|
||||
g.lineStyle(2.5, col.hexDark, 1); g.strokeRect(x - 13, y - 4, 26, 17);
|
||||
return g;
|
||||
}
|
||||
makeCity(x, y, col) {
|
||||
const g = this.add.graphics().setDepth(D.building);
|
||||
g.fillStyle(0x000000, 0.25); g.fillRoundedRect(x - 17, y - 10, 36, 24, 3);
|
||||
// Drop shadow
|
||||
g.fillStyle(0x000000, 0.55); g.fillRoundedRect(x - 19, y - 7, 50, 38, 4);
|
||||
// White halo (3px border around the scaled-up shape)
|
||||
g.fillStyle(0xffffff, 1);
|
||||
g.fillRect(x - 24, y, 27, 21); // lower block halo
|
||||
g.fillRect(x - 8, y - 10, 32, 32); // tower halo
|
||||
g.fillTriangle(x - 11, y - 10, x + 26, y - 10, x + 8, y - 26); // roof halo
|
||||
// Player color fill (~30% larger than original)
|
||||
g.fillStyle(col.hex, 1);
|
||||
g.fillRect(x - 16, y, 16, 14); // lower block
|
||||
g.fillRect(x - 4, y - 8, 20, 22); // tower block
|
||||
g.fillTriangle(x - 6, y - 8, x + 18, y - 8, x + 6, y - 18);
|
||||
g.lineStyle(2, col.hexDark, 1);
|
||||
g.strokeRect(x - 16, y, 16, 14);
|
||||
g.strokeRect(x - 4, y - 8, 20, 22);
|
||||
g.fillRect(x - 21, y, 21, 18); // lower block
|
||||
g.fillRect(x - 5, y - 10, 26, 29); // tower
|
||||
g.fillTriangle(x - 8, y - 10, x + 23, y - 10, x + 8, y - 23); // roof
|
||||
// Dark outlines
|
||||
g.lineStyle(2.5, col.hexDark, 1);
|
||||
g.strokeRect(x - 21, y, 21, 18);
|
||||
g.strokeRect(x - 5, y - 10, 26, 29);
|
||||
return g;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@ export default class PreloadScene extends Phaser.Scene {
|
|||
frameWidth: 312,
|
||||
frameHeight: 312,
|
||||
});
|
||||
this.load.image('bg-menu', '/assets/images/background-menu.png');
|
||||
this.load.image('bg-menu', '/assets/images/background-menu.png');
|
||||
this.load.image('bg-room', '/assets/images/background-room.png');
|
||||
this.load.image('bg-casino', '/assets/images/background-casino.png');
|
||||
this.load.image('main-title', '/assets/images/main-title.png');
|
||||
|
|
|
|||
|
|
@ -37,4 +37,4 @@ registerGame({ slug: 'craps', name: 'Craps', category: 'casino', minPlayers: 1,
|
|||
registerGame({ slug: 'roulette', name: 'Roulette', category: 'casino', minPlayers: 1, maxPlayers: 7, minOpponents: 0, maxOpponents: 6 });
|
||||
registerGame({ slug: 'mexicantrain', name: 'Mexican Train', category: 'tabletop', minPlayers: 2, maxPlayers: 4, minOpponents: 1, maxOpponents: 3 });
|
||||
registerGame({ slug: 'hearts', name: 'Hearts', category: 'cards', cardGame: true, minPlayers: 4, maxPlayers: 4, minOpponents: 3, maxOpponents: 3 });
|
||||
registerGame({ slug: 'catan', name: 'Settlers of Catan', category: 'tabletop', minPlayers: 3, maxPlayers: 4, minOpponents: 2, maxOpponents: 3 });
|
||||
registerGame({ slug: 'catan', name: 'Settlers of Catan', category: 'tabletop', cardGame: true, minPlayers: 3, maxPlayers: 4, minOpponents: 2, maxOpponents: 3 });
|
||||
|
|
|
|||
Loading…
Reference in New Issue