export const MAP_W = 258; export const MAP_H = 183; export const CELL_SIZE = 4; export const SIZE = MAP_W * MAP_H; export const INF = 1e9; export function indexOf(x, y) { return y * MAP_W + x; } export function xyOf(i) { return [i % MAP_W, Math.floor(i / MAP_W)]; } export function inside(x, y) { return x >= 0 && y >= 0 && x < MAP_W && y < MAP_H; } export function clamp(v, a = 0, b = 1) { return Math.max(a, Math.min(b, v)); } export function nearMapEdge(x, y, margin = 1) { return x < margin || y < margin || x >= MAP_W - margin || y >= MAP_H - margin; } export function hash2(x, y, seed) { let h = Math.imul((x | 0) ^ (seed | 0), 374761393) + Math.imul((y | 0) ^ ((seed >>> 1) | 0), 668265263); h = (h ^ (h >>> 13)) >>> 0; h = Math.imul(h, 1274126177) >>> 0; return ((h ^ (h >>> 16)) >>> 0) / 4294967295; } export function rand(seed, n) { return hash2(n * 7919 + 17, n * 104729 + 31, seed); } export function smoothstep(t) { t = clamp(t); return t * t * (3 - 2 * t); } export function lerp(a, b, t) { return a + (b - a) * t; } export function valueNoise(x, y, seed, scale) { const sx = x / scale; const sy = y / scale; const x0 = Math.floor(sx); const y0 = Math.floor(sy); const tx = smoothstep(sx - x0); const ty = smoothstep(sy - y0); const a = hash2(x0, y0, seed); const b = hash2(x0 + 1, y0, seed); const c = hash2(x0, y0 + 1, seed); const d = hash2(x0 + 1, y0 + 1, seed); return lerp(lerp(a, b, tx), lerp(c, d, tx), ty); } export function fbm(x, y, seed) { let amp = 1; let scale = 54; let sum = 0; let norm = 0; for (let i = 0; i < 5; i++) { sum += valueNoise(x, y, seed + i * 101, scale) * amp; norm += amp; amp *= 0.5; scale *= 0.5; } return sum / norm; } export function pickEntities(candidates, { max = 99, minDistance = 5, threshold = 0, seed = 0, jitter = 0.04 } = {}) { const sorted = candidates .filter((p) => Number.isFinite(p.score) && p.score >= threshold) .map((p) => ({ ...p, score: p.score + hash2(p.x, p.y, seed + 54321) * jitter })) .sort((a, b) => b.score - a.score); const out = []; for (const candidate of sorted) { if (out.every((p) => Math.hypot(p.x - candidate.x, p.y - candidate.y) >= minDistance)) { out.push(candidate); if (out.length >= max) break; } } return out; } export function createMapFields() { const flowTo = new Int32Array(SIZE); flowTo.fill(-1); return { elevation: new Float32Array(SIZE), moisture: new Float32Array(SIZE), slope: new Float32Array(SIZE), sea: new Uint8Array(SIZE), ocean: new Uint8Array(SIZE), lake: new Uint8Array(SIZE), river: new Float32Array(SIZE), floodplain: new Float32Array(SIZE), plain: new Float32Array(SIZE), agriculture: new Float32Array(SIZE), ridgeField: new Float32Array(SIZE), valleyField: new Float32Array(SIZE), basinField: new Float32Array(SIZE), coastalLowland: new Float32Array(SIZE), flowAccum: new Float32Array(SIZE), erosionField: new Float32Array(SIZE), depositionField: new Float32Array(SIZE), arcSpineField: new Float32Array(SIZE), branchRidgeField: new Float32Array(SIZE), depositionalLowland: new Float32Array(SIZE), alluvialFanField: new Float32Array(SIZE), deltaField: new Float32Array(SIZE), naturalBarrierScore: new Float32Array(SIZE), flowTo, portSuitability: new Float32Array(SIZE), crossingSuitability: new Float32Array(SIZE), passSuitability: new Float32Array(SIZE), }; } export class MinHeap { constructor() { this.items = []; } push(item) { this.items.push(item); let i = this.items.length - 1; while (i > 0) { const parent = (i - 1) >> 1; if (this.items[parent].f <= item.f) break; this.items[i] = this.items[parent]; i = parent; } this.items[i] = item; } pop() { if (this.items.length === 0) return null; const root = this.items[0]; const last = this.items.pop(); if (this.items.length > 0) { let i = 0; while (true) { const left = i * 2 + 1; const right = left + 1; if (left >= this.items.length) break; const child = right < this.items.length && this.items[right].f < this.items[left].f ? right : left; if (this.items[child].f >= last.f) break; this.items[i] = this.items[child]; i = child; } this.items[i] = last; } return root; } get length() { return this.items.length; } } export function weightedScore(terms) { let total = 0; for (const [value, weight] of terms) total += value * weight; return total; }