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