splittintting
This commit is contained in:
parent
b17be0e0d2
commit
d2e7a80e72
11 changed files with 1341 additions and 1349 deletions
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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue