316 lines
No EOL
12 KiB
JavaScript
316 lines
No EOL
12 KiB
JavaScript
import { INF, MAP_H, MAP_W, SIZE, clamp, fbm, finiteFieldValue, hash2, indexOf, inside, pickEntities, valueNoise } from "./mapUtils.js";
|
|
import { localConfluenceScore } from "./mapGeography.js";
|
|
|
|
export function buildFeatureContext(seed, terrain) {
|
|
const worldOriginX = Math.floor(Number(terrain?.generationContext?.originX || terrain?.originX || 0));
|
|
const worldOriginY = Math.floor(Number(terrain?.generationContext?.originY || terrain?.originY || 0));
|
|
const worldX = (x) => worldOriginX + x;
|
|
const worldY = (y) => worldOriginY + y;
|
|
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 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)]);
|
|
}
|
|
|
|
// --- 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, river) : 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 = finiteFieldValue(geoHabitability, i, 0);
|
|
const geoLow = finiteFieldValue(geoLowlandCapacity, i, 0);
|
|
const geoValley = finiteFieldValue(geoValleyAccess, i, 0);
|
|
const geoCoast = finiteFieldValue(geoCoastalAccess, i, 0);
|
|
const geoB = finiteFieldValue(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(worldX(x) * 0.34 + 13, worldY(y) * 0.34 - 31, seed + 7001) * 0.46 + valueNoise(worldX(x), worldY(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 +
|
|
finiteFieldValue(geoHabitability, i, developable[i]) * 0.18 +
|
|
finiteFieldValue(geoNaturalCentrality, i, 0) * 0.12 -
|
|
finiteFieldValue(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(worldX(x), worldY(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 = finiteFieldValue(geoHabitability, i, developable[i]);
|
|
const gAccess = finiteFieldValue(geoAccessibility, i, 0);
|
|
const gCentral = finiteFieldValue(geoNaturalCentrality, i, townSuitability[i]);
|
|
const gLow = finiteFieldValue(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 || finiteFieldValue(geoValleyAccess, i, 0) > 0.25) st.valleyCells++;
|
|
if (coastalSettlement[i] > 0.25 || finiteFieldValue(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(worldX(x), worldY(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,
|
|
originX: worldOriginX,
|
|
originY: worldOriginY,
|
|
}));
|
|
}
|
|
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(worldX(x), worldY(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, originX: worldOriginX, originY: worldOriginY });
|
|
}
|
|
|
|
|
|
return {
|
|
geography,
|
|
geoHabitability,
|
|
geoAccessibility,
|
|
geoNaturalCentrality,
|
|
geoLowlandCapacity,
|
|
geoValleyAccess,
|
|
geoCoastalAccess,
|
|
geoBarrier,
|
|
geoCorridorSuitability,
|
|
fieldValue: finiteFieldValue,
|
|
regionIdAt,
|
|
inFocusedPrefecture,
|
|
developable,
|
|
ruralSuitability,
|
|
townSuitability,
|
|
valleySettlement,
|
|
coastalSettlement,
|
|
confluenceField,
|
|
barrierCost,
|
|
corridorCost,
|
|
settlementCluster,
|
|
settlementScore,
|
|
regionStats,
|
|
visibilityFactor,
|
|
pickRegionalPoints,
|
|
pickGlobalPoints,
|
|
};
|
|
} |