map/mapGeography.js
2026-05-28 00:30:09 +09:00

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),
},
},
};
}