327 lines
12 KiB
JavaScript
327 lines
12 KiB
JavaScript
|
|
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,
|
||
|
|
};
|
||
|
|
}
|