splittintting

This commit is contained in:
33333-33333 2026-05-28 02:47:33 +09:00
commit d2e7a80e72
11 changed files with 1341 additions and 1349 deletions

327
mapFeatureContext.js Normal file
View file

@ -0,0 +1,327 @@
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,
};
}

214
mapFeatureLanduse.js Normal file
View file

@ -0,0 +1,214 @@
import { MAP_H, MAP_W, SIZE, clamp, hash2, indexOf, inside } from "./mapUtils.js";
import { LANDUSE } from "./landuseCodes.js";
export function buildFeatureLanduse(ctx) {
const {
seed,
elevation, slope, sea, river, floodplain, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland,
developable, ruralSuitability, valleySettlement, coastalSettlement,
modernCities, logisticsParks,
roadLanduseInfluence, roadInfluence, roadDensityInfluence, railInfluence2, stationInfluence, stationDensityInfluence, villageInfluence,
cityInfluence, coreInfluence, oldTownInfluence, townInfluence, industrialInfluence, logisticsInfluence,
} = ctx;
const populationDensity = new Float32Array(SIZE);
const landuse = new Uint8Array(SIZE);
// Re-run land-use classification after landuse allocation. The loop above is
// intentionally inside a helper to keep all thresholds in one place.
function classifyLanduse() {
landuse.fill(LANDUSE.RURAL);
let maxDensity = 0;
const baseNoiseSeed = seed + 15000;
const urbanCapacity = new Float32Array(SIZE);
const ruralDensityFloor = 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]) continue;
const transport = Math.max(roadLanduseInfluence[i] * 0.95, railInfluence2[i] * 0.95, stationInfluence[i] * 0.82);
const densityTransport = Math.max(roadDensityInfluence[i] * 0.95, stationDensityInfluence[i] * 1.05, railInfluence2[i] * 0.85);
const urban = cityInfluence[i] * 0.76 + stationInfluence[i] * 0.30 + roadInfluence[i] * 0.14 + railInfluence2[i] * 0.10;
const core = coreInfluence[i];
const oldTown = oldTownInfluence[i] * 0.70 + townInfluence[i] * 0.38;
const rural = villageInfluence[i] * 0.24 + ruralSuitability[i] * 0.30;
const riverUrban = clamp(river[i] * 0.12 + valleyField[i] * 0.10 + plain[i] * 0.08 + basinField[i] * 0.08 - floodplain[i] * 0.10);
urbanCapacity[i] = clamp(
developable[i] * 0.66 +
plain[i] * 0.16 +
basinField[i] * 0.16 +
valleyField[i] * 0.16 +
coastalLowland[i] * 0.12 +
roadInfluence[i] * 0.14 + roadDensityInfluence[i] * 0.16 + transport * 0.08 +
riverUrban * 0.14 -
slope[i] * 0.18 -
ridgeField[i] * 0.12 -
floodplain[i] * 0.08
);
const highPenaltyDensity = Math.max(0, elevation[i] - 0.58);
const agrarianDensity = clamp(
agriculture[i] * 0.045 +
ruralSuitability[i] * 0.035 +
developable[i] * 0.028 +
plain[i] * 0.018 +
basinField[i] * 0.014 +
valleySettlement[i] * 0.014 +
coastalSettlement[i] * 0.012 +
villageInfluence[i] * 0.040 +
townInfluence[i] * 0.022 +
roadDensityInfluence[i] * 0.038 +
stationDensityInfluence[i] * 0.020 +
railInfluence2[i] * 0.012 -
slope[i] * 0.030 -
ridgeField[i] * 0.020 -
highPenaltyDensity * 0.058
);
const remoteWilderness = elevation[i] > 0.60 && slope[i] > 0.34 && ridgeField[i] > 0.38 && densityTransport < 0.035 && villageInfluence[i] < 0.025 && townInfluence[i] < 0.025 && cityInfluence[i] < 0.025;
ruralDensityFloor[i] = remoteWilderness ? 0 : clamp(agrarianDensity, 0, elevation[i] > 0.62 || slope[i] > 0.42 ? 0.052 : 0.105);
populationDensity[i] = clamp(
urban * 0.66 +
core * 0.46 +
oldTown * 0.28 +
townInfluence[i] * 0.16 +
villageInfluence[i] * 0.14 +
roadDensityInfluence[i] * 0.42 +
stationDensityInfluence[i] * 0.34 +
railInfluence2[i] * 0.12 +
transport * 0.05 +
agrarianDensity * 0.34
);
maxDensity = Math.max(maxDensity, populationDensity[i]);
if (elevation[i] > 0.67 || (slope[i] > 0.56 && ridgeField[i] > 0.30) || ridgeField[i] > 0.70) {
landuse[i] = LANDUSE.FOREST;
continue;
}
if (industrialInfluence[i] > 0.22 && urbanCapacity[i] > 0.10) {
landuse[i] = LANDUSE.INDUSTRIAL;
continue;
}
if (logisticsInfluence[i] > 0.24 && urbanCapacity[i] > 0.08 && (roadInfluence[i] > 0.08 || railInfluence2[i] > 0.06)) {
landuse[i] = LANDUSE.LOGISTICS;
continue;
}
if (core > 0.38 && urbanCapacity[i] > 0.10) {
landuse[i] = LANDUSE.CBD;
continue;
}
if (oldTown > 0.18 && urbanCapacity[i] > 0.09) {
landuse[i] = LANDUSE.OLD_URBAN;
continue;
}
const suburbanity = urban * 0.88 + transport * 0.23 + stationInfluence[i] * 0.12 + townInfluence[i] * 0.08 + riverUrban * 0.08;
const edgeTaper = clamp(cityInfluence[i] * 0.52 + stationInfluence[i] * 0.16 + roadLanduseInfluence[i] * 0.10 + 0.28);
const sprawlBias = clamp(0.58 + hash2(x, y, baseNoiseSeed) * 0.42);
const sprawlScore = suburbanity * sprawlBias * edgeTaper - core * 0.12;
if (sprawlScore > 0.24 && urbanCapacity[i] > 0.10) {
landuse[i] = LANDUSE.SUBURB;
} else if (roadLanduseInfluence[i] > 0.30 && urbanCapacity[i] > 0.10 && (townInfluence[i] > 0.05 || cityInfluence[i] > 0.11)) {
landuse[i] = LANDUSE.SUBURB;
} else if (agriculture[i] > 0.16 || rural > 0.18 || (developable[i] > 0.13 && plain[i] > 0.13) || (basinField[i] > 0.18 && slope[i] < 0.34) || (coastalLowland[i] > 0.16 && slope[i] < 0.32)) {
landuse[i] = LANDUSE.FARMLAND;
} else {
const usablePlain = slope[i] < 0.30 && (plain[i] > 0.18 || developable[i] > 0.20 || basinField[i] > 0.20 || coastalLowland[i] > 0.18);
landuse[i] = elevation[i] > 0.58 || slope[i] > 0.38 ? LANDUSE.FOREST : usablePlain ? LANDUSE.FARMLAND : LANDUSE.RURAL;
}
}
}
const baseLanduse = landuse.slice();
const isBuilt = (lu) => lu >= LANDUSE.OLD_URBAN && lu <= LANDUSE.ROADSIDE;
for (let y = 1; y < MAP_H - 1; y++) {
for (let x = 1; x < MAP_W - 1; x++) {
const i = indexOf(x, y);
if (sea[i] || baseLanduse[i] === LANDUSE.FOREST) continue;
const transport = Math.max(roadLanduseInfluence[i] * 0.95, railInfluence2[i] * 0.95, stationInfluence[i] * 0.82);
const densityTransport = Math.max(roadDensityInfluence[i] * 0.95, stationDensityInfluence[i] * 1.05, railInfluence2[i] * 0.85);
let urbanNeighbors = 0;
let cbdNeighbors = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
const lu = baseLanduse[i + dy * MAP_W + dx];
if (isBuilt(lu)) urbanNeighbors++;
if (lu === LANDUSE.CBD) cbdNeighbors++;
}
}
if (baseLanduse[i] === LANDUSE.OLD_URBAN && coreInfluence[i] > 0.31 && cbdNeighbors >= 3) {
landuse[i] = LANDUSE.CBD;
continue;
}
if ((baseLanduse[i] === LANDUSE.FARMLAND || baseLanduse[i] === LANDUSE.RURAL) && urbanCapacity[i] > 0.10) {
const fringeChance = urbanNeighbors * 0.055 + cityInfluence[i] * 0.13 + transport * 0.12 + stationInfluence[i] * 0.08;
const noise = 0.23 + hash2(x, y, seed + 15050) * 0.24;
if (fringeChance > 0.34 + noise) {
landuse[i] = LANDUSE.SUBURB;
}
}
if ((river[i] > 0.10 || railInfluence2[i] > 0.16 || roadLanduseInfluence[i] > 0.22) && urbanNeighbors >= 3 && landuse[i] <= LANDUSE.FARMLAND && urbanCapacity[i] > 0.09) {
landuse[i] = cbdNeighbors >= 1 || coreInfluence[i] > 0.15 ? LANDUSE.OLD_URBAN : LANDUSE.SUBURB;
}
}
}
for (const park of logisticsParks) {
const r = 3;
for (let dy = -r; dy <= r; dy++) {
for (let dx = -r; dx <= r; dx++) {
const x = park.x + dx;
const y = park.y + dy;
if (!inside(x, y)) continue;
const i = indexOf(x, y);
if (sea[i] || landuse[i] === LANDUSE.CBD || landuse[i] === LANDUSE.FOREST) continue;
if (Math.hypot(dx, dy) <= r && (agriculture[i] > 0.18 || plain[i] > 0.15 || roadInfluence[i] > 0.06 || railInfluence2[i] > 0.05)) {
landuse[i] = LANDUSE.LOGISTICS;
}
}
}
}
if (maxDensity > 0) {
for (let i = 0; i < SIZE; i++) {
if (sea[i]) continue;
const lu = landuse[i];
let floor = ruralDensityFloor[i];
if (lu === LANDUSE.FARMLAND) {
floor = Math.max(floor, clamp(0.024 + agriculture[i] * 0.044 + ruralSuitability[i] * 0.024 + roadDensityInfluence[i] * 0.030 + stationDensityInfluence[i] * 0.026 + villageInfluence[i] * 0.018, 0, 0.110));
} else if (lu === LANDUSE.LOGISTICS) {
floor = Math.max(floor, clamp(0.018 + roadDensityInfluence[i] * 0.026 + railInfluence2[i] * 0.014 + logisticsInfluence[i] * 0.012, 0, 0.060));
} else if (lu === LANDUSE.RURAL) {
floor = Math.max(floor, clamp(0.012 + ruralSuitability[i] * 0.020 + developable[i] * 0.014 + roadDensityInfluence[i] * 0.024 + stationDensityInfluence[i] * 0.020, 0, 0.070));
} else if (lu === LANDUSE.FOREST) {
floor = Math.min(floor, (roadDensityInfluence[i] > 0.04 || villageInfluence[i] > 0.03) ? 0.026 : 0);
}
const normalized = populationDensity[i] / maxDensity;
populationDensity[i] = clamp(Math.max(normalized, floor));
if (lu === LANDUSE.FOREST && floor === 0 && populationDensity[i] < 0.012) populationDensity[i] = 0;
}
}
}
classifyLanduse();
for (const city of modernCities) {
let urbanFootprintCells = 0;
let coreFootprintCells = 0;
const r = Math.ceil((city.urbanRadius || 8) * 1.3);
for (let dy = -r; dy <= r; dy++) {
for (let dx = -r; dx <= r; dx++) {
const x = city.x + dx;
const y = city.y + dy;
if (!inside(x, y)) continue;
const i = indexOf(x, y);
if (sea[i]) continue;
if (Math.hypot(dx, dy) > r) continue;
if (landuse[i] >= LANDUSE.OLD_URBAN && landuse[i] <= LANDUSE.ROADSIDE) urbanFootprintCells++;
if (landuse[i] === LANDUSE.CBD) coreFootprintCells++;
}
}
city.urbanFootprintCells = urbanFootprintCells;
city.coreFootprintCells = coreFootprintCells;
}
return { landuse, populationDensity };
}

47
mapFeatureSettlements.js Normal file
View file

@ -0,0 +1,47 @@
import { MAP_H, MAP_W, SIZE, clamp, indexOf } from "./mapUtils.js";
import { influenceFromPoints } from "./mapGeneratorHelpers.js";
export function buildSettlementDemandFields(ctx) {
const {
sea, agriculture, plain, basinField, coastalLowland, slope, ridgeField,
modernCities, markets, commercialPorts, villages,
} = ctx;
const preliminaryUrbanInfluence = influenceFromPoints(modernCities, 18, (c) => clamp((c.population || 60000) / 260000, 0.55, 2.0));
const preliminaryTownInfluence = influenceFromPoints([...markets, ...commercialPorts], 10, (p) => p.portClass === "major" ? 1.35 : clamp((p.population || 12000) / 36000, 0.42, 1.1));
const preliminaryVillageInfluence = influenceFromPoints(villages, 7, (v) => clamp((v.population || 1800) / 5200, 0.22, 0.9));
const settlementDemand = new Float32Array(SIZE);
const urbanEdge = new Float32Array(SIZE);
const logisticsPreSuitability = 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]) continue;
const density = clamp(preliminaryUrbanInfluence[i] * 0.62 + preliminaryTownInfluence[i] * 0.34 + preliminaryVillageInfluence[i] * 0.16);
settlementDemand[i] = density;
urbanEdge[i] = clamp(1 - Math.abs(density - 0.46) / 0.32);
logisticsPreSuitability[i] = clamp(
agriculture[i] * 0.30 +
plain[i] * 0.24 +
basinField[i] * 0.14 +
coastalLowland[i] * 0.12 +
preliminaryTownInfluence[i] * 0.18 +
urbanEdge[i] * 0.34 -
preliminaryUrbanInfluence[i] * 0.20 -
slope[i] * 0.50 -
ridgeField[i] * 0.32
);
}
}
return {
preliminaryUrbanInfluence,
preliminaryTownInfluence,
preliminaryVillageInfluence,
settlementDemand,
urbanEdge,
logisticsPreSuitability,
};
}

191
mapFeatureTransportTools.js Normal file
View file

@ -0,0 +1,191 @@
import { INF, MAP_H, MAP_W, SIZE, clamp, hash2, indexOf, inside } from "./mapUtils.js";
export function buildFeatureTransportCostFields(ctx) {
const {
seed,
sea, elevation, slope, river, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland,
portSuitability, passSuitability, crossingSuitability, naturalBarrierScore,
settlementDemand, urbanEdge, logisticsPreSuitability,
preliminaryTownInfluence, preliminaryVillageInfluence,
valleySettlement, coastalSettlement, developable,
} = ctx;
const expressway = new Float32Array(SIZE);
const rail = new Float32Array(SIZE);
const national = new Float32Array(SIZE);
const local = new Float32Array(SIZE);
const expresswayPotential = new Float32Array(SIZE);
const railPotential = new Float32Array(SIZE);
const nationalPotential = new Float32Array(SIZE);
const localPotential = new Float32Array(SIZE);
function seaAdjacency(x, y, radius = 1) {
let sum = 0;
let total = 0;
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
if (!dx && !dy) continue;
const nx = x + dx;
const ny = y + dy;
if (!inside(nx, ny)) continue;
total++;
if (sea[indexOf(nx, ny)]) sum += 1;
}
}
return total > 0 ? sum / total : 0;
}
function highAltitudeTransportClosed(i) {
// Above this contour the generator should treat mountains as no-road
// terrain. A strong mapped pass is the exception, so genuine saddle
// crossings can still exist without roads drilling through entire ranges.
return elevation[i] >= 0.70;
}
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
const i = indexOf(x, y);
if (sea[i] || highAltitudeTransportClosed(i)) {
expressway[i] = rail[i] = national[i] = local[i] = INF;
expresswayPotential[i] = railPotential[i] = nationalPotential[i] = localPotential[i] = 0;
continue;
}
const density = settlementDemand[i];
const mediumDensity = clamp(1 - Math.abs(density - 0.42) / 0.30);
const highDensity = clamp((density - 0.32) / 0.50);
const lowland = clamp(plain[i] * 0.48 + basinField[i] * 0.28 + valleyField[i] * 0.24 + coastalLowland[i] * 0.26 + agriculture[i] * 0.16);
const pass = passSuitability?.[i] || 0;
const crossing = crossingSuitability?.[i] || 0;
const waterCrossingPenalty = river[i] > 0.18 ? (1 - crossing) * (0.72 + river[i] * 1.35) : 0;
const seaNear = seaAdjacency(x, y, 1);
const seaBroad = seaAdjacency(x, y, 3);
const seaWide = seaAdjacency(x, y, 5);
// Roads should use coastal lowlands when there is a settlement/port reason,
// but should not casually trace beaches or hop over small bays.
const coastalTraversePenalty = clamp(seaBroad * 1.72 + seaWide * 0.82 - coastalLowland[i] * 0.48 - (portSuitability?.[i] || 0) * 0.30);
const highMountain = clamp((elevation[i] - 0.52) * 3.6 + slope[i] * 0.95 + ridgeField[i] * 1.05 - pass * 0.55 - valleyField[i] * 0.12);
const extremeMountain = clamp((elevation[i] - 0.64) * 4.8 + slope[i] * 1.55 + ridgeField[i] * 1.45 - pass * 0.80);
const denseCorePenalty = clamp((density - 0.66) / 0.28);
const openPlainParallelPenalty = clamp(plain[i] * 0.48 + agriculture[i] * 0.26 - density * 0.20 - valleyField[i] * 0.20 - coastalLowland[i] * 0.12);
const boundaryRidgePenalty = Math.pow(naturalBarrierScore?.[i] || 0, 2);
if (extremeMountain > 0.92 && pass < 0.34) {
expressway[i] = rail[i] = INF;
national[i] = elevation[i] >= 0.68 ? INF : 2.9 + extremeMountain * 2.8 + waterCrossingPenalty;
local[i] = elevation[i] >= 0.69 ? INF : 2.2 + extremeMountain * 2.3 + waterCrossingPenalty * 0.55;
expresswayPotential[i] = 0;
railPotential[i] = 0;
nationalPotential[i] = clamp(lowland * 0.18 + pass * 0.24 - extremeMountain * 0.55);
localPotential[i] = clamp(valleySettlement[i] * 0.14 + pass * 0.18 - extremeMountain * 0.28);
continue;
}
expresswayPotential[i] = clamp(
mediumDensity * 0.62 +
urbanEdge[i] * 0.38 +
logisticsPreSuitability[i] * 0.54 +
lowland * 0.36 +
agriculture[i] * 0.16 -
denseCorePenalty * 0.54 -
slope[i] * 0.82 -
highMountain * 0.92 -
coastalTraversePenalty * 0.32 -
river[i] * 0.14
);
railPotential[i] = clamp(
highDensity * 0.80 +
preliminaryTownInfluence[i] * 0.22 +
lowland * 0.46 +
valleyField[i] * 0.22 +
coastalLowland[i] * 0.22 -
slope[i] * 1.28 -
highMountain * 1.10 -
coastalTraversePenalty * 0.26 -
ridgeField[i] * 0.34
);
nationalPotential[i] = clamp(
density * 0.46 +
preliminaryTownInfluence[i] * 0.32 +
preliminaryVillageInfluence[i] * 0.20 +
agriculture[i] * 0.24 +
valleyField[i] * 0.28 +
coastalLowland[i] * 0.26 +
pass * 0.18 +
crossing * 0.18 -
slope[i] * 0.48 -
highMountain * 0.24 -
coastalTraversePenalty * 0.16 -
ridgeField[i] * 0.18
);
localPotential[i] = clamp(
preliminaryVillageInfluence[i] * 0.52 +
agriculture[i] * 0.38 +
coastalSettlement[i] * 0.30 +
valleySettlement[i] * 0.30 +
developable[i] * 0.18 -
slope[i] * 0.34 -
coastalTraversePenalty * 0.08 -
ridgeField[i] * 0.10
);
expressway[i] = Math.max(0.18,
1.62 - expresswayPotential[i] * 0.96 +
denseCorePenalty * 1.30 +
slope[i] * 5.4 +
highMountain * 5.8 +
extremeMountain * 4.2 +
boundaryRidgePenalty * 4.2 +
waterCrossingPenalty * 2.1 +
coastalTraversePenalty * 2.65 +
seaNear * 2.35 +
seaWide * 1.10 +
openPlainParallelPenalty * 0.12 +
hash2(x, y, seed + 13301) * 0.04
);
rail[i] = Math.max(0.16,
1.48 - railPotential[i] * 1.02 +
slope[i] * 7.2 +
highMountain * 7.0 +
extremeMountain * 4.8 +
boundaryRidgePenalty * 2.4 +
waterCrossingPenalty * 1.7 +
coastalTraversePenalty * 1.75 +
seaNear * 1.50 +
seaWide * 0.70 +
hash2(x, y, seed + 13302) * 0.03
);
national[i] = Math.max(0.16,
1.28 - nationalPotential[i] * 0.84 +
slope[i] * 2.8 +
ridgeField[i] * 1.18 +
Math.max(0, elevation[i] - 0.62) * 3.0 +
highMountain * 2.9 +
boundaryRidgePenalty * 1.8 +
waterCrossingPenalty * 1.25 -
valleyField[i] * 0.18 -
coastalLowland[i] * 0.08 +
coastalTraversePenalty * 1.55 +
seaNear * 0.84 +
seaWide * 0.48 -
pass * 0.42 +
hash2(x, y, seed + 13303) * 0.05
);
local[i] = Math.max(0.14,
1.12 - localPotential[i] * 0.86 +
slope[i] * 1.72 +
ridgeField[i] * 0.82 +
Math.max(0, elevation[i] - 0.68) * 1.9 +
highMountain * 1.24 +
boundaryRidgePenalty * 0.72 +
waterCrossingPenalty * 0.65 -
valleyField[i] * 0.22 -
coastalLowland[i] * 0.10 +
coastalTraversePenalty * 0.82 +
seaNear * 0.48 +
seaWide * 0.26 +
hash2(x, y, seed + 13304) * 0.07
);
}
}
return { expressway, rail, national, local, expresswayPotential, railPotential, nationalPotential, localPotential };
}

View file

@ -1,8 +1,11 @@
import { INF, MAP_H, MAP_W, SIZE, MinHeap, clamp, fbm, hash2, indexOf, inside, pickEntities, rand, valueNoise, xyOf } from "./mapUtils.js";
import { INF, MAP_H, MAP_W, SIZE, MinHeap, clamp, hash2, indexOf, inside, pickEntities, rand, valueNoise, xyOf } from "./mapUtils.js";
import { distanceToNearest, influenceFromPaths, influenceFromPoints, samplePath } from "./mapGeneratorHelpers.js";
import { LANDUSE } from "./landuseCodes.js";
import { buildDensityFlowRoadTransportSystem, createPathInfluenceCache, packDebugField, pathAverageField, pathLengthCells, routeQualityAcceptable } from "./mapTransport.js";
import { buildUnifiedRailODNetwork } from "./mapTransportOD.js";
import { buildFeatureContext } from "./mapFeatureContext.js";
import { buildFeatureLanduse } from "./mapFeatureLanduse.js";
import { buildSettlementDemandFields } from "./mapFeatureSettlements.js";
import { buildFeatureTransportCostFields } from "./mapFeatureTransportTools.js";
// Lightweight Human Geography V2
// --------------------------------
@ -16,7 +19,6 @@ import { buildUnifiedRailODNetwork } from "./mapTransportOD.js";
export function generateMapFeatures(seed, terrain) {
const {
elevation,
moisture,
slope,
sea,
river,
@ -28,290 +30,41 @@ export function generateMapFeatures(seed, terrain) {
basinField,
coastalLowland,
flowAccum,
arcSpineField,
branchRidgeField,
depositionalLowland,
alluvialFanField,
deltaField,
portSuitability,
crossingSuitability,
passSuitability,
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 });
}
const featureContext = buildFeatureContext(seed, terrain);
const {
geoHabitability,
geoAccessibility,
geoNaturalCentrality,
geoLowlandCapacity,
geoValleyAccess,
geoCoastalAccess,
geoBarrier,
fieldValue,
regionIdAt,
inFocusedPrefecture,
developable,
ruralSuitability,
townSuitability,
valleySettlement,
coastalSettlement,
confluenceField,
barrierCost,
corridorCost,
settlementCluster,
settlementScore,
regionStats,
visibilityFactor,
pickRegionalPoints,
pickGlobalPoints,
} = featureContext;
// --- 2. Sparse points ----------------------------------------------------
let ports = pickGlobalPoints(portSuitability || coastalSettlement, {
threshold: 0.30 + rand(seed, 1001) * 0.08,
@ -797,216 +550,25 @@ export function generateMapFeatures(seed, terrain) {
}
// --- 4. Field-derived transport corridors -------------------------------
const preliminaryUrbanInfluence = influenceFromPoints(modernCities, 18, (c) => clamp((c.population || 60000) / 260000, 0.55, 2.0));
const preliminaryTownInfluence = influenceFromPoints([...markets, ...commercialPorts], 10, (p) => p.portClass === "major" ? 1.35 : clamp((p.population || 12000) / 36000, 0.42, 1.1));
const preliminaryVillageInfluence = influenceFromPoints(villages, 7, (v) => clamp((v.population || 1800) / 5200, 0.22, 0.9));
const settlementDemand = new Float32Array(SIZE);
const urbanEdge = new Float32Array(SIZE);
const logisticsPreSuitability = 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]) continue;
const density = clamp(preliminaryUrbanInfluence[i] * 0.62 + preliminaryTownInfluence[i] * 0.34 + preliminaryVillageInfluence[i] * 0.16);
settlementDemand[i] = density;
urbanEdge[i] = clamp(1 - Math.abs(density - 0.46) / 0.32);
logisticsPreSuitability[i] = clamp(
agriculture[i] * 0.30 +
plain[i] * 0.24 +
basinField[i] * 0.14 +
coastalLowland[i] * 0.12 +
preliminaryTownInfluence[i] * 0.18 +
urbanEdge[i] * 0.34 -
preliminaryUrbanInfluence[i] * 0.20 -
slope[i] * 0.50 -
ridgeField[i] * 0.32
);
}
}
function buildTransportCostFields() {
const expressway = new Float32Array(SIZE);
const rail = new Float32Array(SIZE);
const national = new Float32Array(SIZE);
const local = new Float32Array(SIZE);
const expresswayPotential = new Float32Array(SIZE);
const railPotential = new Float32Array(SIZE);
const nationalPotential = new Float32Array(SIZE);
const localPotential = new Float32Array(SIZE);
function seaAdjacency(x, y, radius = 1) {
let sum = 0;
let total = 0;
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
if (!dx && !dy) continue;
const nx = x + dx;
const ny = y + dy;
if (!inside(nx, ny)) continue;
total++;
if (sea[indexOf(nx, ny)]) sum += 1;
}
}
return total > 0 ? sum / total : 0;
}
function highAltitudeTransportClosed(i) {
// Above this contour the generator should treat mountains as no-road
// terrain. A strong mapped pass is the exception, so genuine saddle
// crossings can still exist without roads drilling through entire ranges.
return elevation[i] >= 0.70;
}
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
const i = indexOf(x, y);
if (sea[i] || highAltitudeTransportClosed(i)) {
expressway[i] = rail[i] = national[i] = local[i] = INF;
expresswayPotential[i] = railPotential[i] = nationalPotential[i] = localPotential[i] = 0;
continue;
}
const density = settlementDemand[i];
const mediumDensity = clamp(1 - Math.abs(density - 0.42) / 0.30);
const highDensity = clamp((density - 0.32) / 0.50);
const lowland = clamp(plain[i] * 0.48 + basinField[i] * 0.28 + valleyField[i] * 0.24 + coastalLowland[i] * 0.26 + agriculture[i] * 0.16);
const pass = passSuitability?.[i] || 0;
const crossing = crossingSuitability?.[i] || 0;
const waterCrossingPenalty = river[i] > 0.18 ? (1 - crossing) * (0.72 + river[i] * 1.35) : 0;
const seaNear = seaAdjacency(x, y, 1);
const seaBroad = seaAdjacency(x, y, 3);
const seaWide = seaAdjacency(x, y, 5);
// Roads should use coastal lowlands when there is a settlement/port reason,
// but should not casually trace beaches or hop over small bays.
const coastalTraversePenalty = clamp(seaBroad * 1.72 + seaWide * 0.82 - coastalLowland[i] * 0.48 - (portSuitability?.[i] || 0) * 0.30);
const highMountain = clamp((elevation[i] - 0.52) * 3.6 + slope[i] * 0.95 + ridgeField[i] * 1.05 - pass * 0.55 - valleyField[i] * 0.12);
const extremeMountain = clamp((elevation[i] - 0.64) * 4.8 + slope[i] * 1.55 + ridgeField[i] * 1.45 - pass * 0.80);
const denseCorePenalty = clamp((density - 0.66) / 0.28);
const openPlainParallelPenalty = clamp(plain[i] * 0.48 + agriculture[i] * 0.26 - density * 0.20 - valleyField[i] * 0.20 - coastalLowland[i] * 0.12);
const boundaryRidgePenalty = Math.pow(naturalBarrierScore?.[i] || 0, 2);
if (extremeMountain > 0.92 && pass < 0.34) {
expressway[i] = rail[i] = INF;
national[i] = elevation[i] >= 0.68 ? INF : 2.9 + extremeMountain * 2.8 + waterCrossingPenalty;
local[i] = elevation[i] >= 0.69 ? INF : 2.2 + extremeMountain * 2.3 + waterCrossingPenalty * 0.55;
expresswayPotential[i] = 0;
railPotential[i] = 0;
nationalPotential[i] = clamp(lowland * 0.18 + pass * 0.24 - extremeMountain * 0.55);
localPotential[i] = clamp(valleySettlement[i] * 0.14 + pass * 0.18 - extremeMountain * 0.28);
continue;
}
expresswayPotential[i] = clamp(
mediumDensity * 0.62 +
urbanEdge[i] * 0.38 +
logisticsPreSuitability[i] * 0.54 +
lowland * 0.36 +
agriculture[i] * 0.16 -
denseCorePenalty * 0.54 -
slope[i] * 0.82 -
highMountain * 0.92 -
coastalTraversePenalty * 0.32 -
river[i] * 0.14
);
railPotential[i] = clamp(
highDensity * 0.80 +
preliminaryTownInfluence[i] * 0.22 +
lowland * 0.46 +
valleyField[i] * 0.22 +
coastalLowland[i] * 0.22 -
slope[i] * 1.28 -
highMountain * 1.10 -
coastalTraversePenalty * 0.26 -
ridgeField[i] * 0.34
);
nationalPotential[i] = clamp(
density * 0.46 +
preliminaryTownInfluence[i] * 0.32 +
preliminaryVillageInfluence[i] * 0.20 +
agriculture[i] * 0.24 +
valleyField[i] * 0.28 +
coastalLowland[i] * 0.26 +
pass * 0.18 +
crossing * 0.18 -
slope[i] * 0.48 -
highMountain * 0.24 -
coastalTraversePenalty * 0.16 -
ridgeField[i] * 0.18
);
localPotential[i] = clamp(
preliminaryVillageInfluence[i] * 0.52 +
agriculture[i] * 0.38 +
coastalSettlement[i] * 0.30 +
valleySettlement[i] * 0.30 +
developable[i] * 0.18 -
slope[i] * 0.34 -
coastalTraversePenalty * 0.08 -
ridgeField[i] * 0.10
);
expressway[i] = Math.max(0.18,
1.62 - expresswayPotential[i] * 0.96 +
denseCorePenalty * 1.30 +
slope[i] * 5.4 +
highMountain * 5.8 +
extremeMountain * 4.2 +
boundaryRidgePenalty * 4.2 +
waterCrossingPenalty * 2.1 +
coastalTraversePenalty * 2.65 +
seaNear * 2.35 +
seaWide * 1.10 +
openPlainParallelPenalty * 0.12 +
hash2(x, y, seed + 13301) * 0.04
);
rail[i] = Math.max(0.16,
1.48 - railPotential[i] * 1.02 +
slope[i] * 7.2 +
highMountain * 7.0 +
extremeMountain * 4.8 +
boundaryRidgePenalty * 2.4 +
waterCrossingPenalty * 1.7 +
coastalTraversePenalty * 1.75 +
seaNear * 1.50 +
seaWide * 0.70 +
hash2(x, y, seed + 13302) * 0.03
);
national[i] = Math.max(0.16,
1.28 - nationalPotential[i] * 0.84 +
slope[i] * 2.8 +
ridgeField[i] * 1.18 +
Math.max(0, elevation[i] - 0.62) * 3.0 +
highMountain * 2.9 +
boundaryRidgePenalty * 1.8 +
waterCrossingPenalty * 1.25 -
valleyField[i] * 0.18 -
coastalLowland[i] * 0.08 +
coastalTraversePenalty * 1.55 +
seaNear * 0.84 +
seaWide * 0.48 -
pass * 0.42 +
hash2(x, y, seed + 13303) * 0.05
);
local[i] = Math.max(0.14,
1.12 - localPotential[i] * 0.86 +
slope[i] * 1.72 +
ridgeField[i] * 0.82 +
Math.max(0, elevation[i] - 0.68) * 1.9 +
highMountain * 1.24 +
boundaryRidgePenalty * 0.72 +
waterCrossingPenalty * 0.65 -
valleyField[i] * 0.22 -
coastalLowland[i] * 0.10 +
coastalTraversePenalty * 0.82 +
seaNear * 0.48 +
seaWide * 0.26 +
hash2(x, y, seed + 13304) * 0.07
);
}
}
return { expressway, rail, national, local, expresswayPotential, railPotential, nationalPotential, localPotential };
}
const transportFields = buildTransportCostFields();
const {
preliminaryUrbanInfluence,
preliminaryTownInfluence,
preliminaryVillageInfluence,
settlementDemand,
urbanEdge,
logisticsPreSuitability,
} = buildSettlementDemandFields({
sea, agriculture, plain, basinField, coastalLowland, slope, ridgeField,
modernCities, markets, commercialPorts, villages,
});
const transportFields = buildFeatureTransportCostFields({
seed,
sea, elevation, slope, river, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland,
portSuitability, passSuitability, crossingSuitability, naturalBarrierScore,
settlementDemand, urbanEdge, logisticsPreSuitability,
preliminaryTownInfluence, preliminaryVillageInfluence,
valleySettlement, coastalSettlement, developable,
});
const cachedInfluenceFromPaths = createPathInfluenceCache(influenceFromPaths);
const componentCityInfluence = influenceFromPoints([...modernCities, ...markets], 11, (p) => clamp((p.population || 8000) / 50000, 0.18, 8.0));
@ -1333,11 +895,11 @@ export function generateMapFeatures(seed, terrain) {
const water = pathWaterCrossingStats(path);
const tunnel = pathTunnelStats(path);
const bridgeLimit = overrides.bridgeLimit ?? (mode === "expressway" ? 20 : 10);
const tunnelLimit = overrides.tunnelLimit ?? (mode === "expressway" ? 10 : 0);
const tunnelLimit = overrides.tunnelLimit ?? (mode === "expressway" ? 10 : mode === "national" ? 10 : 0);
const maxSeaRun = overrides.maxSeaRun ?? bridgeLimit;
const maxTunnelRun = overrides.maxTunnelRun ?? tunnelLimit;
const maxSeaShare = overrides.maxSeaShare ?? (mode === "expressway" ? 0.22 : mode === "rail" ? 0.030 : mode === "national" ? 0.10 : 0.05);
const maxTunnelShare = overrides.maxTunnelShare ?? (mode === "expressway" ? 0.12 : 0);
const maxTunnelShare = overrides.maxTunnelShare ?? (mode === "expressway" ? 0.12 : mode === "national" ? 0.12 : 0);
if (water.maxSeaRun > maxSeaRun || water.seaShare > maxSeaShare) return false;
if (tunnel.maxTunnelRun > maxTunnelRun || tunnel.tunnelShare > maxTunnelShare) return false;
if (water.seaCells > 0) {
@ -2265,7 +1827,6 @@ const premodernRoads = [];
const cityInfluence = new Float32Array(SIZE);
const coreInfluence = new Float32Array(SIZE);
const oldTownInfluence = influenceFromPoints([...markets, ...castleTowns, ...ports], 7, (p) => p.kind === "Major Port" ? 1.2 : 0.9);
const populationDensity = new Float32Array(SIZE);
function addKernel(grid, p, radius, weight, exponent = 1.7, terrainWeighted = true, combine = "max") {
const r = Math.ceil(radius);
@ -2730,204 +2291,14 @@ const premodernRoads = [];
transportDebugLayers.postConnectivityMajorCityExpresswayGuarantee = ensureMajorCityExpresswayConnections(100000);
transportDebugLayers.postConnectivityExpresswayDedup = dedupeTransportPathSet(expressways, { minLength: 8, sampleStep: 2 });
transportDebugLayers.postConnectivityExpresswayEndpointICs = ensureExpresswayEndpointsHaveICs();
var landuse = new Uint8Array(SIZE);
// Re-run land-use classification after landuse allocation. The loop above is
// intentionally inside a helper to keep all thresholds in one place.
function classifyLanduse() {
landuse.fill(LANDUSE.RURAL);
let maxDensity = 0;
const baseNoiseSeed = seed + 15000;
const urbanCapacity = new Float32Array(SIZE);
const ruralDensityFloor = 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]) continue;
const transport = Math.max(roadLanduseInfluence[i] * 0.95, railInfluence2[i] * 0.95, stationInfluence[i] * 0.82);
const densityTransport = Math.max(roadDensityInfluence[i] * 0.95, stationDensityInfluence[i] * 1.05, railInfluence2[i] * 0.85);
const urban = cityInfluence[i] * 0.76 + stationInfluence[i] * 0.30 + roadInfluence[i] * 0.14 + railInfluence2[i] * 0.10;
const core = coreInfluence[i];
const oldTown = oldTownInfluence[i] * 0.70 + townInfluence[i] * 0.38;
const rural = villageInfluence[i] * 0.24 + ruralSuitability[i] * 0.30;
const riverUrban = clamp(river[i] * 0.12 + valleyField[i] * 0.10 + plain[i] * 0.08 + basinField[i] * 0.08 - floodplain[i] * 0.10);
urbanCapacity[i] = clamp(
developable[i] * 0.66 +
plain[i] * 0.16 +
basinField[i] * 0.16 +
valleyField[i] * 0.16 +
coastalLowland[i] * 0.12 +
roadInfluence[i] * 0.14 + roadDensityInfluence[i] * 0.16 + transport * 0.08 +
riverUrban * 0.14 -
slope[i] * 0.18 -
ridgeField[i] * 0.12 -
floodplain[i] * 0.08
);
const highPenaltyDensity = Math.max(0, elevation[i] - 0.58);
const agrarianDensity = clamp(
agriculture[i] * 0.045 +
ruralSuitability[i] * 0.035 +
developable[i] * 0.028 +
plain[i] * 0.018 +
basinField[i] * 0.014 +
valleySettlement[i] * 0.014 +
coastalSettlement[i] * 0.012 +
villageInfluence[i] * 0.040 +
townInfluence[i] * 0.022 +
roadDensityInfluence[i] * 0.038 +
stationDensityInfluence[i] * 0.020 +
railInfluence2[i] * 0.012 -
slope[i] * 0.030 -
ridgeField[i] * 0.020 -
highPenaltyDensity * 0.058
);
const remoteWilderness = elevation[i] > 0.60 && slope[i] > 0.34 && ridgeField[i] > 0.38 && densityTransport < 0.035 && villageInfluence[i] < 0.025 && townInfluence[i] < 0.025 && cityInfluence[i] < 0.025;
ruralDensityFloor[i] = remoteWilderness ? 0 : clamp(agrarianDensity, 0, elevation[i] > 0.62 || slope[i] > 0.42 ? 0.052 : 0.105);
populationDensity[i] = clamp(
urban * 0.66 +
core * 0.46 +
oldTown * 0.28 +
townInfluence[i] * 0.16 +
villageInfluence[i] * 0.14 +
roadDensityInfluence[i] * 0.42 +
stationDensityInfluence[i] * 0.34 +
railInfluence2[i] * 0.12 +
transport * 0.05 +
agrarianDensity * 0.34
);
maxDensity = Math.max(maxDensity[i]);
if (elevation[i] > 0.67 || (slope[i] > 0.56 && ridgeField[i] > 0.30) || ridgeField[i] > 0.70) {
landuse[i] = LANDUSE.FOREST;
continue;
}
if (industrialInfluence[i] > 0.22 && urbanCapacity[i] > 0.10) {
landuse[i] = LANDUSE.INDUSTRIAL;
continue;
}
if (logisticsInfluence[i] > 0.24 && urbanCapacity[i] > 0.08 && (roadInfluence[i] > 0.08 || railInfluence2[i] > 0.06)) {
landuse[i] = LANDUSE.LOGISTICS;
continue;
}
if (core > 0.38 && urbanCapacity[i] > 0.10) {
landuse[i] = LANDUSE.CBD;
continue;
}
if (oldTown > 0.18 && urbanCapacity[i] > 0.09) {
landuse[i] = LANDUSE.OLD_URBAN;
continue;
}
const suburbanity = urban * 0.88 + transport * 0.23 + stationInfluence[i] * 0.12 + townInfluence[i] * 0.08 + riverUrban * 0.08;
const edgeTaper = clamp(cityInfluence[i] * 0.52 + stationInfluence[i] * 0.16 + roadLanduseInfluence[i] * 0.10 + 0.28);
const sprawlBias = clamp(0.58 + hash2(x, y, baseNoiseSeed) * 0.42);
const sprawlScore = suburbanity * sprawlBias * edgeTaper - core * 0.12;
if (sprawlScore > 0.24 && urbanCapacity[i] > 0.10) {
landuse[i] = LANDUSE.SUBURB;
} else if (roadLanduseInfluence[i] > 0.30 && urbanCapacity[i] > 0.10 && (townInfluence[i] > 0.05 || cityInfluence[i] > 0.11)) {
landuse[i] = LANDUSE.SUBURB;
} else if (agriculture[i] > 0.16 || rural > 0.18 || (developable[i] > 0.13 && plain[i] > 0.13) || (basinField[i] > 0.18 && slope[i] < 0.34) || (coastalLowland[i] > 0.16 && slope[i] < 0.32)) {
landuse[i] = LANDUSE.FARMLAND;
} else {
const usablePlain = slope[i] < 0.30 && (plain[i] > 0.18 || developable[i] > 0.20 || basinField[i] > 0.20 || coastalLowland[i] > 0.18);
landuse[i] = elevation[i] > 0.58 || slope[i] > 0.38 ? LANDUSE.FOREST : usablePlain ? LANDUSE.FARMLAND : LANDUSE.RURAL;
}
}
}
const baseLanduse = landuse.slice();
const isBuilt = (lu) => lu >= LANDUSE.OLD_URBAN && lu <= LANDUSE.ROADSIDE;
for (let y = 1; y < MAP_H - 1; y++) {
for (let x = 1; x < MAP_W - 1; x++) {
const i = indexOf(x, y);
if (sea[i] || baseLanduse[i] === LANDUSE.FOREST) continue;
const transport = Math.max(roadLanduseInfluence[i] * 0.95, railInfluence2[i] * 0.95, stationInfluence[i] * 0.82);
const densityTransport = Math.max(roadDensityInfluence[i] * 0.95, stationDensityInfluence[i] * 1.05, railInfluence2[i] * 0.85);
let urbanNeighbors = 0;
let cbdNeighbors = 0;
for (let dy = -1; dy <= 1; dy++) {
for (let dx = -1; dx <= 1; dx++) {
if (!dx && !dy) continue;
const lu = baseLanduse[indexOf(x + dx, y + dy)];
if (isBuilt(lu)) urbanNeighbors++;
if (lu === LANDUSE.CBD) cbdNeighbors++;
}
}
if (baseLanduse[i] === LANDUSE.OLD_URBAN && coreInfluence[i] > 0.31 && cbdNeighbors >= 3) {
landuse[i] = LANDUSE.CBD;
continue;
}
if ((baseLanduse[i] === LANDUSE.FARMLAND || baseLanduse[i] === LANDUSE.RURAL) && urbanCapacity[i] > 0.10) {
const fringeChance = urbanNeighbors * 0.055 + cityInfluence[i] * 0.13 + transport * 0.12 + stationInfluence[i] * 0.08;
const noise = 0.23 + hash2(x, y, seed + 15050) * 0.24;
if (fringeChance > 0.34 + noise) {
landuse[i] = LANDUSE.SUBURB;
}
}
if ((river[i] > 0.10 || railInfluence2[i] > 0.16 || roadLanduseInfluence[i] > 0.22) && urbanNeighbors >= 3 && landuse[i] <= LANDUSE.FARMLAND && urbanCapacity[i] > 0.09) {
landuse[i] = cbdNeighbors >= 1 || coreInfluence[i] > 0.15 ? LANDUSE.OLD_URBAN : LANDUSE.SUBURB;
}
}
}
for (const park of logisticsParks) {
const r = 3;
for (let dy = -r; dy <= r; dy++) {
for (let dx = -r; dx <= r; dx++) {
const x = park.x + dx;
const y = park.y + dy;
if (!inside(x, y)) continue;
const i = indexOf(x, y);
if (sea[i] || landuse[i] === LANDUSE.CBD || landuse[i] === LANDUSE.FOREST) continue;
if (Math.hypot(dx, dy) <= r && (agriculture[i] > 0.18 || plain[i] > 0.15 || roadInfluence[i] > 0.06 || railInfluence2[i] > 0.05)) {
landuse[i] = LANDUSE.LOGISTICS;
}
}
}
}
if (maxDensity > 0) {
for (let i = 0; i < SIZE; i++) {
if (sea[i]) continue;
const lu = landuse[i];
let floor = ruralDensityFloor[i];
if (lu === LANDUSE.FARMLAND) {
floor = Math.max(floor, clamp(0.024 + agriculture[i] * 0.044 + ruralSuitability[i] * 0.024 + roadDensityInfluence[i] * 0.030 + stationDensityInfluence[i] * 0.026 + villageInfluence[i] * 0.018, 0, 0.110));
} else if (lu === LANDUSE.LOGISTICS) {
floor = Math.max(floor, clamp(0.018 + roadDensityInfluence[i] * 0.026 + railInfluence2[i] * 0.014 + logisticsInfluence[i] * 0.012, 0, 0.060));
} else if (lu === LANDUSE.RURAL) {
floor = Math.max(floor, clamp(0.012 + ruralSuitability[i] * 0.020 + developable[i] * 0.014 + roadDensityInfluence[i] * 0.024 + stationDensityInfluence[i] * 0.020, 0, 0.070));
} else if (lu === LANDUSE.FOREST) {
floor = Math.min(floor, (roadDensityInfluence[i] > 0.04 || villageInfluence[i] > 0.03) ? 0.026 : 0);
}
const normalized = populationDensity[i] / maxDensity;
populationDensity[i] = clamp(Math.max(normalized, floor));
if (lu === LANDUSE.FOREST && floor === 0 && populationDensity[i] < 0.012) populationDensity[i] = 0;
}
}
}
classifyLanduse();
for (const city of modernCities) {
let urbanFootprintCells = 0;
let coreFootprintCells = 0;
const r = Math.ceil((city.urbanRadius || 8) * 1.3);
for (let dy = -r; dy <= r; dy++) {
for (let dx = -r; dx <= r; dx++) {
const x = city.x + dx;
const y = city.y + dy;
if (!inside(x, y)) continue;
const i = indexOf(x, y);
if (sea[i]) continue;
if (Math.hypot(dx, dy) > r) continue;
if (landuse[i] >= LANDUSE.OLD_URBAN && landuse[i] <= LANDUSE.ROADSIDE) urbanFootprintCells++;
if (landuse[i] === LANDUSE.CBD) coreFootprintCells++;
}
}
city.urbanFootprintCells = urbanFootprintCells;
city.coreFootprintCells = coreFootprintCells;
}
const { landuse, populationDensity } = buildFeatureLanduse({
seed,
elevation, slope, sea, river, floodplain, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland,
developable, ruralSuitability, valleySettlement, coastalSettlement,
modernCities, logisticsParks,
roadLanduseInfluence, roadInfluence, roadDensityInfluence, railInfluence2, stationInfluence, stationDensityInfluence, villageInfluence,
cityInfluence, coreInfluence, oldTownInfluence, townInfluence, industrialInfluence, logisticsInfluence,
});
const transportDebug = {
humanStageVersion: "v2-sparse-raster",
aStarRoutes: 0,

View file

@ -155,7 +155,7 @@ export function finalizeAdminAwareTransport({ seed, terrain, features, admin, ge
...(features.villages || []).filter((p) => (p.population || 0) >= 5000),
...(features.ports || []).filter((p) => (p.population || 0) >= 5000 || p.portClass === "major" || p.portClass === "regional" || p.portClass === "fishing"),
];
const debug = { order: "settlements -> administration -> transport", adminCentersChecked: 0, adminLocalRoadsAdded: 0, nationalTownSpursAdded: 0, nationalTownChainsAdded: 0, nationalTownChainTownsCovered: 0, expresswayEndpointInterchangesAdded: 0, expresswaysSmoothed: 0 };
const debug = { order: "settlements -> administration -> transport", adminCentersChecked: 0, adminLocalRoadsAdded: 0, nationalTownSpursAdded: 0, nationalTownChainsAdded: 0, nationalTownChainTownsCovered: 0, expresswayEndpointInterchangesAdded: 0, expresswaysSmoothed: 0, expresswayMajorCityLinksAdded: 0, railCityChainsAdded: 0, railCityChainCitiesCovered: 0 };
// Local roads after admin: every municipal office cell should lie on a road.
const settlementTargets = [...(features.modernCities || []), ...(features.markets || []), ...(features.villages || []), ...(features.ports || [])];
@ -230,23 +230,23 @@ export function finalizeAdminAwareTransport({ seed, terrain, features, admin, ge
const parts = [];
const before = nearestTrunkOrHub(chain[0], 80);
if (before) {
const p = directPath(before, chain[0], { maxLength: 90, terrain, maxSeaRun: 10, maxTunnelRun: 0 });
const p = directPath(before, chain[0], { maxLength: 90, terrain, maxSeaRun: 10, maxTunnelRun: 10 });
if (p.length) parts.push(p);
}
for (let i = 1; i < chain.length; i++) {
const d = Math.hypot(chain[i - 1].x - chain[i].x, chain[i - 1].y - chain[i].y);
const p = directPath(chain[i - 1], chain[i], { maxLength: Math.max(28, d * 1.35 + 8), terrain, maxSeaRun: 10, maxTunnelRun: 0 });
const p = directPath(chain[i - 1], chain[i], { maxLength: Math.max(28, d * 1.35 + 8), terrain, maxSeaRun: 10, maxTunnelRun: 10 });
if (p.length) parts.push(p);
}
const after = chain.length >= 2 ? nearestTrunkOrHub(chain[chain.length - 1], 80) : null;
if (after) {
const p = directPath(chain[chain.length - 1], after, { maxLength: 90, terrain, maxSeaRun: 10, maxTunnelRun: 0 });
const p = directPath(chain[chain.length - 1], after, { maxLength: 90, terrain, maxSeaRun: 10, maxTunnelRun: 10 });
if (p.length) parts.push(p);
}
let path = concatPaths(parts);
if (path.length < 2) {
const target = nearestTrunkOrHub(chain[0], 90);
path = target ? directPath(chain[0], target, { maxLength: 100, terrain, maxSeaRun: 10, maxTunnelRun: 0 }) : [];
path = target ? directPath(chain[0], target, { maxLength: 100, terrain, maxSeaRun: 10, maxTunnelRun: 10 }) : [];
}
if (path.length >= 2 && chain.some((town) => pathTouchesCell(path, town.x, town.y, 0.65))) {
nationalRoads.push(path);
@ -261,6 +261,106 @@ export function finalizeAdminAwareTransport({ seed, terrain, features, admin, ge
debug.nationalTownChainTownsCovered = chainDebug.townsCovered;
debug.nationalTownSpursAdded = chainDebug.chainsAdded;
function majorCityKey(city) { return city?.name || `${Math.round(city.x)},${Math.round(city.y)}`; }
function expresswayCityComponents(cities, radius = 8.0) {
const parent = new Map();
function find(k) {
const p = parent.get(k);
if (p === k) return k;
const r = find(p);
parent.set(k, r);
return r;
}
function unite(a, b) {
const ra = find(a), rb = find(b);
if (ra !== rb) parent.set(ra, rb);
}
for (const city of cities) parent.set(majorCityKey(city), majorCityKey(city));
for (const path of [...expressways, ...externalExpressways]) {
const near = cities.filter((city) => pathTouchesCell(path, city.x, city.y, radius));
if (near.length >= 2) {
const first = majorCityKey(near[0]);
for (const city of near.slice(1)) unite(first, majorCityKey(city));
}
}
return new Map(cities.map((city) => [majorCityKey(city), find(majorCityKey(city))]));
}
function ensureMajorCityExpresswayLinks(minPopulation = 100000) {
const cities = (features.modernCities || [])
.filter((city) => (city.population || 0) >= minPopulation && inside(city.x, city.y))
.sort((a, b) => (b.population || 0) - (a.population || 0));
const result = { minPopulation, checked: cities.length, added: 0 };
if (cities.length < 2) return result;
for (let iter = 0; iter < cities.length * 2; iter++) {
const comps = expresswayCityComponents(cities);
if (new Set(comps.values()).size <= 1) break;
let best = null;
for (const a of cities) {
for (const b of cities) {
if (a === b || comps.get(majorCityKey(a)) === comps.get(majorCityKey(b))) continue;
const d = Math.hypot(a.x - b.x, a.y - b.y);
const score = d / Math.max(1, Math.log2(Math.sqrt((a.population || 1) * (b.population || 1))));
if (!best || score < best.score) best = { a, b, d, score };
}
}
if (!best) break;
let path = directPath(best.a, best.b, { maxLength: best.d * 1.18 + 12, terrain, maxSeaRun: 20 });
if (!path.length) path = directPath(best.a, best.b, { maxLength: best.d * 1.38 + 28, terrain });
if (!path.length) break;
path = smoothPath(path, 2);
expressways.push(path);
result.added++;
}
return result;
}
function cityRailChain(minPopulation = 50000) {
const cities = (features.modernCities || [])
.filter((city) => (city.population || 0) >= minPopulation && inside(city.x, city.y))
.sort((a, b) => a.x - b.x || a.y - b.y);
const result = { minPopulation, checked: cities.length, chainsAdded: 0, citiesCovered: 0 };
if (cities.length < 2) return result;
const existingRail = [...(features.railways || []), ...(features.branchRailways || []), ...(features.externalRailways || [])];
const uncovered = cities.filter((city) => !anyPathTouches(existingRail, city, 1.2));
if (!uncovered.length) return result;
const remaining = uncovered.slice();
let chain = [remaining.shift()];
while (remaining.length) {
const cur = chain[chain.length - 1];
let bestIndex = 0;
let bestD = Infinity;
for (let i = 0; i < remaining.length; i++) {
const d = Math.hypot(cur.x - remaining[i].x, cur.y - remaining[i].y);
if (d < bestD) { bestD = d; bestIndex = i; }
}
chain.push(remaining.splice(bestIndex, 1)[0]);
}
const parts = [];
for (let i = 1; i < chain.length; i++) {
const a = chain[i - 1], b = chain[i];
const d = Math.hypot(a.x - b.x, a.y - b.y);
let p = directPath(a, b, { maxLength: d * 1.55 + 20, terrain, maxSeaRun: 10, maxTunnelRun: 10 });
if (!p.length) p = directPath(a, b, { maxLength: d * 1.85 + 40, terrain });
if (p.length) parts.push(p);
}
const path = concatPaths(parts);
if (path.length >= 2) {
features.railways = features.railways || [];
features.railways.push(smoothPath(path, 1));
result.chainsAdded = 1;
result.citiesCovered = chain.filter((city) => pathTouchesCell(path, city.x, city.y, 1.2)).length;
}
return result;
}
const railDebug = cityRailChain(50000);
debug.railCityChainsAdded = railDebug.chainsAdded;
debug.railCityChainCitiesCovered = railDebug.citiesCovered;
const expressDebug = ensureMajorCityExpresswayLinks(100000);
debug.expresswayMajorCityLinksAdded = expressDebug.added;
// Expressway finalization after administration: smooth and ensure both endpoints are ICs.
for (let i = 0; i < expressways.length; i++) {
const smoothed = smoothPath(expressways[i], 2);
@ -282,6 +382,7 @@ export function finalizeAdminAwareTransport({ seed, terrain, features, admin, ge
features.externalRoads = dedupePaths(externalRoads, 1);
features.expressways = dedupePaths(expressways, 2);
features.externalExpressways = dedupePaths(externalExpressways, 2);
features.railways = dedupePaths(features.railways || [], 2);
features.interchanges = interchanges;
// Final invariant: after all dedupe passes, every municipal office cell has at least a local road cell on it.

View file

@ -748,6 +748,35 @@ export function splitOversizedPrefecturesByMunicipalityCount(nodes, owner, maxCo
return changed;
}
export function lockPrefectureCapitalNeighborMunicipalities(owner, nodes, seeds = [], maxNeighbors = 6) {
let changed = 0;
const seedIds = new Set((seeds || []).map((node) => node?.id).filter((id) => id !== undefined));
for (const seedNode of seeds || []) {
if (!seedNode || !nodes.has(seedNode.id)) continue;
const prefId = owner.get(seedNode.id);
if (prefId === undefined || prefId < 0) continue;
const neighbors = [...(nodes.get(seedNode.id)?.adjacent || [])]
.map(([id, edge]) => ({ node: nodes.get(id), id, edge }))
.filter((row) => row.node && owner.get(row.id) !== prefId && !seedIds.has(row.id))
.sort((a, b) => (a.edge.crossingCost ?? 1) - (b.edge.crossingCost ?? 1) || Math.hypot(a.node.x - seedNode.x, a.node.y - seedNode.y) - Math.hypot(b.node.x - seedNode.x, b.node.y - seedNode.y));
let taken = 0;
for (const row of neighbors) {
if (taken >= maxNeighbors) break;
const donorPref = owner.get(row.id);
if (donorPref === undefined || donorPref < 0 || donorPref === prefId) continue;
if (!wouldRemainConnectedAfterRemoval(nodes, owner, row.id, donorPref)) continue;
owner.set(row.id, prefId);
changed++;
taken++;
}
}
if (changed) {
repairPrefectureMunicipalityConnectivity(nodes, owner);
repairPrefectureMunicipalityEnclaves(nodes, owner, 6);
}
return changed;
}
function averageRegionalBorderField(adminId, municipalityToPrefectureId, prefectureMask, sea, field) {
if (!field) return 0;
let sum = 0;
@ -803,6 +832,10 @@ export function generatePrefecturesFromMunicipalities(context, adminResult) {
changedForConnectivity += repairPrefectureMunicipalityConnectivity(graph.nodes, owner);
changedForEnclaveRepair += repairPrefectureMunicipalityEnclaves(graph.nodes, owner, 10);
changedForConnectivity += repairPrefectureMunicipalityConnectivity(graph.nodes, owner);
const changedForCapitalNeighborLock = lockPrefectureCapitalNeighborMunicipalities(owner, graph.nodes, seeds, 7);
changedForConnectivity += repairPrefectureMunicipalityConnectivity(graph.nodes, owner);
changedForEnclaveRepair += repairPrefectureMunicipalityEnclaves(graph.nodes, owner, 12);
changedForConnectivity += repairPrefectureMunicipalityConnectivity(graph.nodes, owner);
const maxAdminId = Math.max(-1, ...[...graph.nodes.keys()]);
const municipalityToPrefectureId = new Int16Array(maxAdminId + 1);
municipalityToPrefectureId.fill(-1);
@ -846,6 +879,7 @@ export function generatePrefecturesFromMunicipalities(context, adminResult) {
prefectureMunicipalityCountRebalanceChangedMunicipalities: (changedForMunicipalityCountRebalance || 0) + (changedForPostMergeMunicipalityCountRebalance || 0),
prefectureTinyMunicipalityCountMergeChangedMunicipalities: changedForTinyPrefectureCountMerge || 0,
prefectureOversizedCountSplitChangedMunicipalities: changedForOversizedPrefectureSplit || 0,
prefectureCapitalNeighborLockChangedMunicipalities: changedForCapitalNeighborLock || 0,
finalRegionalMaxMunicipalityCount: Math.max(...prefectureMunicipalityCounts(owner).values()),
finalRegionalMunicipalityCountCap: 88,
finalRegionalMinMunicipalityCount: Math.min(...prefectureMunicipalityCounts(owner).values()),

File diff suppressed because it is too large Load diff

View file

@ -52,6 +52,81 @@ export function pathAverageField(path, field) {
return n ? sum / n : 0;
}
export function markPathInfluence(field, path, radius = 5, strength = 1, sea = null) {
for (const [px, py] of path || []) {
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
if (dx * dx + dy * dy > radius * radius) continue;
const x = px + dx;
const y = py + dy;
if (!inside(x, y)) continue;
const i = indexOf(x, y);
if (sea?.[i]) continue;
const d = Math.hypot(dx, dy);
const v = strength * Math.pow(1 - d / Math.max(1, radius), 1.35);
if (v > field[i]) field[i] = v;
}
}
}
}
export function sampledNetworkCells(paths, step = 2, sea = null) {
const cells = [];
for (let pathId = 0; pathId < (paths || []).length; pathId++) {
const path = paths[pathId];
for (let k = 0; k < (path?.length || 0); k += step) {
const [x, y] = path[k];
if (inside(x, y) && !sea?.[indexOf(x, y)]) cells.push({ x, y, pathId });
}
}
return cells;
}
export function pathCumulativeLengths(path) {
const cum = [0];
for (let k = 1; k < (path?.length || 0); k++) {
cum.push(cum[cum.length - 1] + Math.hypot(path[k][0] - path[k - 1][0], path[k][1] - path[k - 1][1]));
}
return cum;
}
export function pointAtPathDistance(path, cum, dist) {
if (!path?.length) return null;
if (dist <= 0) return { x: path[0][0], y: path[0][1], s: 0 };
const total = cum[cum.length - 1] || 0;
if (dist >= total) {
const p = path[path.length - 1];
return { x: p[0], y: p[1], s: total };
}
let k = 1;
while (k < cum.length && cum[k] < dist) k++;
const a = path[k - 1];
const b = path[k];
const seg = Math.max(0.0001, cum[k] - cum[k - 1]);
const t = clamp((dist - cum[k - 1]) / seg);
return { x: Math.round(a[0] + (b[0] - a[0]) * t), y: Math.round(a[1] + (b[1] - a[1]) * t), s: dist };
}
export function meanFieldAround(field, x, y, radius = 8, sea = null) {
let sum = 0;
let n = 0;
const r = Math.ceil(radius);
for (let dy = -r; dy <= r; dy++) {
for (let dx = -r; dx <= r; dx++) {
if (dx * dx + dy * dy > radius * radius) continue;
const nx = x + dx;
const ny = y + dy;
if (!inside(nx, ny)) continue;
const i = indexOf(nx, ny);
if (sea?.[i]) continue;
const w = 1 - Math.hypot(dx, dy) / Math.max(1, radius);
sum += (field?.[i] || 0) * (0.35 + w);
n += 0.35 + w;
}
}
return n ? sum / n : 0;
}
export function routeQualityStats(path, fields = {}) {
if (!path?.length) return { length: 0, compactness: Infinity, highElevationShare: 1, steepShare: 1, waterShare: 1, avgPotential: 0, avgPenalty: 0 };
const length = pathLengthCells(path);

View file

@ -21,7 +21,7 @@ export const NAME_KANJI_POOLS = {
inlandTerrain: [
"山", "野", "野", "沢",
"森", "林", "岡", "丘", "坂",
"峰", "峠", "嶺", "尾", "平", "坪", "延",
"峰", "嶺", "尾", "平", "坪", "延",
"窪", "久", "迫", "久保", "玖保", "佐古", "作古",
"塚", "牧", "畑", "田", "森", "幡多", "幡", "畠", "秦", "植", "生",
"郷", "里",
@ -85,7 +85,7 @@ export const NAME_KANJI_POOLS = {
],
archaicSuffixes: [
"井", "羽", "江", "恵", "尾",
"伊", "衣", "井", "羽", "江", "恵", "尾",
"賀", "鹿", "喜", "吉", "伎", "久", "玖", "気", "家", "祁", "古", "子",
"佐", "紗", "左", "志", "路", "師", "須", "瀬", "曽", "蘇", "総",
"多", "太", "知", "津", "豆", "土", "登",
@ -107,7 +107,7 @@ export const NAME_KANJI_POOLS = {
settlementWords: [
"里", "郷", "村", "町", "宿", "垣", "坪", "軒", "惣",
"庄", "ノ庄", "之庄", "院", "宮", "ノ宮", "之宮", "寺", "社", "堂",
"庄", "ノ庄", "之庄", "宮", "ノ宮", "之宮", "寺", "社", "堂",
"城", "館", "屋", "家", "所",
"市", "場", "関", "地蔵", "辻", "角", "堰", "通", "路", "道", "筋",
]

View file

@ -655,17 +655,19 @@ function drawBridgeOverlay(ctx, map, path, width, mode = "road") {
}
function drawTunnelOverlay(ctx, map, path, width, mode = "road") {
if (mode !== "expressway") return;
if (mode !== "expressway" && mode !== "national") return;
const vectorMode = mode === "expressway" ? "expressway" : "default";
const limit = mode === "expressway" ? 10 : 10;
const tunnelChunks = specialTransportSubpaths(
map,
path,
(i) => !map.sea?.[i] && (((map.elevation?.[i] || 0) >= 0.74 && (map.ridgeField?.[i] || 0) >= 0.46) || (map.naturalBarrierScore?.[i] || 0) >= 0.82),
2,
true,
10
limit
);
for (const chunk of tunnelChunks) {
drawDottedOutlinePath(ctx, chunk, "rgba(42, 42, 42, 0.96)", 1.15, Math.max(1.9, width * 0.82), "expressway");
drawDottedOutlinePath(ctx, chunk, "rgba(42, 42, 42, 0.96)", mode === "expressway" ? 1.15 : 1.0, Math.max(1.75, width * 0.82), vectorMode);
}
}
@ -1135,8 +1137,8 @@ export function drawMap(canvas, map, options) {
}
if (showRoads) {
for (const path of generalRoadPaths) { drawBridgeOverlay(ctx, map, path, 1.55, "road"); }
for (const path of map.nationalRoads) { drawBridgeOverlay(ctx, map, path, 2.0, "road"); }
for (const path of map.externalRoads) { drawBridgeOverlay(ctx, map, path, 2.0, "road"); }
for (const path of map.nationalRoads) { drawBridgeOverlay(ctx, map, path, 2.0, "road"); drawTunnelOverlay(ctx, map, path, 1.9, "national"); }
for (const path of map.externalRoads) { drawBridgeOverlay(ctx, map, path, 2.0, "road"); drawTunnelOverlay(ctx, map, path, 1.9, "national"); }
for (const path of map.expressways) { drawBridgeOverlay(ctx, map, path, 2.45, "expressway"); drawTunnelOverlay(ctx, map, path, 2.25, "expressway"); }
for (const path of map.externalExpressways) { drawBridgeOverlay(ctx, map, path, 2.45, "expressway"); drawTunnelOverlay(ctx, map, path, 2.25, "expressway"); }
}