387 lines
12 KiB
JavaScript
387 lines
12 KiB
JavaScript
import { INF, MAP_H, MAP_W, SIZE, clamp, indexOf, inside } from "./mapUtils.js";
|
|
import { influenceFromPoints } from "./mapGeneratorHelpers.js";
|
|
|
|
const GEOGRAPHY_VERSION = "unified-geography-v1";
|
|
|
|
function localConfluenceScore(x, y, river) {
|
|
let arms = 0;
|
|
let strong = 0;
|
|
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1],[1,1],[-1,1],[1,-1],[-1,-1]]) {
|
|
const nx = x + dx;
|
|
const ny = y + dy;
|
|
if (!inside(nx, ny)) continue;
|
|
const rv = river?.[indexOf(nx, ny)] || 0;
|
|
if (rv > 0.18) arms++;
|
|
if (rv > 0.34) strong++;
|
|
}
|
|
return clamp((arms >= 3 ? 0.22 : arms === 2 ? 0.09 : 0) + strong * 0.04);
|
|
}
|
|
|
|
function summarizeField(field, sea = null) {
|
|
let min = INF;
|
|
let max = -INF;
|
|
let sum = 0;
|
|
let count = 0;
|
|
for (let i = 0; i < SIZE; i++) {
|
|
if (sea?.[i]) continue;
|
|
const v = field?.[i];
|
|
if (!Number.isFinite(v)) continue;
|
|
min = Math.min(min, v);
|
|
max = Math.max(max, v);
|
|
sum += v;
|
|
count++;
|
|
}
|
|
return {
|
|
min: count ? Math.round(min * 1000) / 1000 : 0,
|
|
max: count ? Math.round(max * 1000) / 1000 : 0,
|
|
mean: count ? Math.round((sum / count) * 1000) / 1000 : 0,
|
|
};
|
|
}
|
|
|
|
function buildProfiles(idField, terrain, fields) {
|
|
if (!idField) return [];
|
|
const { sea } = terrain;
|
|
const rows = new Map();
|
|
for (let i = 0; i < SIZE; i++) {
|
|
if (sea?.[i]) continue;
|
|
const id = idField[i];
|
|
if (id < 0) continue;
|
|
let row = rows.get(id);
|
|
if (!row) {
|
|
row = {
|
|
id,
|
|
area: 0,
|
|
habitableCells: 0,
|
|
lowlandCells: 0,
|
|
barrierCells: 0,
|
|
habitabilitySum: 0,
|
|
accessibilitySum: 0,
|
|
centralitySum: 0,
|
|
barrierSum: 0,
|
|
sx: 0,
|
|
sy: 0,
|
|
};
|
|
rows.set(id, row);
|
|
}
|
|
const x = i % MAP_W;
|
|
const y = Math.floor(i / MAP_W);
|
|
const h = fields.habitability?.[i] || 0;
|
|
const a = fields.accessibility?.[i] || 0;
|
|
const c = fields.centrality?.[i] || fields.naturalCentrality?.[i] || 0;
|
|
const b = fields.geographicBarrier?.[i] || 0;
|
|
row.area++;
|
|
row.habitabilitySum += h;
|
|
row.accessibilitySum += a;
|
|
row.centralitySum += c;
|
|
row.barrierSum += b;
|
|
row.sx += x;
|
|
row.sy += y;
|
|
if (h > 0.26) row.habitableCells++;
|
|
if ((terrain.plain?.[i] || 0) > 0.24 || (terrain.basinField?.[i] || 0) > 0.24 || (terrain.coastalLowland?.[i] || 0) > 0.22) row.lowlandCells++;
|
|
if (b > 0.52) row.barrierCells++;
|
|
}
|
|
return [...rows.values()]
|
|
.map((row) => ({
|
|
id: row.id,
|
|
area: row.area,
|
|
cx: Math.round((row.sx / Math.max(1, row.area)) * 10) / 10,
|
|
cy: Math.round((row.sy / Math.max(1, row.area)) * 10) / 10,
|
|
habitableRatio: Math.round((row.habitableCells / Math.max(1, row.area)) * 1000) / 1000,
|
|
lowlandRatio: Math.round((row.lowlandCells / Math.max(1, row.area)) * 1000) / 1000,
|
|
barrierRatio: Math.round((row.barrierCells / Math.max(1, row.area)) * 1000) / 1000,
|
|
avgHabitability: Math.round((row.habitabilitySum / Math.max(1, row.area)) * 1000) / 1000,
|
|
avgAccessibility: Math.round((row.accessibilitySum / Math.max(1, row.area)) * 1000) / 1000,
|
|
avgCentrality: Math.round((row.centralitySum / Math.max(1, row.area)) * 1000) / 1000,
|
|
avgBarrier: Math.round((row.barrierSum / Math.max(1, row.area)) * 1000) / 1000,
|
|
}))
|
|
.sort((a, b) => b.area - a.area || a.id - b.id);
|
|
}
|
|
|
|
export function buildGeographicBasis(seed, terrain) {
|
|
const {
|
|
elevation,
|
|
slope,
|
|
sea,
|
|
river,
|
|
floodplain,
|
|
plain,
|
|
agriculture,
|
|
ridgeField,
|
|
valleyField,
|
|
basinField,
|
|
coastalLowland,
|
|
flowAccum,
|
|
naturalBarrierScore,
|
|
portSuitability,
|
|
crossingSuitability,
|
|
passSuitability,
|
|
depositionalLowland,
|
|
alluvialFanField,
|
|
deltaField,
|
|
naturalCompartmentId,
|
|
watershedId,
|
|
} = terrain;
|
|
|
|
const habitability = new Float32Array(SIZE);
|
|
const lowlandCapacity = new Float32Array(SIZE);
|
|
const valleyAccess = new Float32Array(SIZE);
|
|
const coastalAccess = new Float32Array(SIZE);
|
|
const geographicBarrier = new Float32Array(SIZE);
|
|
const geographicBarrierCost = new Float32Array(SIZE);
|
|
const corridorSuitability = new Float32Array(SIZE);
|
|
const accessibility = new Float32Array(SIZE);
|
|
const naturalCentrality = new Float32Array(SIZE);
|
|
const centrality = new Float32Array(SIZE);
|
|
const adminBoundaryPreference = new Float32Array(SIZE);
|
|
const boundaryAvoidance = new Float32Array(SIZE);
|
|
|
|
for (let y = 0; y < MAP_H; y++) {
|
|
for (let x = 0; x < MAP_W; x++) {
|
|
const i = indexOf(x, y);
|
|
if (sea?.[i]) {
|
|
geographicBarrier[i] = 1;
|
|
geographicBarrierCost[i] = INF;
|
|
continue;
|
|
}
|
|
|
|
const depositional =
|
|
(depositionalLowland?.[i] || 0) * 0.92 +
|
|
(alluvialFanField?.[i] || 0) * 0.56 +
|
|
(deltaField?.[i] || 0) * 0.86;
|
|
const highElevation = clamp(((elevation?.[i] || 0) - 0.54) / 0.34);
|
|
const lowSlope = clamp(1 - (slope?.[i] || 0) * 2.25);
|
|
const confluence = x > 0 && y > 0 && x < MAP_W - 1 && y < MAP_H - 1 ? localConfluenceScore(x, y, river) : 0;
|
|
const naturalBarrier = naturalBarrierScore?.[i] || 0;
|
|
const pass = passSuitability?.[i] || 0;
|
|
const crossing = crossingSuitability?.[i] || 0;
|
|
const port = portSuitability?.[i] || 0;
|
|
|
|
lowlandCapacity[i] = clamp(
|
|
(plain?.[i] || 0) * 0.38 +
|
|
(agriculture?.[i] || 0) * 0.28 +
|
|
(basinField?.[i] || 0) * 0.24 +
|
|
(coastalLowland?.[i] || 0) * 0.18 +
|
|
depositional * 0.20 +
|
|
lowSlope * 0.13 -
|
|
(slope?.[i] || 0) * 0.55 -
|
|
(ridgeField?.[i] || 0) * 0.38 -
|
|
highElevation * 0.62
|
|
);
|
|
valleyAccess[i] = clamp(
|
|
(valleyField?.[i] || 0) * 0.48 +
|
|
confluence * 0.40 +
|
|
crossing * 0.22 +
|
|
pass * 0.18 +
|
|
(flowAccum?.[i] || 0) * 0.08 +
|
|
(basinField?.[i] || 0) * 0.12 -
|
|
(slope?.[i] || 0) * 0.42 -
|
|
(ridgeField?.[i] || 0) * 0.22
|
|
);
|
|
coastalAccess[i] = clamp(
|
|
(coastalLowland?.[i] || 0) * 0.46 +
|
|
port * 0.34 +
|
|
(deltaField?.[i] || 0) * 0.22 +
|
|
(plain?.[i] || 0) * 0.10 -
|
|
(slope?.[i] || 0) * 0.42 -
|
|
(ridgeField?.[i] || 0) * 0.20
|
|
);
|
|
geographicBarrier[i] = clamp(
|
|
(slope?.[i] || 0) * 0.60 +
|
|
(ridgeField?.[i] || 0) * 0.52 +
|
|
naturalBarrier * 0.58 +
|
|
highElevation * 0.54 +
|
|
Math.max(0, (elevation?.[i] || 0) - 0.68) * 0.90 -
|
|
(valleyField?.[i] || 0) * 0.18 -
|
|
(basinField?.[i] || 0) * 0.12 -
|
|
pass * 0.30 -
|
|
(plain?.[i] || 0) * 0.10 -
|
|
(coastalLowland?.[i] || 0) * 0.06
|
|
);
|
|
geographicBarrierCost[i] = 1 + geographicBarrier[i] * 8.5 + (slope?.[i] || 0) * 3.0 + highElevation * 4.2;
|
|
habitability[i] = clamp(
|
|
lowlandCapacity[i] * 0.70 +
|
|
valleyAccess[i] * 0.22 +
|
|
coastalAccess[i] * 0.26 +
|
|
(agriculture?.[i] || 0) * 0.16 +
|
|
confluence * 0.07 -
|
|
geographicBarrier[i] * 0.38 -
|
|
(floodplain?.[i] || 0) * 0.04
|
|
);
|
|
corridorSuitability[i] = clamp(
|
|
(valleyField?.[i] || 0) * 0.30 +
|
|
(coastalLowland?.[i] || 0) * 0.23 +
|
|
(plain?.[i] || 0) * 0.18 +
|
|
(basinField?.[i] || 0) * 0.18 +
|
|
pass * 0.22 +
|
|
crossing * 0.12 +
|
|
habitability[i] * 0.20 -
|
|
geographicBarrier[i] * 0.32 -
|
|
(slope?.[i] || 0) * 0.20
|
|
);
|
|
accessibility[i] = clamp(
|
|
corridorSuitability[i] * 0.44 +
|
|
port * 0.16 +
|
|
crossing * 0.12 +
|
|
pass * 0.10 +
|
|
habitability[i] * 0.22 -
|
|
geographicBarrier[i] * 0.16
|
|
);
|
|
naturalCentrality[i] = clamp(
|
|
habitability[i] * 0.50 +
|
|
accessibility[i] * 0.30 +
|
|
(basinField?.[i] || 0) * 0.14 +
|
|
(plain?.[i] || 0) * 0.10 +
|
|
(coastalLowland?.[i] || 0) * 0.08 +
|
|
port * 0.08 +
|
|
confluence * 0.06 -
|
|
geographicBarrier[i] * 0.20
|
|
);
|
|
centrality[i] = naturalCentrality[i];
|
|
adminBoundaryPreference[i] = clamp(
|
|
naturalBarrier * 0.54 +
|
|
(ridgeField?.[i] || 0) * 0.32 +
|
|
(river?.[i] || 0) * 0.12 +
|
|
(flowAccum?.[i] || 0) * 0.08 -
|
|
naturalCentrality[i] * 0.20 -
|
|
habitability[i] * 0.08
|
|
);
|
|
boundaryAvoidance[i] = clamp(
|
|
naturalCentrality[i] * 0.54 +
|
|
habitability[i] * 0.24 +
|
|
(plain?.[i] || 0) * 0.12 +
|
|
(basinField?.[i] || 0) * 0.08 -
|
|
geographicBarrier[i] * 0.18
|
|
);
|
|
}
|
|
}
|
|
|
|
const fields = {
|
|
version: GEOGRAPHY_VERSION,
|
|
habitability,
|
|
lowlandCapacity,
|
|
valleyAccess,
|
|
coastalAccess,
|
|
geographicBarrier,
|
|
geographicBarrierCost,
|
|
barrierCost: geographicBarrierCost,
|
|
corridorSuitability,
|
|
accessibility,
|
|
naturalCentrality,
|
|
centrality,
|
|
adminBoundaryPreference,
|
|
boundaryAvoidance,
|
|
};
|
|
|
|
const geographyDebug = {
|
|
version: GEOGRAPHY_VERSION,
|
|
stage: "terrain-derived",
|
|
fields: {
|
|
habitability: summarizeField(habitability, sea),
|
|
accessibility: summarizeField(accessibility, sea),
|
|
centrality: summarizeField(centrality, sea),
|
|
geographicBarrier: summarizeField(geographicBarrier, sea),
|
|
corridorSuitability: summarizeField(corridorSuitability, sea),
|
|
},
|
|
naturalCompartmentCount: new Set([...naturalCompartmentId || []].filter((id, i) => id >= 0 && !sea?.[i])).size,
|
|
watershedCount: new Set([...watershedId || []].filter((id, i) => id >= 0 && !sea?.[i])).size,
|
|
};
|
|
|
|
return {
|
|
...fields,
|
|
compartmentProfiles: buildProfiles(naturalCompartmentId, terrain, fields),
|
|
watershedProfiles: buildProfiles(watershedId, terrain, fields),
|
|
geographyDebug,
|
|
};
|
|
}
|
|
|
|
export function finalizeGeographicBasis(seed, terrain, features, baseGeography) {
|
|
const base = baseGeography || buildGeographicBasis(seed, terrain);
|
|
const { sea } = terrain;
|
|
const {
|
|
roadInfluence,
|
|
railInfluence2,
|
|
stationInfluence,
|
|
populationDensity,
|
|
ports = [],
|
|
modernCities = [],
|
|
markets = [],
|
|
} = features || {};
|
|
|
|
const portInfluence = influenceFromPoints(ports, 16, (p) => p.portClass === "major" ? 1.1 : p.portClass === "regional" ? 0.78 : 0.34);
|
|
const cityInfluence = influenceFromPoints(modernCities, 24, (p) => clamp((p.population || 50000) / 240000, 0.35, 2.4));
|
|
const marketInfluence = influenceFromPoints(markets, 13, (p) => clamp((p.population || 10000) / 48000, 0.18, 1.0));
|
|
|
|
const accessibility = new Float32Array(SIZE);
|
|
const transportAccessibility = new Float32Array(SIZE);
|
|
const centrality = new Float32Array(SIZE);
|
|
const humanCentrality = new Float32Array(SIZE);
|
|
const boundaryAvoidance = new Float32Array(SIZE);
|
|
const adminBoundaryPreference = new Float32Array(SIZE);
|
|
|
|
for (let i = 0; i < SIZE; i++) {
|
|
if (sea?.[i]) continue;
|
|
transportAccessibility[i] = clamp(
|
|
(roadInfluence?.[i] || 0) * 0.36 +
|
|
(railInfluence2?.[i] || 0) * 0.28 +
|
|
(stationInfluence?.[i] || 0) * 0.20 +
|
|
portInfluence[i] * 0.16
|
|
);
|
|
accessibility[i] = clamp(
|
|
(base.accessibility?.[i] || 0) * 0.46 +
|
|
transportAccessibility[i] * 0.48 +
|
|
(base.corridorSuitability?.[i] || 0) * 0.10 -
|
|
(base.geographicBarrier?.[i] || 0) * 0.10
|
|
);
|
|
humanCentrality[i] = clamp(
|
|
(populationDensity?.[i] || 0) * 0.38 +
|
|
cityInfluence[i] * 0.30 +
|
|
marketInfluence[i] * 0.14 +
|
|
transportAccessibility[i] * 0.18
|
|
);
|
|
centrality[i] = clamp(
|
|
(base.naturalCentrality?.[i] || 0) * 0.44 +
|
|
accessibility[i] * 0.26 +
|
|
humanCentrality[i] * 0.36 -
|
|
(base.geographicBarrier?.[i] || 0) * 0.08
|
|
);
|
|
boundaryAvoidance[i] = clamp(
|
|
(base.boundaryAvoidance?.[i] || 0) * 0.52 +
|
|
centrality[i] * 0.42 +
|
|
transportAccessibility[i] * 0.10
|
|
);
|
|
adminBoundaryPreference[i] = clamp(
|
|
(base.adminBoundaryPreference?.[i] || 0) * 0.84 -
|
|
centrality[i] * 0.12 -
|
|
transportAccessibility[i] * 0.08
|
|
);
|
|
}
|
|
|
|
const finalFields = {
|
|
...base,
|
|
accessibility,
|
|
transportAccessibility,
|
|
centrality,
|
|
humanCentrality,
|
|
boundaryAvoidance,
|
|
adminBoundaryPreference,
|
|
};
|
|
|
|
return {
|
|
...finalFields,
|
|
compartmentProfiles: buildProfiles(terrain.naturalCompartmentId, terrain, finalFields),
|
|
watershedProfiles: buildProfiles(terrain.watershedId, terrain, finalFields),
|
|
geographyDebug: {
|
|
...(base.geographyDebug || {}),
|
|
stage: "finalized-with-human-network",
|
|
fields: {
|
|
...(base.geographyDebug?.fields || {}),
|
|
accessibility: summarizeField(accessibility, sea),
|
|
transportAccessibility: summarizeField(transportAccessibility, sea),
|
|
centrality: summarizeField(centrality, sea),
|
|
humanCentrality: summarizeField(humanCentrality, sea),
|
|
adminBoundaryPreference: summarizeField(adminBoundaryPreference, sea),
|
|
boundaryAvoidance: summarizeField(boundaryAvoidance, sea),
|
|
},
|
|
},
|
|
};
|
|
}
|