feat(catan): refine hex tile rendering with inset borders and background

- Add `background-catan-board.png` for enhanced board visuals
- Implement inset polygon logic to create layered hex borders (colored swatch, dark ring, image area)
- Adjust tile image scaling to fit within the innermost inset polygon
- Reduce outer border line width for a crisper appearance
- Add fallback resource color fill for the inner image area if texture is missing
This commit is contained in:
Brian Fertig 2026-05-23 13:04:09 -06:00
parent e562ed21b8
commit 7439945416
2 changed files with 28 additions and 8 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@ -106,31 +106,51 @@ export default class CatanGame extends Phaser.Scene {
this.hexLabels.forEach((t) => t.destroy()); this.hexLabels.forEach((t) => t.destroy());
this.hexLabels = []; this.hexLabels = [];
// Inset a convex hex polygon toward its center.
// s=0 collapses to point; s=1 is original. Uses inradius (79.7px) as scale reference.
const inset = (pts, cx, cy, s) =>
pts.map(p => ({ x: cx + (p.x - cx) * s, y: cy + (p.y - cy) * s }));
for (const hex of this.gs.hexes) { for (const hex of this.gs.hexes) {
const pts = HEXES[hex.id].corners.map((c) => ({ x: NODES[c].x, y: NODES[c].y })); const pts = HEXES[hex.id].corners.map((c) => ({ x: NODES[c].x, y: NODES[c].y }));
const { x, y } = this.hexPos(hex.id); const { x, y } = this.hexPos(hex.id);
// Color fill as fallback base layer // Inradius ≈ 79.7; compute scale factors for 7px colored ring + 4px dark ring
const color = hex.resource === 'desert' ? DESERT_COLOR : RESOURCE_INFO[hex.resource].color; const s1 = 1 - 7 / (HEX_SIZE * Math.sqrt(3) / 2); // after colored border
g.fillStyle(color, 1); const s2 = 1 - 11 / (HEX_SIZE * Math.sqrt(3) / 2); // after dark border (image area)
const innerPts = inset(pts, x, y, s1);
const imagePts = inset(pts, x, y, s2);
// Layer 1: resource swatch fill (outer colored border ring)
const swatch = hex.resource === 'desert' ? DESERT_COLOR : RESOURCE_INFO[hex.resource].swatch;
g.fillStyle(swatch, 1);
g.fillPoints(pts, true); g.fillPoints(pts, true);
// Tile image clipped to hex polygon // Layer 2: dark fill inset (inner black border ring)
g.fillStyle(0x111111, 1);
g.fillPoints(innerPts, true);
// Layer 3: tile image masked to innermost polygon
if (this.textures.exists('catan-tiles')) { if (this.textures.exists('catan-tiles')) {
const frames = CatanGame.TILE_FRAMES[hex.resource] ?? [10, 11]; const frames = CatanGame.TILE_FRAMES[hex.resource] ?? [10, 11];
const frame = frames[Math.floor(Math.random() * 2)]; const frame = frames[Math.floor(Math.random() * 2)];
const maskG = this.make.graphics({ x: 0, y: 0, add: false }); const maskG = this.make.graphics({ x: 0, y: 0, add: false });
maskG.fillStyle(0xffffff); maskG.fillStyle(0xffffff);
maskG.fillPoints(pts, true); maskG.fillPoints(imagePts, true);
const img = this.add.image(x, y, 'catan-tiles', frame) const img = this.add.image(x, y, 'catan-tiles', frame)
.setDisplaySize(HEX_W, HEX_SIZE * 2) .setDisplaySize(HEX_W * s2, HEX_SIZE * 2 * s2)
.setMask(maskG.createGeometryMask()) .setMask(maskG.createGeometryMask())
.setDepth(D.board + 1); .setDepth(D.board + 1);
this.hexImgs.push({ img, maskG }); this.hexImgs.push({ img, maskG });
} else {
// Fallback: resource color fill in the image area
const color = hex.resource === 'desert' ? DESERT_COLOR : RESOURCE_INFO[hex.resource].color;
g.fillStyle(color, 1);
g.fillPoints(imagePts, true);
} }
// Border on top of images // Thin outer crisp stroke
this.hexBorderGfx.lineStyle(4, 0x6b4a1a, 0.85); this.hexBorderGfx.lineStyle(2, 0x4a3210, 0.8);
this.hexBorderGfx.strokePoints(pts, true); this.hexBorderGfx.strokePoints(pts, true);
// Resource label // Resource label