84 lines
3.1 KiB
JavaScript
84 lines
3.1 KiB
JavaScript
// Shared helpers for Seafarers scenario boards.
|
|
//
|
|
// A scenario describes its board as a list of `cells` (one per hex, in hex-id
|
|
// order) plus a row layout. assembleBoard() turns that into registered topology
|
|
// + the per-hex state data CatanLogic expects. No Phaser here.
|
|
|
|
import {
|
|
rowCenters, buildBoard, registerBoard, portsFromEdges,
|
|
HEX_SIZE, BOARD_CX, BOARD_CY, PORT_BAG,
|
|
} from '../../CatanBoard.js';
|
|
|
|
export const SHIP_COST = { wool: 1, lumber: 1 };
|
|
|
|
// Larger Seafarers boards use a slightly smaller hex so they fit the canvas.
|
|
export const SEA_HEX_SIZE = 74;
|
|
|
|
export function shuffle(arr) {
|
|
const a = [...arr];
|
|
for (let i = a.length - 1; i > 0; i--) {
|
|
const j = Math.floor(Math.random() * (i + 1));
|
|
[a[i], a[j]] = [a[j], a[i]];
|
|
}
|
|
return a;
|
|
}
|
|
|
|
// A cell: { kind: 'land'|'sea'|'gold'|'fog'|'desert', resource?, number? }.
|
|
// Distribute generic + 2:1 ports onto coastal land edges, spaced around the rim.
|
|
function autoPorts(geo, hexes, cx, cy) {
|
|
const isLand = (hid) => {
|
|
const k = hexes[hid]?.kind;
|
|
return k === 'land' || k === 'gold' || k === 'desert';
|
|
};
|
|
// Shore edges: touch exactly one land hex (the other side is water or
|
|
// off-board), so a settlement on that shore could use the port.
|
|
const ring = geo.edges.filter((e) => e.hexes.filter(isLand).length === 1);
|
|
ring.sort((p, q) => {
|
|
const pm = midOf(geo, p), qm = midOf(geo, q);
|
|
return Math.atan2(pm.y - cy, pm.x - cx) - Math.atan2(qm.y - cy, qm.x - cx);
|
|
});
|
|
const bag = shuffle(PORT_BAG);
|
|
const n = Math.min(bag.length, ring.length);
|
|
const entries = [];
|
|
const used = new Set();
|
|
for (let i = 0; i < n && ring.length; i++) {
|
|
let idx = Math.round((i * ring.length) / n) % ring.length;
|
|
while (used.has(idx)) idx = (idx + 1) % ring.length;
|
|
used.add(idx);
|
|
entries.push({ edgeId: ring[idx].id, type: bag[i] });
|
|
}
|
|
return portsFromEdges(geo, entries);
|
|
}
|
|
|
|
function midOf(geo, edge) {
|
|
const a = geo.nodes[edge.nodes[0]], b = geo.nodes[edge.nodes[1]];
|
|
return { x: (a.x + b.x) / 2, y: (a.y + b.y) / 2 };
|
|
}
|
|
|
|
// Build a scenario board from cells. Returns the board payload createInitialState
|
|
// merges into game state. `pirate` is a sea hex id (or null).
|
|
export function assembleBoard({ id, rows, cells, homeIds = [], pirate = null, winVP = 13, size = SEA_HEX_SIZE, cx = BOARD_CX, cy = BOARD_CY }) {
|
|
const centers = rowCenters(rows, cx, cy, size);
|
|
const geo = buildBoard({ centers, size, cx, cy });
|
|
registerBoard(id, geo);
|
|
|
|
const hexes = cells.map((c, i) => ({
|
|
id: i,
|
|
kind: c.kind,
|
|
resource: c.resource ?? null,
|
|
number: c.number ?? null,
|
|
hasRobber: false,
|
|
// Fog hexes carry hidden data that replaces kind/resource/number on reveal.
|
|
fogData: c.kind === 'fog' && c.reveal ? { kind: c.reveal.kind, resource: c.reveal.resource ?? null, number: c.reveal.number ?? null } : null,
|
|
}));
|
|
|
|
// Robber starts on the (first) desert, if any.
|
|
const desert = hexes.find((h) => h.kind === 'desert');
|
|
let robberHex = desert ? desert.id : null;
|
|
if (desert) desert.hasRobber = true;
|
|
|
|
const ports = autoPorts(geo, hexes, cx, cy);
|
|
|
|
return { boardId: id, hexes, ports, robberHex, pirateHex: pirate, homeIds: [...homeIds], winVP };
|
|
}
|