import { INF, MAP_H, MAP_W, SIZE, clamp, fbm, hash2, indexOf, inside, pickEntities, valueNoise } from "./mapUtils.js"; export function buildFeatureContext(seed, terrain) { const { elevation, slope, sea, river, floodplain, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland, arcSpineField, branchRidgeField, depositionalLowland, alluvialFanField, deltaField, portSuitability, prefectureMask, prefectureRegionId, naturalBarrierScore, } = terrain; const geography = terrain.geography || {}; const geoHabitability = geography.habitability || null; const geoAccessibility = geography.accessibility || null; const geoNaturalCentrality = geography.naturalCentrality || geography.centrality || null; const geoLowlandCapacity = geography.lowlandCapacity || null; const geoValleyAccess = geography.valleyAccess || null; const geoCoastalAccess = geography.coastalAccess || null; const geoBarrier = geography.geographicBarrier || naturalBarrierScore || null; const geoCorridorSuitability = geography.corridorSuitability || null; function fieldValue(field, i, fallback = 0) { const v = field?.[i]; return Number.isFinite(v) ? v : fallback; } function regionIdAt(x, y) { if (!inside(x, y)) return -1; const i = indexOf(x, y); if (sea[i]) return -1; if (prefectureMask?.[i]) return 0; if (!prefectureRegionId) return 0; const id = prefectureRegionId?.[i]; return id !== undefined && id >= 0 ? id : -1; } function inFocusedPrefecture(p) { return Boolean(p && inside(p.x, p.y) && prefectureMask[indexOf(p.x, p.y)] && !sea[indexOf(p.x, p.y)]); } function localConfluenceScore(x, y) { 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)]; if (rv > 0.18) arms++; if (rv > 0.34) strong++; } return clamp((arms >= 3 ? 0.22 : arms === 2 ? 0.09 : 0) + strong * 0.04); } // --- 1. Human context: one full raster pass ----------------------------- const developable = new Float32Array(SIZE); const ruralSuitability = new Float32Array(SIZE); const townSuitability = new Float32Array(SIZE); const valleySettlement = new Float32Array(SIZE); const coastalSettlement = new Float32Array(SIZE); const confluenceField = new Float32Array(SIZE); const barrierCost = new Float32Array(SIZE); const corridorCost = new Float32Array(SIZE); const settlementCluster = new Float32Array(SIZE); const settlementScore = 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]) { barrierCost[i] = INF; corridorCost[i] = INF; continue; } const naturalBarrier = naturalBarrierScore?.[i] || 0; const depositional = (depositionalLowland?.[i] || 0) + (alluvialFanField?.[i] || 0) * 0.62 + (deltaField?.[i] || 0) * 0.90; const highPenalty = Math.max(0, elevation[i] - 0.56); const lowSlope = clamp(1 - slope[i] * 2.3); const confluence = x > 0 && y > 0 && x < MAP_W - 1 && y < MAP_H - 1 ? localConfluenceScore(x, y) : 0; const openPlainPotential = clamp(plain[i] * 0.68 + agriculture[i] * 0.54 + basinField[i] * 0.30 + lowSlope * 0.22 - river[i] * 0.18 - valleyField[i] * 0.08 - ridgeField[i] * 0.22 - slope[i] * 0.26); const spine = (arcSpineField?.[i] || 0) * 0.58 + (branchRidgeField?.[i] || 0) * 0.38; confluenceField[i] = confluence; const geoH = fieldValue(geoHabitability, i, 0); const geoLow = fieldValue(geoLowlandCapacity, i, 0); const geoValley = fieldValue(geoValleyAccess, i, 0); const geoCoast = fieldValue(geoCoastalAccess, i, 0); const geoB = fieldValue(geoBarrier, i, naturalBarrier); const localDevelopable = clamp( plain[i] * 0.34 + agriculture[i] * 0.24 + basinField[i] * 0.24 + valleyField[i] * 0.24 + coastalLowland[i] * 0.18 + depositional * 0.22 + lowSlope * 0.10 - slope[i] * 0.82 - ridgeField[i] * 0.52 - spine * 0.24 - highPenalty * 1.14 - floodplain[i] * 0.03 ); developable[i] = clamp(localDevelopable * 0.68 + geoH * 0.34 + geoLow * 0.16 - geoB * 0.05); valleySettlement[i] = clamp(( valleyField[i] * 0.52 + river[i] * 0.08 + confluence * 0.38 + depositional * 0.20 + basinField[i] * 0.16 + plain[i] * 0.08 + lowSlope * 0.12 - slope[i] * 0.54 - ridgeField[i] * 0.30 - spine * 0.16 - highPenalty * 0.70 - floodplain[i] * 0.10 ) * 0.74 + geoValley * 0.30 + geoH * 0.08 - geoB * 0.04); coastalSettlement[i] = clamp(( coastalLowland[i] * 0.50 + (portSuitability?.[i] || 0) * 0.30 + (deltaField?.[i] || 0) * 0.20 + plain[i] * 0.10 - slope[i] * 0.52 - ridgeField[i] * 0.24 - spine * 0.12 ) * 0.76 + geoCoast * 0.32 + geoH * 0.06 - geoB * 0.04); const clusterNoise = 0.72 + fbm(x * 0.34 + 13, y * 0.34 - 31, seed + 7001) * 0.46 + valueNoise(x, y, seed + 7002, 8) * 0.16; settlementCluster[i] = clamp((developable[i] * 0.42 + valleySettlement[i] * 0.12 + coastalSettlement[i] * 0.22 + agriculture[i] * 0.48 + plain[i] * 0.32 + openPlainPotential * 0.46) * clusterNoise); ruralSuitability[i] = clamp( agriculture[i] * 0.54 + developable[i] * 0.30 + valleySettlement[i] * 0.18 + coastalSettlement[i] * 0.20 + openPlainPotential * 0.34 + settlementCluster[i] * 0.30 - Math.max(0, elevation[i] - 0.64) * 0.56 ); townSuitability[i] = clamp( developable[i] * 0.38 + agriculture[i] * 0.18 + valleySettlement[i] * 0.16 + coastalSettlement[i] * 0.30 + confluence * 0.20 + basinField[i] * 0.18 + plain[i] * 0.26 + openPlainPotential * 0.44 + settlementCluster[i] * 0.22 - slope[i] * 0.34 - ridgeField[i] * 0.17 - spine * 0.10 ); settlementScore[i] = clamp( ruralSuitability[i] * 0.48 + townSuitability[i] * 0.30 + confluence * 0.08 + fieldValue(geoHabitability, i, developable[i]) * 0.18 + fieldValue(geoNaturalCentrality, i, 0) * 0.12 - fieldValue(geoBarrier, i, 0) * 0.06 ); barrierCost[i] = 1 + slope[i] * 6.4 + ridgeField[i] * 3.2 + spine * 2.2 + highPenalty * 5.8 + river[i] * 0.25 + naturalBarrier * 1.2 - valleyField[i] * 0.55 - plain[i] * 0.30 - coastalLowland[i] * 0.14; corridorCost[i] = Math.max(0.25, barrierCost[i] - developable[i] * 0.42 - valleySettlement[i] * 0.36 - coastalSettlement[i] * 0.12 + hash2(x, y, seed + 7011) * 0.05); } } // --- region statistics --------------------------------------------------- const regionStats = new Map(); function ensureRegion(regionId) { let st = regionStats.get(regionId); if (!st) { st = { id: regionId, area: 0, developableCells: 0, developableSum: 0, valleyCells: 0, coastCells: 0, townCells: 0, plainCells: 0, highCentralityCells: 0, habitabilitySum: 0, accessibilitySum: 0, centralitySum: 0, lowlandCapacitySum: 0, minX: MAP_W, minY: MAP_H, maxX: 0, maxY: 0, }; regionStats.set(regionId, st); } return st; } for (let y = 0; y < MAP_H; y++) { for (let x = 0; x < MAP_W; x++) { const i = indexOf(x, y); if (sea[i]) continue; const regionId = regionIdAt(x, y); if (regionId < 0) continue; const st = ensureRegion(regionId); st.area++; const gHabit = fieldValue(geoHabitability, i, developable[i]); const gAccess = fieldValue(geoAccessibility, i, 0); const gCentral = fieldValue(geoNaturalCentrality, i, townSuitability[i]); const gLow = fieldValue(geoLowlandCapacity, i, plain[i]); st.developableSum += developable[i]; st.habitabilitySum += gHabit; st.accessibilitySum += gAccess; st.centralitySum += gCentral; st.lowlandCapacitySum += gLow; if (developable[i] > 0.16 || gHabit > 0.24) st.developableCells++; if (valleySettlement[i] > 0.24 || fieldValue(geoValleyAccess, i, 0) > 0.25) st.valleyCells++; if (coastalSettlement[i] > 0.25 || fieldValue(geoCoastalAccess, i, 0) > 0.24) st.coastCells++; if (townSuitability[i] > 0.28 || gCentral > 0.31) st.townCells++; if (plain[i] > 0.24 || gLow > 0.26) st.plainCells++; if (gCentral > 0.36 && gHabit > 0.18) st.highCentralityCells++; st.minX = Math.min(st.minX, x); st.minY = Math.min(st.minY, y); st.maxX = Math.max(st.maxX, x); st.maxY = Math.max(st.maxY, y); } } function visibilityFactor(regionId, st) { if (!st || st.area <= 0) return 0; // Treat the focused prefecture and neighboring prefectures with the same // density curve. Only genuinely clipped map-edge slivers are downscaled. return clamp(Math.sqrt(st.area / 1900), 0.32, 1.05); } function pickRegionalPoints(scoreArray, { stride = 1, threshold = 0.25, minDistance = 6, totalMax = 100, seedOffset = 0, quotaForRegion, predicate = () => true, kind = "Point", extraScore = () => 0, }) { const byRegion = new Map(); for (let y = 2; y < MAP_H - 2; y += stride) { for (let x = 2; x < MAP_W - 2; x += stride) { const i = indexOf(x, y); if (sea[i] || !predicate(x, y, i)) continue; const regionId = regionIdAt(x, y); if (regionId < 0) continue; const score = scoreArray[i] + extraScore(x, y, i) + hash2(x, y, seed + seedOffset) * 0.055; if (score < threshold) continue; if (!byRegion.has(regionId)) byRegion.set(regionId, []); byRegion.get(regionId).push({ x, y, score, kind, regionId }); } } const out = []; for (const [regionId, candidates] of [...byRegion.entries()].sort((a, b) => a[0] - b[0])) { const st = regionStats.get(regionId); const quota = quotaForRegion ? quotaForRegion(regionId, st) : 0; if (quota <= 0) continue; out.push(...pickEntities(candidates, { max: quota, minDistance, threshold, seed: seed + seedOffset + regionId * 1009, jitter: 0.04, })); } return out.sort((a, b) => b.score - a.score).slice(0, totalMax); } function pickGlobalPoints(scoreArray, { threshold, max, minDistance, seedOffset = 0, predicate = () => true, stride = 1 }) { const candidates = []; for (let y = 2; y < MAP_H - 2; y += stride) { for (let x = 2; x < MAP_W - 2; x += stride) { const i = indexOf(x, y); if (sea[i] || !predicate(x, y, i)) continue; const score = scoreArray[i] + hash2(x, y, seed + seedOffset) * 0.07; if (score >= threshold) candidates.push({ x, y, score, regionId: regionIdAt(x, y) }); } } return pickEntities(candidates, { max, minDistance, threshold, seed: seed + seedOffset }); } return { geography, geoHabitability, geoAccessibility, geoNaturalCentrality, geoLowlandCapacity, geoValleyAccess, geoCoastalAccess, geoBarrier, geoCorridorSuitability, fieldValue, regionIdAt, inFocusedPrefecture, developable, ruralSuitability, townSuitability, valleySettlement, coastalSettlement, confluenceField, barrierCost, corridorCost, settlementCluster, settlementScore, regionStats, visibilityFactor, pickRegionalPoints, pickGlobalPoints, }; }