This commit is contained in:
33333-33333 2026-05-28 15:48:42 +09:00
commit 860471f805
13 changed files with 1643 additions and 356 deletions

View file

@ -1,11 +1,12 @@
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 { createPointSpatialIndex, distanceToNearest, influenceFromPaths, influenceFromPoints, samplePath } from "./mapGeneratorHelpers.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";
import { buildCoarseCostGraph, refineCoarsePath, routeCoarsePath } from "./mapTransportGraph.js";
// Lightweight Human Geography V2
// --------------------------------
@ -17,6 +18,16 @@ import { buildFeatureTransportCostFields } from "./mapFeatureTransportTools.js";
// 4. synthesize population and land-use fields in one raster pass
export function generateMapFeatures(seed, terrain) {
const SPEED_TOLERANCE = 0.90;
const featureTimings = [];
const nowMs = () => typeof performance !== "undefined" && performance.now ? performance.now() : Date.now();
let timingMark = nowMs();
function markFeatureTiming(key) {
const t = nowMs();
featureTimings.push({ key, ms: Math.round((t - timingMark) * 10) / 10 });
timingMark = t;
}
const {
elevation,
slope,
@ -65,6 +76,7 @@ export function generateMapFeatures(seed, terrain) {
pickRegionalPoints,
pickGlobalPoints,
} = featureContext;
markFeatureTiming("context");
// --- 2. Sparse points ----------------------------------------------------
let ports = pickGlobalPoints(portSuitability || coastalSettlement, {
threshold: 0.30 + rand(seed, 1001) * 0.08,
@ -84,6 +96,8 @@ export function generateMapFeatures(seed, terrain) {
ports[0].kind = "Major Port";
}
const commercialPorts = ports.filter((p) => p.portClass === "major" || p.portClass === "regional");
const portIndex = createPointSpatialIndex(ports, 12);
const commercialPortIndex = createPointSpatialIndex(commercialPorts, 12);
const crossings = pickGlobalPoints(crossingSuitability || confluenceField, {
threshold: 0.30 + rand(seed, 1011) * 0.06,
@ -92,6 +106,7 @@ export function generateMapFeatures(seed, terrain) {
seedOffset: 1010,
predicate: (x, y, i) => river[i] > 0.12 || confluenceField[i] > 0.09,
}).map((p) => ({ ...p, kind: "River Crossing" }));
const crossingIndex = createPointSpatialIndex(crossings, 8);
const passes = pickGlobalPoints(passSuitability || valleySettlement, {
threshold: 0.18 + rand(seed, 1021) * 0.06,
@ -199,6 +214,7 @@ export function generateMapFeatures(seed, terrain) {
const open = Math.max(0, plain[i] * 0.92 + agriculture[i] * 0.72 + basinField[i] * 0.26 + (depositionalLowland?.[i] || 0) * 0.20 - river[i] * 0.22 - valleyField[i] * 0.10 - flowAccum[i] * 0.08 - slope[i] * 0.24 - ridgeField[i] * 0.14);
openPlainVillageScore[i] = clamp(open * 0.82 + settlementCluster[i] * 0.14 + ruralSuitability[i] * 0.16 + fieldValue(geoHabitability, i, developable[i]) * 0.16 + fieldValue(geoLowlandCapacity, i, plain[i]) * 0.12 - geographicAnchorInfluence[i] * 0.05);
}
const initialVillageIndex = createPointSpatialIndex(villages, 8);
const supplementalPlainVillages = pickRegionalPoints(openPlainVillageScore, {
stride: 2,
threshold: 0.235 + rand(seed, 1036) * 0.020,
@ -216,7 +232,7 @@ export function generateMapFeatures(seed, terrain) {
return Math.round(clamp(raw + rand(seed, 1037 + regionId * 29) * 1.4, min, max));
},
extraScore: (x, y, i) => Math.max(0, plain[i] * 0.34 + agriculture[i] * 0.26 - river[i] * 0.20 - valleyField[i] * 0.12),
}).filter((p) => distanceToNearest(villages, p.x, p.y) >= 6.5)
}).filter((p) => !initialVillageIndex.hasWithin(p.x, p.y, 6.5))
.map((p, n) => {
const i = indexOf(p.x, p.y);
const population = Math.round((1100 + Math.pow(rand(seed, 18220 + n * 31 + p.x * 7 + p.y), 1.12) * 7600 + agriculture[i] * 3900 + plain[i] * 2200) / 100) * 100;
@ -233,8 +249,8 @@ export function generateMapFeatures(seed, terrain) {
if (sea[i]) continue;
const openPlainMarket = Math.max(0, plain[i] * 0.68 + agriculture[i] * 0.52 + basinField[i] * 0.24 + (depositionalLowland?.[i] || 0) * 0.18 - river[i] * 0.16 - valleyField[i] * 0.06 - slope[i] * 0.18);
const featurePull = Math.max(
distanceToNearest(ports, x, y) < 10 ? 0.16 : 0,
distanceToNearest(crossings, x, y) < 6 ? 0.035 : 0,
portIndex.hasWithin(x, y, 10) ? 0.16 : 0,
crossingIndex.hasWithin(x, y, 6) ? 0.035 : 0,
confluenceField[i] * 0.08
);
const valleyMouth = valleyField[i] > 0.22 && (plain[i] > 0.22 || basinField[i] > 0.18 || coastalLowland[i] > 0.18) ? 0.14 : 0;
@ -278,10 +294,10 @@ export function generateMapFeatures(seed, terrain) {
const max = st.area > 3600 ? 20 : st.area > 2200 ? 14 : st.area > 800 ? 7 : 3;
return Math.round(clamp(raw + rand(seed, 1043 + regionId * 23) * 0.8, min, max));
},
extraScore: (x, y, i) => (distanceToNearest(commercialPorts, x, y) < 10 ? 0.12 : 0) + coastalSettlement[i] * 0.08 + Math.max(0, plain[i] * 0.32 + agriculture[i] * 0.20 - river[i] * 0.16) * 0.09 + confluenceField[i] * 0.035,
extraScore: (x, y, i) => (commercialPortIndex.hasWithin(x, y, 10) ? 0.12 : 0) + coastalSettlement[i] * 0.08 + Math.max(0, plain[i] * 0.32 + agriculture[i] * 0.20 - river[i] * 0.16) * 0.09 + confluenceField[i] * 0.035,
}).map((p, n) => {
const i = indexOf(p.x, p.y);
const kind = coastalSettlement[i] > 0.38 && distanceToNearest(ports, p.x, p.y) < 11 ? "Port Town" : valleySettlement[i] > 0.48 ? "Valley Market Town" : "Market Town";
const kind = coastalSettlement[i] > 0.38 && portIndex.hasWithin(p.x, p.y, 11) ? "Port Town" : valleySettlement[i] > 0.48 ? "Valley Market Town" : "Market Town";
const population = Math.round((9000 + Math.pow(rand(seed, 18100 + n * 19 + p.x * 5 + p.y), 1.02) * 70000 + marketScore[i] * 40000 + villageInfluence[i] * 7800 + Math.max(0, plain[i] * 0.48 + agriculture[i] * 0.30 + basinField[i] * 0.18 - river[i] * 0.14) * 22000 + coastalSettlement[i] * 12000) / 1000) * 1000;
return { ...p, kind, population };
});
@ -292,6 +308,8 @@ export function generateMapFeatures(seed, terrain) {
const open = Math.max(0, plain[i] * 0.86 + agriculture[i] * 0.64 + basinField[i] * 0.28 + (depositionalLowland?.[i] || 0) * 0.22 - river[i] * 0.20 - valleyField[i] * 0.10 - flowAccum[i] * 0.08 - slope[i] * 0.24 - ridgeField[i] * 0.16);
openPlainMarketScore[i] = clamp(open * 0.78 + townSuitability[i] * 0.16 + fieldValue(geoNaturalCentrality, i, townSuitability[i]) * 0.18 + fieldValue(geoHabitability, i, developable[i]) * 0.12 + villageInfluence[i] * 0.16 + settlementCluster[i] * 0.10 + geographicAnchorInfluence[i] * 0.05);
}
const preSupplementalMarketIndex = createPointSpatialIndex(markets, 12);
const villageIndexForMarketSpacing = createPointSpatialIndex(villages, 8);
const supplementalPlainMarkets = pickRegionalPoints(openPlainMarketScore, {
stride: 2,
threshold: 0.335 + rand(seed, 1046) * 0.025,
@ -309,13 +327,15 @@ export function generateMapFeatures(seed, terrain) {
return Math.round(clamp(raw + rand(seed, 1047 + regionId * 31) * 0.9, min, max));
},
extraScore: (x, y, i) => Math.max(0, plain[i] * 0.24 + agriculture[i] * 0.18 - river[i] * 0.14 - valleyField[i] * 0.08),
}).filter((p) => distanceToNearest(markets, p.x, p.y) >= 10.5 && distanceToNearest(villages, p.x, p.y) >= 4.5)
}).filter((p) => !preSupplementalMarketIndex.hasWithin(p.x, p.y, 10.5) && !villageIndexForMarketSpacing.hasWithin(p.x, p.y, 4.5))
.map((p, n) => {
const i = indexOf(p.x, p.y);
const population = Math.round((10000 + Math.pow(rand(seed, 18340 + n * 37 + p.x * 11 + p.y), 1.02) * 52000 + openPlainMarketScore[i] * 26000 + agriculture[i] * 9000 + plain[i] * 8000) / 1000) * 1000;
return { ...p, kind: "Plain Market Town", population };
});
markets = [...markets, ...supplementalPlainMarkets];
let marketIndex = createPointSpatialIndex(markets, 12);
let villageIndex = createPointSpatialIndex(villages, 8);
// Sparse-area towns: when a developable basin/plain/coast has few nearby towns,
// add a small market town candidate. This avoids large inhabited regions being
@ -330,6 +350,10 @@ export function generateMapFeatures(seed, terrain) {
const livable = clamp(townSuitability[i] * 0.34 + fieldValue(geoNaturalCentrality, i, townSuitability[i]) * 0.22 + fieldValue(geoHabitability, i, developable[i]) * 0.20 + fieldValue(geoAccessibility, i, 0) * 0.12 + developable[i] * 0.22 + plain[i] * 0.24 + agriculture[i] * 0.18 + basinField[i] * 0.16 + coastalSettlement[i] * 0.14 + valleySettlement[i] * 0.12 - fieldValue(geoBarrier, i, 0) * 0.12 - slope[i] * 0.22 - ridgeField[i] * 0.10);
sparseTownScore[i] = clamp(livable * (0.42 + remoteness * 0.88) + settlementGap * 0.16);
}
const preSparseMarketIndex = marketIndex;
const preSparseVillageIndex = villageIndex;
const marketCountByRegion = new Map();
for (const m of markets) marketCountByRegion.set(m.regionId, (marketCountByRegion.get(m.regionId) || 0) + 1);
const sparseMarkets = pickRegionalPoints(sparseTownScore, {
stride: 2,
threshold: 0.315 + rand(seed, 1049) * 0.025,
@ -341,19 +365,20 @@ export function generateMapFeatures(seed, terrain) {
quotaForRegion: (regionId, st) => {
if (!st || st.developableCells < 90) return 0;
const vf = visibilityFactor(regionId, st);
const underServed = clamp(1.0 - ((markets.filter((m) => m.regionId === regionId).length || 0) / Math.max(1, st.area / 850)));
const underServed = clamp(1.0 - ((marketCountByRegion.get(regionId) || 0) / Math.max(1, st.area / 850)));
const raw = (st.developableCells / 900 + st.plainCells / 720 + st.coastCells / 560 + 0.55) * vf * (0.55 + underServed * 0.75);
return Math.round(clamp(raw + rand(seed, 1050 + regionId * 37) * 0.45, 0, st.area > 2000 ? 2 : 1));
},
extraScore: (x, y, i) => clamp((0.34 - existingTownInfluenceForSparseFill[i]) * 0.38 + plain[i] * 0.10 + agriculture[i] * 0.08 + coastalSettlement[i] * 0.06),
})
.filter((p) => distanceToNearest(markets, p.x, p.y) >= 12 && distanceToNearest(villages, p.x, p.y) >= 4.5)
.filter((p) => !preSparseMarketIndex.hasWithin(p.x, p.y, 12) && !preSparseVillageIndex.hasWithin(p.x, p.y, 4.5))
.map((p, n) => {
const i = indexOf(p.x, p.y);
const population = Math.round((8000 + Math.pow(rand(seed, 18480 + n * 41 + p.x * 13 + p.y), 1.08) * 36000 + sparseTownScore[i] * 26000) / 1000) * 1000;
return { ...p, kind: "Sparse Market Town", population };
});
markets = [...markets, ...sparseMarkets];
marketIndex = createPointSpatialIndex(markets, 12);
const defenseScore = new Float32Array(SIZE);
for (let i = 0; i < SIZE; i++) {
@ -477,7 +502,7 @@ export function generateMapFeatures(seed, terrain) {
// regional pass.
modernCities.sort((a, b) => b.capacity - a.capacity || b.score - a.score);
const regionalCapitalSlots = Math.max(1, Math.min(4, Math.round(Math.sqrt(Math.max(1, modernCities.length)))));
const regionalCapitalSlots = Math.max(1, Math.min(2, Math.floor(Math.sqrt(Math.max(1, modernCities.length)) / 2)));
for (const [rank, city] of modernCities.entries()) {
const i = indexOf(city.x, city.y);
const st = regionStats.get(city.regionId);
@ -489,11 +514,35 @@ export function generateMapFeatures(seed, terrain) {
fieldValue(geoAccessibility, i, 0) * 0.18 +
Math.log10((city.capacity || 26000) + 1) / 7 * 0.26
);
const isTopCenter = rank < regionalCapitalSlots || geoTierScore > 0.58;
const isTopCenter = rank < regionalCapitalSlots || geoTierScore > 0.8;
const isRegionalCapital = isFirstInRegion && (isTopCenter || (city.capacity || 0) > 210000 || (st?.highCentralityCells || 0) > 220);
const rawPop = isRegionalCapital
? 180000 + rand(seed, 12201 + city.regionId * 17) * (isTopCenter ? 760000 : 360000)
: 52000 + Math.pow(rand(seed, 12202 + rank * 19 + city.x), 0.68) * 360000;
const u = rand(st.seed, city.x, city.y, 9101);
const v = rand(st.seed, city.x, city.y, 9102);
const w = rand(st.seed, city.x, city.y, 9103);
let rawPop;
if (isRegionalCapital) {
if (isTopCenter) {
// largest 3M - 11M
rawPop =
3000000 +
Math.pow(u, 0.42) * 5200000 +
Math.pow(v, 3.2) * 2800000;
} else {
// larger 0.25M - 2.5M
rawPop =
250000 +
Math.pow(u, 0.55) * 1450000 +
Math.pow(v, 2.4) * 900000;
}
} else {
// normal 5k - 0.75k
rawPop =
52000 +
Math.pow(u, 0.72) * 520000 +
Math.pow(v, 3.0) * 320000;
}
const capMultiplier = isRegionalCapital ? (isTopCenter ? 1.66 : 1.42) : 1.20;
const population = Math.round(Math.min(rawPop, city.capacity * capMultiplier) / 1000) * 1000;
const floor = isRegionalCapital ? (isTopCenter ? 210000 : 120000) : 42000;
@ -524,14 +573,18 @@ export function generateMapFeatures(seed, terrain) {
if (!nearestCity.city) return true;
return nearestCity.d >= urbanSettlementExclusionRadius(nearestCity.city, "market");
});
marketIndex = createPointSpatialIndex(markets, 12);
villages = villages.filter((v) => {
const nearestCity = modernCities.reduce((best, city) => {
const d = Math.hypot(v.x - city.x, v.y - city.y);
return d < best.d ? { city, d } : best;
}, { city: null, d: Infinity });
if (nearestCity.city && nearestCity.d < urbanSettlementExclusionRadius(nearestCity.city, "village")) return false;
return distanceToNearest(markets, v.x, v.y) >= 3.4;
return !marketIndex.hasWithin(v.x, v.y, 3.4);
});
marketIndex = createPointSpatialIndex(markets, 12);
villageIndex = createPointSpatialIndex(villages, 8);
const cityIndex = createPointSpatialIndex(modernCities, 12);
villageInfluence = influenceFromPoints(villages, 6, (v) => clamp((v.population || 1800) / 4200, 0.35, 1.2));
const settlementHierarchyDebug = {
version: "phase2-unified-settlement-hierarchy",
@ -542,6 +595,7 @@ export function generateMapFeatures(seed, terrain) {
villagesAfterHierarchyFilter: villages.length,
regionalCapitalSlots,
};
markFeatureTiming("settlement-placement");
function cityPopulationCap(city) {
const radius = city?.isRegionalCapital ? 29 : city?.population >= 350000 ? 26 : 18;
@ -561,6 +615,7 @@ export function generateMapFeatures(seed, terrain) {
sea, agriculture, plain, basinField, coastalLowland, slope, ridgeField,
modernCities, markets, commercialPorts, villages,
});
markFeatureTiming("settlement-demand");
const transportFields = buildFeatureTransportCostFields({
seed,
sea, elevation, slope, river, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland,
@ -569,7 +624,26 @@ export function generateMapFeatures(seed, terrain) {
preliminaryTownInfluence, preliminaryVillageInfluence,
valleySettlement, coastalSettlement, developable,
});
markFeatureTiming("transport-cost-fields");
const cachedInfluenceFromPaths = createPathInfluenceCache(influenceFromPaths);
const coarseRouteStats = { graphBuilds: 0, attempted: 0, routed: 0, refined: 0, fallback: 0, skipped: 0 };
const coarseGraphCache = new WeakMap();
function coarseGraphFor(costField, mode = "national") {
const scale = mode === "local" ? 5 : mode === "rail" ? 4 : mode === "expressway" ? 4 : 4;
let byMode = coarseGraphCache.get(costField);
if (!byMode) {
byMode = new Map();
coarseGraphCache.set(costField, byMode);
}
const key = `${mode}:${scale}`;
let graph = byMode.get(key);
if (!graph) {
graph = buildCoarseCostGraph({ sea, costField }, { scale });
byMode.set(key, graph);
coarseRouteStats.graphBuilds++;
}
return graph;
}
const componentCityInfluence = influenceFromPoints([...modernCities, ...markets], 11, (p) => clamp((p.population || 8000) / 50000, 0.18, 8.0));
const componentCapitalInfluence = influenceFromPoints(modernCities.filter((p) => p.isPrefecturalCapital), 16, () => 5.0);
@ -637,17 +711,30 @@ export function generateMapFeatures(seed, terrain) {
return support > 0.14 || transportFields.nationalPotential[i] > 0.37;
}
const routeScratch = {
score: new Float32Array(SIZE),
cameFrom: new Int32Array(SIZE),
seen: new Int32Array(SIZE),
closed: new Int32Array(SIZE),
epoch: 0,
};
function traceCorridorByCost(start, goalRegionPredicate, costField, penaltyField, options = {}) {
if (!start || !inside(start.x, start.y)) return [];
const startIndex = indexOf(start.x, start.y);
if (sea[startIndex] || costField[startIndex] >= INF) return [];
const score = new Float32Array(SIZE);
const cameFrom = new Int32Array(SIZE);
const closed = new Uint8Array(SIZE);
score.fill(INF);
cameFrom.fill(-1);
const { score, cameFrom, seen, closed } = routeScratch;
let epoch = ++routeScratch.epoch;
if (routeScratch.epoch > 2000000000) {
routeScratch.seen.fill(0);
routeScratch.closed.fill(0);
routeScratch.epoch = 1;
epoch = 1;
}
const heap = new MinHeap();
seen[startIndex] = epoch;
score[startIndex] = 0;
cameFrom[startIndex] = -1;
heap.push({ i: startIndex, f: 0 });
const curvePenalty = options.curvePenalty ?? 0.12;
const penaltyStrength = options.penaltyStrength ?? 1.0;
@ -662,9 +749,10 @@ export function generateMapFeatures(seed, terrain) {
while (heap.length && expanded++ < maxExpanded) {
const current = heap.pop();
if (!current || closed[current.i]) continue;
closed[current.i] = 1;
const [cx, cy] = xyOf(current.i);
if (!current || closed[current.i] === epoch) continue;
closed[current.i] = epoch;
const cx = current.i % MAP_W;
const cy = Math.floor(current.i / MAP_W);
if (current.i !== startIndex && Math.hypot(cx - start.x, cy - start.y) >= minGoalDistance && goalRegionPredicate(cx, cy, current.i)) {
goalIndex = current.i;
break;
@ -677,12 +765,13 @@ export function generateMapFeatures(seed, terrain) {
if (!inside(nx, ny)) continue;
if (bounds && (nx < bounds.minX || nx > bounds.maxX || ny < bounds.minY || ny > bounds.maxY)) continue;
const ni = indexOf(nx, ny);
if (closed[ni] || sea[ni] || costField[ni] >= INF) continue;
if (closed[ni] === epoch || sea[ni] || costField[ni] >= INF) continue;
if (sameRegion >= 0 && options.keepRegion !== false && regionIdAt(nx, ny) !== sameRegion) continue;
const prev = cameFrom[current.i];
let turn = 0;
if (prev >= 0) {
const [px, py] = xyOf(prev);
const px = prev % MAP_W;
const py = Math.floor(prev / MAP_W);
const ax = cx - px;
const ay = cy - py;
turn = Math.abs(ax * dy - ay * dx) > 0 ? curvePenalty : 0;
@ -699,7 +788,8 @@ export function generateMapFeatures(seed, terrain) {
);
const surfaceGrain = (options.surfaceGrain ?? 0) * valueNoise(nx, ny, seed + 13941, 18);
const nd = score[current.i] + Math.max(0.08, costField[ni] + antiConcentration + turn - terrainFlowBias + surfaceGrain) * Math.hypot(dx, dy);
if (nd < score[ni]) {
if (seen[ni] !== epoch || nd < score[ni]) {
seen[ni] = epoch;
score[ni] = nd;
cameFrom[ni] = current.i;
const h = goalHint ? Math.hypot(nx - goalHint.x, ny - goalHint.y) * heuristicWeight : 0;
@ -711,7 +801,7 @@ export function generateMapFeatures(seed, terrain) {
if (goalIndex < 0) return [];
const path = [];
for (let p = goalIndex; p >= 0; p = cameFrom[p]) {
path.push(xyOf(p));
path.push([p % MAP_W, Math.floor(p / MAP_W)]);
if (p === startIndex) break;
}
return path.reverse();
@ -1407,7 +1497,7 @@ export function generateMapFeatures(seed, terrain) {
...markets.filter(inRegion).map((p) => ({ ...p, nodeWeight: 3.2 + (p.population || 0) / 25000 })),
...commercialPorts.filter(inRegion).map((p) => ({ ...p, nodeWeight: p.portClass === "major" ? 6.5 : 4.6 })),
...passes.filter(inRegion).map((p) => ({ ...p, nodeWeight: 2.2 })),
].sort((a, b) => b.nodeWeight - a.nodeWeight).slice(0, (regionStats.get(regionId)?.area || 0) > 2200 ? 16 : 10);
].sort((a, b) => b.nodeWeight - a.nodeWeight).slice(0, (regionStats.get(regionId)?.area || 0) > 2200 ? 13 : 8);
}
function dedupePointCandidates(points, minDistance = 5) {
@ -1474,13 +1564,13 @@ export function generateMapFeatures(seed, terrain) {
}
function transportCandidatePoints(mode, potentialField, options = {}) {
const maxCells = options.maxCells ?? (mode === "expressway" ? 18 : mode === "rail" ? 26 : 40);
const maxSettlements = options.maxSettlements ?? (mode === "expressway" ? 20 : mode === "rail" ? 34 : 56);
const maxCells = options.maxCells ?? (mode === "expressway" ? 14 : mode === "rail" ? 20 : 30);
const maxSettlements = options.maxSettlements ?? (mode === "expressway" ? 16 : mode === "rail" ? 27 : 44);
const minDistance = options.minDistance ?? (mode === "expressway" ? 13 : mode === "rail" ? 10 : 8);
const cells = preferenceCellAnchors(mode, potentialField, maxCells, minDistance);
const settlements = modeSettlementAnchors(mode, potentialField, maxSettlements);
return dedupePointCandidates([...settlements, ...cells].sort((a, b) => b.score - a.score), Math.max(4, minDistance * 0.55))
.slice(0, options.maxTotal ?? (mode === "expressway" ? 32 : mode === "rail" ? 48 : 74));
.slice(0, options.maxTotal ?? (mode === "expressway" ? 26 : mode === "rail" ? 38 : 58));
}
function routeBetweenTrafficCandidates(a, b, mode, costField, penaltyField, options = {}) {
@ -1491,14 +1581,38 @@ export function generateMapFeatures(seed, terrain) {
if (sea[indexOf(start.x, start.y)] || sea[indexOf(target.x, target.y)]) return [];
const d = Math.hypot(start.x - target.x, start.y - target.y);
const snap = options.snapRadius ?? (mode === "expressway" ? 3 : mode === "rail" ? 2.5 : 2.25);
const searchPad = options.searchPad ?? Math.ceil(Math.max(18, Math.min(58, d * (mode === "expressway" ? 0.46 : mode === "rail" ? 0.42 : 0.36))));
const searchPad = options.searchPad ?? Math.ceil(Math.max(16, Math.min(48, d * (mode === "expressway" ? 0.40 : mode === "rail" ? 0.37 : 0.31))));
const bounds = options.bounds || {
minX: Math.max(0, Math.min(start.x, target.x) - searchPad),
maxX: Math.min(MAP_W - 1, Math.max(start.x, target.x) + searchPad),
minY: Math.max(0, Math.min(start.y, target.y) - searchPad),
maxY: Math.min(MAP_H - 1, Math.max(start.y, target.y) + searchPad),
};
const path = traceCorridorByCost(
const coarseThreshold = options.coarseThreshold ?? (mode === "local" ? 18 : mode === "rail" ? 22 : mode === "expressway" ? 30 : 20);
let path = [];
if (!options.forceFullResolution && d >= coarseThreshold) {
coarseRouteStats.attempted++;
const graph = coarseGraphFor(costField, mode);
const coarse = routeCoarsePath(start, target, graph, { heuristicWeight: mode === "rail" ? 0.72 : 0.86 });
if (coarse.length >= 2) {
const refined = refineCoarsePath([[start.x, start.y], ...coarse.slice(1, -1), [target.x, target.y]], costField, {
sea,
snapRadius: mode === "local" ? 2 : 1,
});
if (refined.length >= 4) {
path = refined;
coarseRouteStats.routed++;
coarseRouteStats.refined++;
} else {
coarseRouteStats.skipped++;
}
} else {
coarseRouteStats.skipped++;
}
}
if (!path.length) {
coarseRouteStats.fallback++;
path = traceCorridorByCost(
start,
(x, y) => Math.hypot(x - target.x, y - target.y) <= snap,
costField,
@ -1508,17 +1622,19 @@ export function generateMapFeatures(seed, terrain) {
penaltyStrength: options.penaltyStrength ?? (mode === "expressway" ? 1.9 : mode === "rail" ? 1.35 : mode === "national" ? 1.05 : 0.78),
minGoalDistance: Math.min(8, Math.max(2, d * 0.08)),
keepRegion: false,
maxExpanded: Math.min(SIZE, Math.max(1800, Math.floor(d * d * (mode === "expressway" ? 3.8 : mode === "rail" ? 4.6 : 5.2)))),
maxExpanded: Math.min(Math.floor(SIZE * SPEED_TOLERANCE), Math.max(1300, Math.floor(d * d * (mode === "expressway" ? 2.9 : mode === "rail" ? 3.5 : 3.9)))),
terrainFlowBias: options.terrainFlowBias ?? (mode === "expressway" ? 0.10 : mode === "rail" ? 0.12 : mode === "national" ? 0.24 : 0.30),
surfaceGrain: options.surfaceGrain ?? (mode === "national" ? 0.030 : mode === "local" ? 0.042 : 0.010),
bounds,
goalHint: options.goalHint || target,
heuristicWeight: options.heuristicWeight ?? (mode === "expressway" ? 0.66 : mode === "rail" ? 0.54 : mode === "national" ? 0.46 : 0.30),
}
);
);
}
if (path.length < 4) return [];
if (options.maxPathLength && pathLengthCells(path) > options.maxPathLength) return [];
const relaxed = relaxRouteToTerrain(path, costField, {
const skipRelax = options.skipRelax ?? (mode === "local" && path.length > 42);
const relaxed = skipRelax ? path : relaxRouteToTerrain(path, costField, {
radius: options.relaxRadius ?? (mode === "national" ? 2 : 1),
lineWeight: options.relaxLineWeight ?? (mode === "national" ? 0.30 : mode === "expressway" ? 0.24 : 0.48),
grain: options.surfaceGrain ?? 0.020,
@ -1695,12 +1811,77 @@ const premodernRoads = [];
const interchanges = [];
const externalGateways = [];
// Premodern roads connect castles/markets/ports sparsely.
for (const c of castles) {
const near = [...markets, ...ports, ...crossings].sort((a, b) => Math.hypot(a.x - c.x, a.y - c.y) - Math.hypot(b.x - c.x, b.y - c.y)).slice(0, 2);
for (const n of near) {
const path = routeLight(c, n, 2);
if (path.length > 2) premodernRoads.push(path);
function pathEntirelyLand(path) {
if (!path || path.length < 2) return false;
for (let k = 1; k < path.length; k++) {
const a = path[k - 1];
const b = path[k];
if (!a || !b) return false;
const steps = Math.max(1, Math.ceil(Math.hypot(b[0] - a[0], b[1] - a[1])));
for (let s = 0; s <= steps; s++) {
const t = s / steps;
const x = Math.round(a[0] + (b[0] - a[0]) * t);
const y = Math.round(a[1] + (b[1] - a[1]) * t);
if (!inside(x, y)) return false;
const i = indexOf(x, y);
if (sea[i] || transportFields.local[i] >= INF) return false;
}
}
return true;
}
function addPremodernRoad(a, b, edgeSet) {
if (!a || !b) return false;
const keyA = `${Math.round(a.x)},${Math.round(a.y)}`;
const keyB = `${Math.round(b.x)},${Math.round(b.y)}`;
const key = keyA < keyB ? `${keyA}|${keyB}` : `${keyB}|${keyA}`;
if (edgeSet.has(key)) return false;
edgeSet.add(key);
const d = Math.hypot(a.x - b.x, a.y - b.y);
if (d < 5 || d > 96) return false;
const path = routeLight(a, b, 2.4, transportFields.local);
if (path.length <= 2 || !pathEntirelyLand(path)) return false;
if (!routePhysicalAcceptable(path, 'local', { maxSeaRun: 0, maxSeaShare: 0, maxTunnelRun: 0, maxTunnelShare: 0 })) return false;
premodernRoads.push(path);
return true;
}
// Premodern roads are a loose land-only network around the labelled historical
// places (castles, market/village nodes, ports, crossings), not sea chords.
{
const edgeSet = new Set();
const labelledPremodernNodes = dedupePointCandidates([
...castles.map((p) => ({ ...p, premodernRole: "castle", premodernWeight: 1.35 })),
...castleTowns.map((p) => ({ ...p, premodernRole: "castle-town", premodernWeight: 1.25 })),
...markets.filter((p) => (p.population || 0) >= 2500).map((p) => ({ ...p, premodernRole: "market", premodernWeight: 1.0 + Math.min(0.45, (p.population || 0) / 60000) })),
...villages.filter((p) => (p.population || 0) >= 4200).map((p) => ({ ...p, premodernRole: "village", premodernWeight: 0.74 })),
...commercialPorts.map((p) => ({ ...p, premodernRole: "port", premodernWeight: p.portClass === "major" ? 1.22 : 1.0 })),
...crossings.map((p) => ({ ...p, premodernRole: "crossing", premodernWeight: 0.78 })),
].filter((p) => inside(p.x, p.y) && !sea[indexOf(p.x, p.y)]), 5.0)
.sort((a, b) => (b.premodernWeight || 0) - (a.premodernWeight || 0))
.slice(0, 54);
for (const c of castles) {
const near = labelledPremodernNodes
.filter((n) => n !== c && Math.hypot(n.x - c.x, n.y - c.y) <= 88)
.sort((a, b) => Math.hypot(a.x - c.x, a.y - c.y) - Math.hypot(b.x - c.x, b.y - c.y))
.slice(0, 2);
for (const n of near) addPremodernRoad(c, n, edgeSet);
}
for (const node of labelledPremodernNodes) {
if (premodernRoads.length >= 68) break;
const quota = node.premodernRole === "castle" || node.premodernRole === "castle-town" ? 3 : node.premodernRole === "market" ? 2 : 1;
const near = labelledPremodernNodes
.filter((n) => n !== node)
.map((n) => ({ n, d: Math.hypot(n.x - node.x, n.y - node.y), cost: Math.hypot(n.x - node.x, n.y - node.y) / Math.max(0.45, n.premodernWeight || 0.8) }))
.filter((e) => e.d >= 7 && e.d <= 82)
.sort((a, b) => a.cost - b.cost)
.slice(0, quota);
for (const { n } of near) {
if (premodernRoads.length >= 68) break;
addPremodernRoad(node, n, edgeSet);
}
}
}
@ -1750,10 +1931,12 @@ const premodernRoads = [];
transportFields, settlementDemand, preliminaryUrbanInfluence, preliminaryTownInfluence, preliminaryVillageInfluence,
modernCities, markets, ports, commercialPorts, externalGateways, geographicUrbanAnchors,
regionIdAt, routeBetweenTrafficCandidates, addCorridorInfluencePenalty, transportRouteAcceptable, pruneParallelSameMode, cachedInfluenceFromPaths,
speedTolerance: SPEED_TOLERANCE,
});
railways.push(...railOD.railways);
branchRailways.push(...railOD.branchRailways);
railODDebug = railOD.debug;
markFeatureTiming("rail-od");
const { transportDebugLayers, runLocalAccessPass, stitchRasterNearContacts, stitchLongLocalBranches, sanitizeLocalRoads, downgradeShortNationalRoads, connectAllRoadNetworksFinal } = buildDensityFlowRoadTransportSystem({
@ -1771,6 +1954,7 @@ const premodernRoads = [];
relaxRouteToTerrain, transportRouteAcceptable, repairTransportConnectivity,
repairDanglingTransportEndpoints, pruneDanglingTerminalSegments, pruneParallelSameMode,
});
markFeatureTiming("road-system");
// Land-use road influence intentionally excludes expressways. Expressways
// are through-corridors here, not automatic suburbanization generators.
@ -1798,9 +1982,9 @@ const premodernRoads = [];
return urbanDensity > 0.50 ? 6 : ruralDensity > 0.24 ? 12 : 20;
}
function shouldPlaceRailStation(x, y, i) {
const nearCity = (preliminaryUrbanInfluence[i] || 0) > 0.12 || distanceToNearest(modernCities, x, y) < 4.8;
const nearTown = (preliminaryTownInfluence[i] || 0) > 0.13 || distanceToNearest(markets, x, y) < 4.2;
const nearVillage = (preliminaryVillageInfluence[i] || 0) > 0.20 || distanceToNearest(villages, x, y) < 3.4;
const nearCity = (preliminaryUrbanInfluence[i] || 0) > 0.12 || cityIndex.hasWithin(x, y, 4.8);
const nearTown = (preliminaryTownInfluence[i] || 0) > 0.13 || marketIndex.hasWithin(x, y, 4.2);
const nearVillage = (preliminaryVillageInfluence[i] || 0) > 0.20 || villageIndex.hasWithin(x, y, 3.4);
const lowland = plain[i] > 0.16 || coastalLowland[i] > 0.16 || basinField[i] > 0.20 || valleyField[i] > 0.22;
const terrainOk = slope[i] < 0.42 && ridgeField[i] < 0.60 && elevation[i] < 0.76;
return terrainOk && lowland && (nearCity || nearTown || nearVillage);
@ -1822,6 +2006,7 @@ const premodernRoads = [];
}
const stationInfluence = influenceFromPoints(stations, 7, (s) => s.kind === "Major Station" ? 1.35 : 0.85);
const stationDensityInfluence = influenceFromPoints(stations, 10, (s) => s.kind === "Major Station" ? 1.85 : 1.05);
markFeatureTiming("transport-influence-stations");
// --- 5. Approximate city/town influence and land-use ---------------------
const cityInfluence = new Float32Array(SIZE);
@ -1921,7 +2106,7 @@ const premodernRoads = [];
slope[i] * 0.44 -
ridgeField[i] * 0.32
);
const nearMajorCity = modernCities.some((c) => Math.hypot(c.x - x, c.y - y) < 4);
const nearMajorCity = cityIndex.hasWithin(x, y, 4);
logisticsScore[i] = nearMajorCity ? 0 : flatAgriculturalCorridor;
}
}
@ -1950,9 +2135,9 @@ const premodernRoads = [];
return { ...p, score: (p.population || 1200) / 16000 + remoteness * 3.2 + ruralValue + (p.portClass ? 0.8 : 0) + (p.kind === "Logistics Park" ? 1.0 : 0) + transportFields.localPotential[i] };
})
.sort((a, b) => b.score - a.score)
.slice(0, 130);
.slice(0, 62);
const localPenalty = cachedInfluenceFromPaths(minorRoads, 4, "final-local:minor");
runLocalAccessPass({ candidates, accessInfluence, localPenalty, maxAdded: 90, maxLength: 92, debugMode: "local-access", from: "unserved", to: "network" });
runLocalAccessPass({ candidates, accessInfluence, localPenalty, maxAdded: 42, maxLength: 78, debugMode: "local-access", from: "unserved", to: "network" });
}
function addRuralRoadMeshConnectors() {
@ -1967,14 +2152,14 @@ const premodernRoads = [];
})
.filter((p) => p.score > 0.30)
.sort((a, b) => b.score - a.score)
.slice(0, 170);
.slice(0, 78);
runLocalAccessPass({
candidates,
accessInfluence: roadInfluenceNow,
localPenalty,
maxAdded: 70,
maxAdded: 32,
minSpacing: 3.0,
maxLength: 76,
maxLength: 64,
debugMode: "rural-mesh",
from: "rural-settlement",
to: "local-network",
@ -1990,7 +2175,8 @@ const premodernRoads = [];
transportDebugLayers.contactStitchesAfterConnectivity = stitchRasterNearContacts();
// Keep this as the final road topology operation. Later sanitization can cut
// the short connectors that intentionally merge isolated components.
transportDebugLayers.finalRoadNetworkConnectivity = connectAllRoadNetworksFinal(96);
transportDebugLayers.finalRoadNetworkConnectivity = connectAllRoadNetworksFinal(46);
markFeatureTiming("local-road-cleanup");
function dedupeTransportPathSet(paths, options = {}) {
const before = paths.length;
@ -2143,10 +2329,12 @@ const premodernRoads = [];
let added = 0;
for (const path of [...expressways, ...externalExpressways]) {
if (!path || path.length < 2) continue;
const endpoints = [path[0], path[path.length - 1]];
for (const [x, y] of endpoints) if (ensureInterchangePoint(x, y)) added++;
const first = path[0];
const last = path[path.length - 1];
if (ensureInterchangePoint(first[0], first[1], 'Terminal IC', 'expressway-terminal-endpoint')) added++;
if (ensureInterchangePoint(last[0], last[1], 'Terminal IC', 'expressway-terminal-endpoint')) added++;
}
return { added, total: interchanges.length };
return { added, total: interchanges.length, strategy: "terminal ICs restored" };
}
function ensureNationalRoadCoverageForTowns(minPopulation = 5000) {
@ -2167,7 +2355,7 @@ const premodernRoads = [];
.filter((row) => row.d >= 10 && row.d <= 90)
.sort((a, b) => a.d - b.d);
let addedPath = null;
for (const cand of candidates.slice(0, 8)) {
for (const cand of candidates.slice(0, 5)) {
const path = routeBetweenTrafficCandidates(town, cand.q, 'national', transportFields.national, nationalPenalty, {
curvePenalty: 0.055,
penaltyStrength: 0.60,
@ -2192,26 +2380,73 @@ const premodernRoads = [];
return debug;
}
function expresswayFringeAnchorForCity(city) {
if (!city || !inside(city.x, city.y)) return null;
const inner = Math.max(10, Math.round((city.coreRadius || 4) + 7));
const outer = Math.max(inner + 8, Math.round(Math.min(34, (city.urbanRadius || 13) * 2.0)));
let best = null;
for (let dy = -outer; dy <= outer; dy++) {
for (let dx = -outer; dx <= outer; dx++) {
const x = Math.round(city.x + dx);
const y = Math.round(city.y + dy);
if (!inside(x, y)) continue;
const d = Math.hypot(dx, dy);
if (d < inner || d > outer) continue;
const i = indexOf(x, y);
if (sea[i] || elevation[i] >= 0.70 || transportFields.expressway[i] >= INF) continue;
const radialBand = clamp(1 - Math.abs(d - (inner + outer) * 0.54) / Math.max(2, (outer - inner) * 0.55));
const corePenalty = clamp(settlementDemand[i] * 0.78 + preliminaryTownInfluence[i] * 0.58 + preliminaryVillageInfluence[i] * 0.42);
const score =
(transportFields.expresswayPotential[i] || 0) * 1.18 +
(urbanEdge[i] || 0) * 0.54 +
(logisticsPreSuitability[i] || 0) * 0.40 +
(plain[i] || 0) * 0.20 +
(basinField[i] || 0) * 0.12 +
radialBand * 0.32 -
corePenalty * 1.18 -
(slope[i] || 0) * 1.05 -
(ridgeField[i] || 0) * 0.82 +
hash2(x, y, seed + 24891 + city.x * 11 + city.y * 17) * 0.04;
if (!best || score > best.score) best = { x, y, score, city, population: city.population || 0, regionId: regionIdAt(x, y), role: 'expressway-fringe-anchor' };
}
}
return best;
}
function nearPath(path, p, radius) {
return (path || []).some(([x, y]) => Math.hypot(x - p.x, y - p.y) <= radius);
}
function ensureMajorCityExpresswayConnections(minPopulation = 100000) {
const majorCities = modernCities
.filter((c) => (c.population || 0) >= minPopulation)
.sort((a, b) => (b.population || 0) - (a.population || 0));
const debug = { minPopulation, cityCount: majorCities.length, added: 0, pairs: [] };
const debug = { minPopulation, cityCount: majorCities.length, added: 0, pairs: [], strategy: 'fringe-anchor-only' };
if (majorCities.length < 2) return debug;
const anchors = majorCities
.map((city) => expresswayFringeAnchorForCity(city))
.filter(Boolean)
.sort((a, b) => (b.population || 0) - (a.population || 0));
debug.anchorCount = anchors.length;
if (anchors.length < 2) return debug;
const permissiveExpresswayCost = new Float32Array(SIZE);
for (let i = 0; i < SIZE; i++) {
if (sea[i] || elevation[i] >= 0.70) {
permissiveExpresswayCost[i] = INF;
continue;
}
const urbanCore = clamp(settlementDemand[i] * 0.78 + preliminaryTownInfluence[i] * 0.62 + preliminaryVillageInfluence[i] * 0.46);
const terrainBase = Number.isFinite(transportFields.expressway[i]) && transportFields.expressway[i] < INF
? transportFields.expressway[i]
: 0.28 + Math.max(0, slope[i] - 0.18) * 0.9 + Math.max(0, elevation[i] - 0.58) * 1.6 + ridgeField[i] * 0.45;
permissiveExpresswayCost[i] = sea[i]
? 0.72 + (naturalBarrierScore?.[i] || 0) * 0.16
: terrainBase + Math.max(0, elevation[i] - 0.70) * 1.7;
: 0.40 + Math.max(0, slope[i] - 0.18) * 1.25 + Math.max(0, elevation[i] - 0.58) * 2.1 + ridgeField[i] * 0.80;
permissiveExpresswayCost[i] = terrainBase + urbanCore * 2.6 + Math.max(0, elevation[i] - 0.62) * 3.0;
}
const componentOfCities = () => {
const componentOfAnchors = () => {
const parent = new Map();
const keyOf = (city) => city.name || `${city.x},${city.y}`;
const keyOf = (anchor) => anchor.city?.name || `${anchor.x},${anchor.y}`;
function find(k) {
const p = parent.get(k);
if (p === k) return k;
@ -2223,74 +2458,70 @@ const premodernRoads = [];
const ra = find(a); const rb = find(b);
if (ra !== rb) parent.set(ra, rb);
}
for (const city of majorCities) parent.set(keyOf(city), keyOf(city));
const paths = [...expressways, ...externalExpressways];
for (const path of paths) {
const near = majorCities.filter((city) => path.some(([x, y]) => Math.hypot(city.x - x, city.y - y) <= 7.5));
for (const anchor of anchors) parent.set(keyOf(anchor), keyOf(anchor));
for (const path of [...expressways, ...externalExpressways]) {
const near = anchors.filter((anchor) => nearPath(path, anchor, 8.5));
if (near.length >= 2) {
const k0 = keyOf(near[0]);
for (let i = 1; i < near.length; i++) union(k0, keyOf(near[i]));
}
}
return new Map(majorCities.map((city) => [keyOf(city), find(keyOf(city))]));
return new Map(anchors.map((anchor) => [keyOf(anchor), find(keyOf(anchor))]));
};
const pairPriority = (a, b) => {
const d = Math.hypot(a.x - b.x, a.y - b.y);
const pop = Math.sqrt((a.population || minPopulation) * (b.population || minPopulation));
return d / Math.max(1, Math.log2(pop));
};
for (let iter = 0; iter < majorCities.length * 2; iter++) {
const comps = componentOfCities();
const keyOf = (anchor) => anchor.city?.name || `${anchor.x},${anchor.y}`;
for (let iter = 0; iter < anchors.length * 2; iter++) {
const comps = componentOfAnchors();
const reps = new Set(comps.values());
if (reps.size <= 1) break;
let best = null;
for (const a of majorCities) {
for (const b of majorCities) {
if (a === b) continue;
const ka = a.name || `${a.x},${a.y}`;
const kb = b.name || `${b.x},${b.y}`;
if (comps.get(ka) === comps.get(kb)) continue;
const score = pairPriority(a, b);
if (!best || score < best.score) best = { a, b, score, d: Math.hypot(a.x - b.x, a.y - b.y) };
for (const a of anchors) {
for (const b of anchors) {
if (a === b || comps.get(keyOf(a)) === comps.get(keyOf(b))) continue;
const d = Math.hypot(a.x - b.x, a.y - b.y);
if (d < 32 || d > 220) continue;
const pop = Math.sqrt((a.population || minPopulation) * (b.population || minPopulation));
const score = d / Math.max(1, Math.log2(pop)) - ((a.score || 0) + (b.score || 0)) * 0.18;
if (!best || score < best.score) best = { a, b, score, d };
}
}
if (!best) break;
let path = routeBetweenTrafficCandidates(best.a, best.b, 'expressway', permissiveExpresswayCost, null, {
curvePenalty: 0.045,
penaltyStrength: 0.18,
terrainFlowBias: 0.03,
surfaceGrain: 0.001,
relaxRadius: 2,
relaxLineWeight: 0.15,
maxPathLength: best.d * 3.5 + 110,
snapRadius: 4.0,
searchPad: Math.ceil(Math.max(20, Math.min(96, best.d * 0.55 + 16))),
heuristicWeight: 0.78,
maxSeaRun: 20,
maxTunnelRun: 20,
maxSeaShare: 0.45,
maxTunnelShare: 0.45,
curvePenalty: 0.125,
penaltyStrength: 0.95,
terrainFlowBias: 0.09,
surfaceGrain: 0.006,
relaxRadius: 1,
relaxLineWeight: 0.20,
maxPathLength: best.d * 2.70 + 84,
snapRadius: 2.0,
searchPad: Math.ceil(Math.max(42, Math.min(112, best.d * 0.58 + 12))),
heuristicWeight: 0.72,
maxSeaRun: 0,
maxTunnelRun: 10,
maxSeaShare: 0,
maxTunnelShare: 0.12,
});
if (!path.length) path = directBridgeTunnelConnector(best.a, best.b, 20);
if (path.length >= 2) {
path = smoothRasterPath(path, 2);
expressways.push(path);
debug.added++;
debug.pairs.push({ from: best.a.name, to: best.b.name, distance: Math.round(best.d), length: Math.round(pathLengthCells(path)) });
} else {
break;
if (path.length >= 4 && transportRouteAcceptable(path, 'expressway', transportFields.expresswayPotential, null, { minLength: 16, maxLength: best.d * 2.85 + 96, maxSeaRun: 0, maxTunnelRun: 10, maxSeaShare: 0, maxTunnelShare: 0.12 })) {
path = smoothRasterPath(path, 1);
if (transportRouteAcceptable(path, 'expressway', transportFields.expresswayPotential, null, { minLength: 16, maxLength: best.d * 2.95 + 104, maxSeaRun: 0, maxTunnelRun: 10, maxSeaShare: 0, maxTunnelShare: 0.12 })) {
expressways.push(path);
debug.added++;
debug.pairs.push({ from: best.a.city?.name, to: best.b.city?.name, distance: Math.round(best.d), length: Math.round(pathLengthCells(path)) });
continue;
}
}
break;
}
debug.finalExpresswayCount = expressways.length;
return debug;
}
transportDebugLayers.postConnectivityNationalCoverage = ensureNationalRoadCoverageForTowns(5000);
transportDebugLayers.postConnectivityMajorCityExpresswayGuarantee = ensureMajorCityExpresswayConnections(100000);
transportDebugLayers.postConnectivityNationalCoverage = ensureNationalRoadCoverageForTowns(6000);
transportDebugLayers.postConnectivityMajorCityExpresswayGuarantee = ensureMajorCityExpresswayConnections(110000);
transportDebugLayers.postConnectivityExpresswayDedup = dedupeTransportPathSet(expressways, { minLength: 8, sampleStep: 2 });
transportDebugLayers.postConnectivityExpresswayEndpointICs = ensureExpresswayEndpointsHaveICs();
markFeatureTiming("post-connectivity-guarantees");
const { landuse, populationDensity } = buildFeatureLanduse({
seed,
elevation, slope, sea, river, floodplain, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland,
@ -2299,8 +2530,11 @@ const premodernRoads = [];
roadLanduseInfluence, roadInfluence, roadDensityInfluence, railInfluence2, stationInfluence, stationDensityInfluence, villageInfluence,
cityInfluence, coreInfluence, oldTownInfluence, townInfluence, industrialInfluence, logisticsInfluence,
});
markFeatureTiming("landuse");
const transportDebug = {
humanStageVersion: "v2-sparse-raster",
featureTimings,
coarseRouting: coarseRouteStats,
aStarRoutes: 0,
regionalNodeCount: [...regionStats.keys()].reduce((sum, regionId) => sum + importantNodesForRegion(regionId).length, 0),
fieldCorridorTransport: false,