175 lines
4.5 KiB
JavaScript
175 lines
4.5 KiB
JavaScript
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;
|
|
}
|