map/mapFeatureContext.js
2026-05-28 02:47:33 +09:00

327 lines
No EOL
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,
};
}