splittintting
This commit is contained in:
parent
b17be0e0d2
commit
d2e7a80e72
11 changed files with 1341 additions and 1349 deletions
327
mapFeatureContext.js
Normal file
327
mapFeatureContext.js
Normal 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
214
mapFeatureLanduse.js
Normal 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
47
mapFeatureSettlements.js
Normal 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
191
mapFeatureTransportTools.js
Normal 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 };
|
||||
}
|
||||
|
||||
751
mapFeatures.js
751
mapFeatures.js
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
942
mapTransport.js
942
mapTransport.js
File diff suppressed because it is too large
Load diff
|
|
@ -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);
|
||||
|
|
|
|||
6
names.js
6
names.js
|
|
@ -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: [
|
||||
"里", "郷", "村", "町", "宿", "垣", "坪", "軒", "惣",
|
||||
"庄", "ノ庄", "之庄", "院", "宮", "ノ宮", "之宮", "寺", "社", "堂",
|
||||
"庄", "ノ庄", "之庄", "宮", "ノ宮", "之宮", "寺", "社", "堂",
|
||||
"城", "館", "屋", "家", "所",
|
||||
"市", "場", "関", "地蔵", "辻", "角", "堰", "通", "路", "道", "筋",
|
||||
]
|
||||
|
|
|
|||
12
renderer.js
12
renderer.js
|
|
@ -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"); }
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue