tweeaaaaaaaaak
This commit is contained in:
parent
32398c13e8
commit
47af930e18
8 changed files with 446 additions and 136 deletions
484
mapGenerator.js
484
mapGenerator.js
|
|
@ -1,4 +1,4 @@
|
|||
import { CUSTOM_NAMES, KIND_SUFFIXES, NAME_PARTS } from "./names.js";
|
||||
import { CUSTOM_NAMES, NAME_PARTS } from "./names.js";
|
||||
|
||||
export const MAP_W = 172;
|
||||
export const MAP_H = 122;
|
||||
|
|
@ -90,6 +90,16 @@ function neighbors8(x, y) {
|
|||
return out;
|
||||
}
|
||||
|
||||
function neighbors4(x, y) {
|
||||
const out = [];
|
||||
for (const [dx, dy] of [[1, 0], [-1, 0], [0, 1], [0, -1]]) {
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
if (inside(nx, ny)) out.push([nx, ny, 1]);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function distanceToNearest(points, x, y, fallback = 999) {
|
||||
let best = fallback;
|
||||
for (const p of points) best = Math.min(best, Math.hypot(p.x - x, p.y - y));
|
||||
|
|
@ -663,7 +673,7 @@ function tagInsidePrefecture(points, prefectureMask) {
|
|||
return points.map((p) => ({ ...p, insidePrefecture: Boolean(prefectureMask[indexOf(p.x, p.y)]) }));
|
||||
}
|
||||
|
||||
function defaultName(seed, id, kind) {
|
||||
function defaultName(seed, id) {
|
||||
const prefixKey = id.split("-")[0];
|
||||
const n = Number(id.split("-")[1] || 0);
|
||||
const prefixes = NAME_PARTS.prefixes || [""];
|
||||
|
|
@ -673,8 +683,7 @@ function defaultName(seed, id, kind) {
|
|||
const useInfix = hash2(n + prefixKey.length * 17, seed + n * 31, seed + 2777) >= 0.7;
|
||||
const infix = useInfix ? infixes[(seed * 3 + n * 11) % infixes.length] : "";
|
||||
const suffixWord = suffixes[(seed * 5 + n * 13 + prefixKey.length) % suffixes.length];
|
||||
const kindSuffix = KIND_SUFFIXES[prefixKey] || kind || "";
|
||||
return `${prefix}${infix}${suffixWord}${kindSuffix}`;
|
||||
return `${prefix}${infix}${suffixWord}`;
|
||||
}
|
||||
|
||||
function attachIdsAndNames(points, prefix, seed, kindOverride = null) {
|
||||
|
|
@ -684,7 +693,7 @@ function attachIdsAndNames(points, prefix, seed, kindOverride = null) {
|
|||
return {
|
||||
...p,
|
||||
id,
|
||||
name: CUSTOM_NAMES[id] || defaultName(seed + prefix.length * 1000, id, kind),
|
||||
name: CUSTOM_NAMES[id] || defaultName(seed + prefix.length * 1000, id),
|
||||
insidePrefecture: Boolean(p.insidePrefecture),
|
||||
};
|
||||
});
|
||||
|
|
@ -720,12 +729,15 @@ function generateAdminRegions(centers, prefectureMask, sea, elevation, slope, ri
|
|||
|
||||
const ridgeBarrier = Math.max(ridgeField[ni], ridgeField[curIndex]);
|
||||
const riverBarrier = Math.max(river[ni], river[curIndex]);
|
||||
const ridgePenalty = Math.max(0, elevation[ni] - 0.40) * 9.4 + Math.abs(elevation[ni] - elevation[curIndex]) * 8.8 + ridgeBarrier * 14.2;
|
||||
const slopePenalty = slope[ni] * 7.8;
|
||||
const riverPenalty = riverBarrier > 0.7 ? 13.0 : riverBarrier > 0.42 ? 8.2 : riverBarrier > 0.22 ? 3.8 : 0;
|
||||
const urbanContinuityBonus = (landuse[ni] >= 2 && landuse[ni] <= 4 && populationDensity[ni] > 0.20) ? 1.95 : 0;
|
||||
const valleyLocalityBonus = valleyField[ni] * 0.03;
|
||||
const stepCost = Math.max(0.25, 0.72 + ridgePenalty + slopePenalty + riverPenalty - valleyLocalityBonus - urbanContinuityBonus) * step;
|
||||
const highDivide = Math.max(elevation[ni], elevation[curIndex]);
|
||||
const watershedBarrier = ridgeBarrier * (27.0 + Math.max(0, highDivide - 0.46) * 46.0);
|
||||
const ridgePenalty = Math.max(0, highDivide - 0.36) * 16.0 + Math.abs(elevation[ni] - elevation[curIndex]) * 12.4 + watershedBarrier;
|
||||
const slopePenalty = slope[ni] * 10.6;
|
||||
const valleyBarrier = valleyField[ni] > 0.50 ? valleyField[ni] * (riverBarrier > 0.16 ? 7.2 : 2.6) : 0;
|
||||
const riverPenalty = riverBarrier > 0.7 ? 22.0 : riverBarrier > 0.42 ? 14.8 : riverBarrier > 0.22 ? 7.4 : riverBarrier > 0.12 ? 2.2 : 0;
|
||||
const urbanContinuityBonus = (landuse[ni] >= 2 && landuse[ni] <= 4 && populationDensity[ni] > 0.20) ? 1.65 : 0;
|
||||
const valleyLocalityBonus = valleyField[ni] * 0.16;
|
||||
const stepCost = Math.max(0.25, 0.72 + ridgePenalty + slopePenalty + riverPenalty + valleyBarrier - valleyLocalityBonus - urbanContinuityBonus) * step;
|
||||
const nextDist = dist[curIndex] + stepCost;
|
||||
|
||||
if (nextDist < dist[ni]) {
|
||||
|
|
@ -741,11 +753,11 @@ function generateAdminRegions(centers, prefectureMask, sea, elevation, slope, ri
|
|||
|
||||
function terrainBoundaryStrength(i, elevation, slope, river, ridgeField, valleyField) {
|
||||
return clamp(
|
||||
ridgeField[i] * 2.75 +
|
||||
river[i] * 2.05 +
|
||||
slope[i] * 1.18 +
|
||||
Math.max(0, elevation[i] - 0.5) * 1.05 -
|
||||
valleyField[i] * 0.10
|
||||
ridgeField[i] * 3.15 +
|
||||
river[i] * 2.45 +
|
||||
valleyField[i] * 0.62 +
|
||||
slope[i] * 1.06 +
|
||||
Math.max(0, elevation[i] - 0.5) * 1.18
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -764,7 +776,7 @@ function smoothAdminRegionsTerrainAware(adminId, prefectureMask, sea, elevation,
|
|||
|
||||
const counts = new Map();
|
||||
let ownCount = 0;
|
||||
for (const [nx, ny] of neighbors8(x, y)) {
|
||||
for (const [nx, ny] of neighbors4(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!prefectureMask[ni] || sea[ni]) continue;
|
||||
const id = current[ni];
|
||||
|
|
@ -880,6 +892,192 @@ function mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function removeMunicipalExclaves(adminId, prefectureMask, sea, adminCenters = [], protectedPoints = [], maxIslandCells = 220) {
|
||||
const protectedByAdmin = new Map();
|
||||
for (const p of [...adminCenters, ...protectedPoints]) {
|
||||
if (!p || !inside(p.x, p.y)) continue;
|
||||
const id = adminId[indexOf(p.x, p.y)];
|
||||
if (id < 0) continue;
|
||||
if (!protectedByAdmin.has(id)) protectedByAdmin.set(id, new Set());
|
||||
protectedByAdmin.get(id).add(indexOf(p.x, p.y));
|
||||
}
|
||||
|
||||
const ids = new Set();
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i] && adminId[i] >= 0) ids.add(adminId[i]);
|
||||
|
||||
const globalSeen = new Uint8Array(SIZE);
|
||||
const queue = [];
|
||||
for (const id of ids) {
|
||||
const components = [];
|
||||
for (let i = 0; i < SIZE; i++) {
|
||||
if (globalSeen[i] || adminId[i] !== id || !prefectureMask[i] || sea[i]) continue;
|
||||
const comp = [];
|
||||
let hasProtected = protectedByAdmin.get(id)?.has(i) || false;
|
||||
queue.length = 0;
|
||||
queue.push(i);
|
||||
globalSeen[i] = 1;
|
||||
for (let q = 0; q < queue.length; q++) {
|
||||
const cur = queue[q];
|
||||
comp.push(cur);
|
||||
if (protectedByAdmin.get(id)?.has(cur)) hasProtected = true;
|
||||
const [x, y] = xyOf(cur);
|
||||
for (const [nx, ny] of neighbors4(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (globalSeen[ni] || adminId[ni] !== id || !prefectureMask[ni] || sea[ni]) continue;
|
||||
globalSeen[ni] = 1;
|
||||
queue.push(ni);
|
||||
}
|
||||
}
|
||||
components.push({ cells: comp, hasProtected });
|
||||
}
|
||||
if (components.length <= 1) continue;
|
||||
components.sort((a, b) => (b.hasProtected ? 1000000 : 0) + b.cells.length - ((a.hasProtected ? 1000000 : 0) + a.cells.length));
|
||||
const keep = new Set(components[0].cells);
|
||||
for (const component of components.slice(1)) {
|
||||
const mainSize = components[0].cells.length;
|
||||
if (component.hasProtected && component.cells.length > maxIslandCells && component.cells.length > mainSize * 0.42) continue;
|
||||
if (component.cells.length > maxIslandCells && component.cells.length > mainSize * 0.36) continue;
|
||||
const counts = new Map();
|
||||
for (const ci of component.cells) {
|
||||
const [x, y] = xyOf(ci);
|
||||
for (const [nx, ny] of neighbors4(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!prefectureMask[ni] || sea[ni]) continue;
|
||||
const other = adminId[ni];
|
||||
if (other >= 0 && other !== id) counts.set(other, (counts.get(other) || 0) + 1);
|
||||
}
|
||||
}
|
||||
let target = -1;
|
||||
let best = -1;
|
||||
for (const [other, count] of counts) if (count > best) { best = count; target = other; }
|
||||
if (target >= 0) for (const ci of component.cells) adminId[ci] = target;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse, passes = 6) {
|
||||
let current = new Int16Array(adminId);
|
||||
for (let pass = 0; pass < passes; pass++) {
|
||||
const next = new Int16Array(current);
|
||||
for (let y = 1; y < MAP_H - 1; y++) {
|
||||
for (let x = 1; x < MAP_W - 1; x++) {
|
||||
const i = indexOf(x, y);
|
||||
const own = current[i];
|
||||
if (!prefectureMask[i] || sea[i] || own < 0) continue;
|
||||
|
||||
const urbanCell = (landuse[i] >= 2 && landuse[i] <= 4) || landuse[i] === 7 || landuse[i] === 8 || populationDensity[i] > 0.24;
|
||||
if (urbanCell) continue;
|
||||
|
||||
let isBoundary = false;
|
||||
const counts = new Map([[own, 0]]);
|
||||
for (const [nx, ny] of neighbors8(x, y)) {
|
||||
const ni = indexOf(nx, ny);
|
||||
if (!prefectureMask[ni] || sea[ni]) continue;
|
||||
const id = current[ni];
|
||||
if (id < 0) continue;
|
||||
if (id !== own) isBoundary = true;
|
||||
const terrain = terrainBoundaryStrength(ni, elevation, slope, river, ridgeField, valleyField);
|
||||
const weight = terrain > 0.60 ? 0.45 : 1.0;
|
||||
counts.set(id, (counts.get(id) || 0) + weight);
|
||||
}
|
||||
if (!isBoundary) continue;
|
||||
|
||||
let localBarrier = terrainBoundaryStrength(i, elevation, slope, river, ridgeField, valleyField);
|
||||
for (let dy = -1; dy <= 1; dy++) {
|
||||
for (let dx = -1; dx <= 1; dx++) {
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
if (!inside(nx, ny)) continue;
|
||||
localBarrier = Math.max(localBarrier, terrainBoundaryStrength(indexOf(nx, ny), elevation, slope, river, ridgeField, valleyField));
|
||||
}
|
||||
}
|
||||
if (localBarrier > 0.50) continue;
|
||||
|
||||
let bestId = own;
|
||||
let best = counts.get(own) || 0;
|
||||
for (const [id, score] of counts) {
|
||||
if (id === own) continue;
|
||||
const adjusted = score + (populationDensity[i] < 0.12 ? 0.42 : 0) + (plainnessForBoundary(elevation, slope, ridgeField, i) ? 0.24 : 0);
|
||||
if (adjusted > best + 0.65) {
|
||||
best = adjusted;
|
||||
bestId = id;
|
||||
}
|
||||
}
|
||||
if (bestId !== own) next[i] = bestId;
|
||||
}
|
||||
}
|
||||
current = next;
|
||||
}
|
||||
adminId.set(current);
|
||||
}
|
||||
|
||||
function plainnessForBoundary(elevation, slope, ridgeField, i) {
|
||||
return elevation[i] < 0.58 && slope[i] < 0.26 && ridgeField[i] < 0.34;
|
||||
}
|
||||
|
||||
function recalculatePopulationAfterLanduse(modernCities, satelliteCities, populationDensity, landuse, prefectureMask, sea, stationInfluence, roadInfluence, railInfluence) {
|
||||
populationDensity.fill(0);
|
||||
const allCities = [...modernCities, ...satelliteCities];
|
||||
for (const city of allCities) {
|
||||
const urbanR = Math.max(4, city.urbanRadius || 8);
|
||||
const coreR = Math.max(2, city.coreRadius || 3);
|
||||
const popScale = clamp((Math.log10(Math.max(12000, city.population || 12000)) - 4) / 2.25, 0.16, 1.65);
|
||||
const r = Math.ceil(urbanR * 2.2);
|
||||
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] || !prefectureMask[i]) continue;
|
||||
const d = Math.hypot(dx, dy);
|
||||
const lu = landuse[i];
|
||||
const landuseWeight = lu === 3 ? 1.85 : lu === 2 ? 1.42 : lu === 4 ? 1.05 : lu === 7 ? 0.82 : lu === 8 ? 0.68 : 0.10;
|
||||
const radial = 1 / (1 + Math.pow(d / urbanR, 2.5));
|
||||
const core = Math.exp(-(d * d) / (coreR * coreR * 2.0));
|
||||
const transit = Math.max(stationInfluence?.[i] || 0, (railInfluence?.[i] || 0) * 0.55, (roadInfluence?.[i] || 0) * 0.24);
|
||||
populationDensity[i] += popScale * landuseWeight * (radial * 0.78 + core * 0.38 + transit * 0.18);
|
||||
}
|
||||
}
|
||||
}
|
||||
let maxDensity = 0;
|
||||
for (let i = 0; i < SIZE; i++) if (prefectureMask[i] && !sea[i]) maxDensity = Math.max(maxDensity, populationDensity[i]);
|
||||
if (maxDensity > 0) for (let i = 0; i < SIZE; i++) populationDensity[i] = clamp(populationDensity[i] / maxDensity);
|
||||
|
||||
for (const city of allCities) {
|
||||
let urbanCells = 0;
|
||||
let coreCells = 0;
|
||||
let densitySum = 0;
|
||||
const r = Math.ceil((city.urbanRadius || 8) * 2.0);
|
||||
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 (!prefectureMask[i] || sea[i]) continue;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d > r) continue;
|
||||
const lu = landuse[i];
|
||||
if (lu >= 2 && lu <= 8) {
|
||||
urbanCells++;
|
||||
densitySum += populationDensity[i];
|
||||
if (lu === 3) coreCells++;
|
||||
}
|
||||
}
|
||||
}
|
||||
const base = city.isPrefecturalCapital ? 90000 : city.kind === "Satellite City" ? 16000 : 32000;
|
||||
const urbanComponent = urbanCells * (city.isPrefecturalCapital ? 1500 : city.kind === "Satellite City" ? 900 : 1200);
|
||||
const coreComponent = coreCells * 3200;
|
||||
const densityComponent = densitySum * 650;
|
||||
city.population = Math.round((base + urbanComponent + coreComponent + densityComponent) / 1000) * 1000;
|
||||
city.urbanRadius = clamp(5.0 + Math.sqrt(city.population) / 95, city.kind === "Satellite City" ? 5 : 7, city.isPrefecturalCapital ? 34 : 28);
|
||||
city.coreRadius = clamp(1.8 + Math.sqrt(city.population) / 360, 2.2, 9);
|
||||
}
|
||||
}
|
||||
|
||||
export function generateMap(seedInput = 114514) {
|
||||
const seed = Number(seedInput) >>> 0;
|
||||
|
||||
|
|
@ -1495,8 +1693,7 @@ export function generateMap(seedInput = 114514) {
|
|||
const d = Math.hypot(dx, dy);
|
||||
if (d > radius || d === 0) continue;
|
||||
const weight = (radius + 0.35 - d) / (radius + 0.35);
|
||||
const highRidgeGuard = clamp((elevation[ni] - 0.68) / 0.18) * clamp(ridgeField[ni] * 1.2);
|
||||
const carve = Math.max(0, weight) * (0.008 + r * 0.026) * Math.max(0.45, slope[ni] + 0.22) * (1 - highRidgeGuard * 0.72);
|
||||
const carve = Math.max(0, weight) * (0.008 + r * 0.026) * Math.max(0.45, slope[ni] + 0.22);
|
||||
fluvialElevation[ni] = clamp(fluvialElevation[ni] - carve, seaLevel + 0.005, 1);
|
||||
erosionField[ni] = clamp(erosionField[ni] + carve * 3.0);
|
||||
valleyField[ni] = clamp(valleyField[ni] + carve * 12.0);
|
||||
|
|
@ -1523,6 +1720,57 @@ export function generateMap(seedInput = 114514) {
|
|||
}
|
||||
|
||||
elevation.set(fluvialElevation);
|
||||
|
||||
// Broad alluvial/coastal/basin plains. The plain score alone is not enough;
|
||||
// the elevation surface must also be locally calm, otherwise every lowland
|
||||
// still reads as rugged terrain. Smooth only low, wet depositional cells and
|
||||
// leave ridges/headwaters untouched.
|
||||
for (let pass = 0; pass < 4; pass++) {
|
||||
const nextElevation = new Float32Array(elevation);
|
||||
for (let y = 2; y < MAP_H - 2; y++) {
|
||||
for (let x = 2; x < MAP_W - 2; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
const lowland = clamp(
|
||||
coastalLowland[i] * 0.72 +
|
||||
basinField[i] * 0.54 +
|
||||
valleyField[i] * 0.34 +
|
||||
Math.pow(flowAccum[i], 0.58) * 0.24 -
|
||||
ridgeField[i] * 0.62 -
|
||||
Math.max(0, elevation[i] - 0.54) * 1.65 -
|
||||
slope[i] * 0.74
|
||||
);
|
||||
if (lowland <= 0.12) continue;
|
||||
let sum = 0;
|
||||
let weight = 0;
|
||||
for (let dy = -2; dy <= 2; dy++) {
|
||||
for (let dx = -2; dx <= 2; dx++) {
|
||||
const nx = x + dx;
|
||||
const ny = y + dy;
|
||||
const ni = indexOf(nx, ny);
|
||||
if (sea[ni]) continue;
|
||||
const d = Math.hypot(dx, dy);
|
||||
if (d > 2.25) continue;
|
||||
const compatible = clamp(1 - Math.abs(elevation[ni] - elevation[i]) / 0.11);
|
||||
const w = compatible / (1 + d);
|
||||
sum += elevation[ni] * w;
|
||||
weight += w;
|
||||
}
|
||||
}
|
||||
if (weight <= 0) continue;
|
||||
const localMean = sum / weight;
|
||||
const terrace = Math.round(localMean * 42) / 42;
|
||||
const target = lerp(localMean, terrace, 0.28);
|
||||
nextElevation[i] = clamp(lerp(elevation[i], target, lowland * 0.42), seaLevel + 0.006, 1);
|
||||
if (lowland > 0.55) {
|
||||
depositionField[i] = clamp(depositionField[i] + lowland * 0.018);
|
||||
erosionField[i] = Math.max(0, erosionField[i] - lowland * 0.012);
|
||||
}
|
||||
}
|
||||
}
|
||||
elevation.set(nextElevation);
|
||||
}
|
||||
|
||||
for (let y = 1; y < MAP_H - 1; y++) {
|
||||
for (let x = 1; x < MAP_W - 1; x++) {
|
||||
const i = indexOf(x, y);
|
||||
|
|
@ -1871,12 +2119,47 @@ export function generateMap(seedInput = 114514) {
|
|||
for (const p of near) addPremodernRoad(market, p);
|
||||
}
|
||||
|
||||
function urbanSiteSuitability(p) {
|
||||
const i = indexOf(p.x, p.y);
|
||||
if (sea[i]) return 0;
|
||||
const portBonus = p.kind === "Port Town" || p.portClass === "major" || p.portClass === "regional" ? 0.18 : 0;
|
||||
const historicalBonus = p.kind === "Market City" || p.kind === "Castle Town" ? 0.05 : 0;
|
||||
return clamp(
|
||||
plain[i] * 0.46 +
|
||||
agriculture[i] * 0.18 +
|
||||
basinField[i] * 0.20 +
|
||||
coastalLowland[i] * 0.20 +
|
||||
valleyField[i] * 0.12 +
|
||||
portBonus + historicalBonus -
|
||||
slope[i] * 0.58 -
|
||||
ridgeField[i] * 0.34 -
|
||||
Math.max(0, elevation[i] - 0.55) * 1.35
|
||||
);
|
||||
}
|
||||
|
||||
function cityPopulationCap(p) {
|
||||
const i = indexOf(p.x, p.y);
|
||||
const suitability = urbanSiteSuitability(p);
|
||||
if (suitability < 0.18 || elevation[i] > 0.66 || slope[i] > 0.82 || ridgeField[i] > 0.72) return 85000;
|
||||
if (suitability < 0.28 || elevation[i] > 0.60 || slope[i] > 0.62) return 180000;
|
||||
if (suitability < 0.38) return 420000;
|
||||
return INF;
|
||||
}
|
||||
|
||||
let castleTowns = castles.map((c) => ({ x: c.x, y: c.y, score: c.score + 0.45, kind: "Castle Town" }));
|
||||
const cityCandidates = [
|
||||
...castleTowns.map((p) => ({ ...p, score: p.score + 0.4 })),
|
||||
...ports.map((p) => ({ ...p, kind: "Port Town", score: p.score + 0.28 })),
|
||||
...markets.map((p) => ({ ...p, kind: "Market City", score: p.score + 0.12 })),
|
||||
];
|
||||
].map((p) => {
|
||||
const i = indexOf(p.x, p.y);
|
||||
const suitability = urbanSiteSuitability(p);
|
||||
return {
|
||||
...p,
|
||||
urbanSuitability: suitability,
|
||||
score: p.score + suitability * 0.72 - slope[i] * 0.20 - ridgeField[i] * 0.16 - Math.max(0, elevation[i] - 0.58) * 0.78,
|
||||
};
|
||||
}).filter((p) => p.urbanSuitability >= 0.10 || p.kind === "Castle Town");
|
||||
|
||||
let modernCities = pickEntities(cityCandidates, {
|
||||
max: 7 + Math.floor(rand(seed, 1061) * 10),
|
||||
|
|
@ -1890,30 +2173,78 @@ export function generateMap(seedInput = 114514) {
|
|||
const rankBase = rank === "Prefectural Capital" ? 420000 : rank === "Regional Center" ? 115000 : 26000;
|
||||
const rankSpread = rank === "Prefectural Capital" ? 1450000 : rank === "Regional Center" ? 520000 : 185000;
|
||||
const pi = indexOf(p.x, p.y);
|
||||
const geographyBoost = clamp(plain[pi] * 0.34 + agriculture[pi] * 0.18 + basinField[pi] * 0.2 + coastalLowland[pi] * 0.18 + valleyField[pi] * 0.12 + (p.kind === "Port Town" ? 0.22 : 0));
|
||||
const population = Math.round((rankBase + rankSpread * Math.pow(rawScale + geographyBoost * 0.18, 1.75)) / 1000) * 1000;
|
||||
const suitability = p.urbanSuitability ?? urbanSiteSuitability(p);
|
||||
const geographyBoost = clamp(plain[pi] * 0.34 + agriculture[pi] * 0.18 + basinField[pi] * 0.2 + coastalLowland[pi] * 0.18 + valleyField[pi] * 0.12 + suitability * 0.24 + (p.kind === "Port Town" ? 0.22 : 0));
|
||||
const rawPopulation = Math.round((rankBase + rankSpread * Math.pow(rawScale + geographyBoost * 0.18, 1.75)) / 1000) * 1000;
|
||||
const population = Math.min(rawPopulation, cityPopulationCap(p));
|
||||
const urbanRadius = clamp(7.5 + Math.sqrt(population) / 80 + (rank === "Prefectural Capital" ? 3.0 : rank === "Regional Center" ? 1.5 : 0), 8, 32);
|
||||
const coreRadius = clamp(2.6 + Math.sqrt(population) / 320, 3, 9);
|
||||
const urbanWeight = clamp(0.74 + Math.log10(Math.max(10000, population)) * 0.36, 1.15, 3.05);
|
||||
return { ...p, population, urbanRadius, coreRadius, urbanWeight, rank, kind: p.kind || "City" };
|
||||
});
|
||||
|
||||
function fallbackCapitalCandidate() {
|
||||
const pools = [...markets, ...ports, ...villages].filter((p) => p && prefectureMask[indexOf(p.x, p.y)] && !sea[indexOf(p.x, p.y)]);
|
||||
let best = null;
|
||||
let bestScore = -INF;
|
||||
for (const p of pools) {
|
||||
const i = indexOf(p.x, p.y);
|
||||
const score = urbanSiteSuitability(p) * 1.6 + plain[i] * 0.32 + populationDensityProxyForCapital(i) + (p.kind?.includes("Port") ? 0.18 : 0) + (p.score || 0);
|
||||
if (score > bestScore) { bestScore = score; best = p; }
|
||||
}
|
||||
if (best) return { ...best, kind: "Market City", population: 360000, urbanRadius: 15, coreRadius: 4.6, urbanWeight: 1.9, score: bestScore };
|
||||
|
||||
for (let y = 4; y < MAP_H - 4; y++) {
|
||||
for (let x = 4; x < MAP_W - 4; x++) {
|
||||
const i = indexOf(x, y);
|
||||
if (!prefectureMask[i] || sea[i]) continue;
|
||||
const score = plain[i] * 0.72 + agriculture[i] * 0.24 + basinField[i] * 0.18 + coastalLowland[i] * 0.14 - slope[i] * 0.72 - ridgeField[i] * 0.32;
|
||||
if (score > bestScore) { bestScore = score; best = { x, y, score, kind: "Market City" }; }
|
||||
}
|
||||
}
|
||||
return best ? { ...best, population: 320000, urbanRadius: 14, coreRadius: 4.2, urbanWeight: 1.7 } : null;
|
||||
}
|
||||
|
||||
function populationDensityProxyForCapital(i) {
|
||||
return settlementScore[i] * 0.18 + marketScore[i] * 0.12;
|
||||
}
|
||||
|
||||
if (modernCities.length === 0 || !modernCities.some((city) => prefectureMask[indexOf(city.x, city.y)])) {
|
||||
const fallbackCapital = fallbackCapitalCandidate();
|
||||
if (fallbackCapital) modernCities.unshift(fallbackCapital);
|
||||
}
|
||||
|
||||
if (modernCities.length > 0) {
|
||||
modernCities.sort((a, b) => (b.population || 0) + b.score * 90000 - ((a.population || 0) + a.score * 90000));
|
||||
let capitalIndex = -1;
|
||||
let capitalScore = -INF;
|
||||
for (let i = 0; i < modernCities.length; i++) {
|
||||
const city = modernCities[i];
|
||||
const ci = indexOf(city.x, city.y);
|
||||
if (!prefectureMask[ci] || sea[ci]) continue;
|
||||
const suitability = urbanSiteSuitability(city);
|
||||
const score = suitability * 900000 + (city.population || 0) * 0.55 + (city.score || 0) * 120000 - slope[ci] * 180000 - Math.max(0, elevation[ci] - 0.58) * 360000;
|
||||
if (score > capitalScore) { capitalScore = score; capitalIndex = i; }
|
||||
}
|
||||
if (capitalIndex > 0) modernCities.unshift(modernCities.splice(capitalIndex, 1)[0]);
|
||||
const capCell = indexOf(modernCities[0].x, modernCities[0].y);
|
||||
const capPopulation = prefectureMask[capCell]
|
||||
? Math.max(modernCities[0].population || 0, 620000)
|
||||
: Math.min(modernCities[0].population || 0, 180000);
|
||||
modernCities[0] = {
|
||||
...modernCities[0],
|
||||
rank: "Prefectural Capital",
|
||||
kind: "Prefectural Capital",
|
||||
isPrefecturalCapital: true,
|
||||
population: Math.max(modernCities[0].population || 0, 620000),
|
||||
urbanRadius: Math.max(modernCities[0].urbanRadius || 0, 18),
|
||||
coreRadius: Math.max(modernCities[0].coreRadius || 0, 5.5),
|
||||
urbanWeight: Math.max(modernCities[0].urbanWeight || 0, 2.15),
|
||||
rank: prefectureMask[capCell] ? "Prefectural Capital" : "Regional Center",
|
||||
kind: prefectureMask[capCell] ? "Prefectural Capital" : (modernCities[0].kind || "City"),
|
||||
isPrefecturalCapital: Boolean(prefectureMask[capCell]),
|
||||
population: capPopulation,
|
||||
urbanRadius: prefectureMask[capCell] ? Math.max(modernCities[0].urbanRadius || 0, 18) : modernCities[0].urbanRadius,
|
||||
coreRadius: prefectureMask[capCell] ? Math.max(modernCities[0].coreRadius || 0, 5.5) : modernCities[0].coreRadius,
|
||||
urbanWeight: prefectureMask[capCell] ? Math.max(modernCities[0].urbanWeight || 0, 2.15) : modernCities[0].urbanWeight,
|
||||
};
|
||||
for (let i = 1; i < modernCities.length; i++) modernCities[i] = { ...modernCities[i], isPrefecturalCapital: false };
|
||||
}
|
||||
|
||||
const capital = modernCities[0] || markets[0] || ports[0] || { x: Math.floor(MAP_W / 2), y: Math.floor(MAP_H / 2), score: 1, population: 0, urbanRadius: 12, coreRadius: 4, urbanWeight: 1, isPrefecturalCapital: true };
|
||||
const capital = modernCities.find((city) => city.isPrefecturalCapital && prefectureMask[indexOf(city.x, city.y)]) || modernCities.find((city) => prefectureMask[indexOf(city.x, city.y)]) || markets.find((p) => prefectureMask[indexOf(p.x, p.y)]) || ports.find((p) => prefectureMask[indexOf(p.x, p.y)]) || { x: Math.floor(MAP_W / 2), y: Math.floor(MAP_H / 2), score: 1, population: 0, urbanRadius: 12, coreRadius: 4, urbanWeight: 1, isPrefecturalCapital: true };
|
||||
|
||||
const populationDensity = new Float32Array(SIZE);
|
||||
let maxPopulationDensity = 0;
|
||||
|
|
@ -1964,19 +2295,19 @@ export function generateMap(seedInput = 114514) {
|
|||
const i = indexOf(x, y);
|
||||
const e = elevation[i];
|
||||
const s = slope[i];
|
||||
const pass = nearPassPoint(x, y, type === "express" ? 7 : 5);
|
||||
if (e > 0.86) return INF;
|
||||
if (pass && e > 0.82 && s > 0.16) return INF;
|
||||
if (!pass && e > 0.80) return INF;
|
||||
if (!pass && e > 0.72 && s > 0.18) return INF;
|
||||
if (!pass && e > 0.68 && s > 0.32) return INF;
|
||||
if (!pass && e > 0.74) return type === "express" ? 175 : 215;
|
||||
if (!pass && e > 0.66 && s > 0.22) return type === "express" ? 105 : 135;
|
||||
const passDiscount = pass ? 0.18 : 1;
|
||||
const mountain = Math.max(0, e - 0.50);
|
||||
const steep = Math.max(0, s - 0.18);
|
||||
const typeFactor = type === "express" ? 260 : type === "rail" ? 320 : 120;
|
||||
return (mountain * mountain * typeFactor + steep * steep * 120) * passDiscount;
|
||||
const pass = nearPassPoint(x, y, type === "express" ? 7 : type === "rail" ? 6 : 5);
|
||||
if (e > 0.84) return INF;
|
||||
if (pass && e > 0.80 && s > 0.16) return INF;
|
||||
if (!pass && e > 0.78) return INF;
|
||||
if (!pass && e > 0.70 && s > 0.16) return INF;
|
||||
if (!pass && e > 0.66 && s > 0.28) return INF;
|
||||
if (!pass && e > 0.72) return type === "express" ? 260 : type === "rail" ? 330 : type === "minor" ? 80 : 155;
|
||||
if (!pass && e > 0.64 && s > 0.20) return type === "express" ? 145 : type === "rail" ? 180 : type === "minor" ? 54 : 96;
|
||||
const passDiscount = pass ? (type === "minor" ? 0.35 : 0.22) : 1;
|
||||
const mountain = Math.max(0, e - 0.48);
|
||||
const steep = Math.max(0, s - 0.15);
|
||||
const typeFactor = type === "express" ? 360 : type === "rail" ? 430 : type === "minor" ? 115 : 210;
|
||||
return (mountain * mountain * typeFactor + steep * steep * 150 + ridgeField[i] * 9.5) * passDiscount;
|
||||
}
|
||||
|
||||
function transportAccessPoint(node, mode = "road", salt = 0) {
|
||||
|
|
@ -1994,7 +2325,7 @@ export function generateMap(seedInput = 114514) {
|
|||
if (!inside(x, y)) continue;
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) continue;
|
||||
const barrier = mode === "express" || mode === "rail" ? mountainBarrierPenalty(x, y, mode) : 0;
|
||||
const barrier = mode === "express" || mode === "rail" ? mountainBarrierPenalty(x, y, mode) : mountainBarrierPenalty(x, y, "road");
|
||||
if (barrier >= INF) continue;
|
||||
const targetD = (minR + maxR) * 0.5;
|
||||
const flatness = plain[i] * 1.0 + agriculture[i] * 0.2 + valleyField[i] * 0.26 + coastalLowland[i] * 0.16 - slope[i] * 1.22 - ridgeField[i] * 0.72 - Math.max(0, elevation[i] - 0.58) * 2.35;
|
||||
|
|
@ -2127,9 +2458,11 @@ export function generateMap(seedInput = 114514) {
|
|||
function roadCost(x, y) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) return INF;
|
||||
const barrier = mountainBarrierPenalty(x, y, "road");
|
||||
if (barrier >= INF) return INF;
|
||||
const density = densityValue(x, y);
|
||||
const nodeAvoid = distanceToNearest(townAvoidNodes, x, y) < 2.2 ? 2.0 : 0;
|
||||
return Math.max(0.35, 1 + slope[i] * 15.8 + Math.max(0, elevation[i] - 0.56) * 2.6 + nodeAvoid + (river[i] > 0.45 ? 0.85 : 0) + floodplain[i] * 0.22 - density * 0.50 - plain[i] * 0.18 - valleyField[i] * 0.24 - coastalLowland[i] * 0.18 + ridgeField[i] * 0.90 + normalEdgePenalty(x, y) + hash2(x, y, seed + 333) * 0.08);
|
||||
return Math.max(0.35, 1 + slope[i] * 17.8 + barrier + Math.max(0, elevation[i] - 0.54) * 9.2 + nodeAvoid + (river[i] > 0.45 ? 0.85 : 0) + floodplain[i] * 0.22 - density * 0.50 - plain[i] * 0.22 - valleyField[i] * 0.28 - coastalLowland[i] * 0.20 + ridgeField[i] * 1.15 + normalEdgePenalty(x, y) + hash2(x, y, seed + 333) * 0.08);
|
||||
}
|
||||
|
||||
function expresswayCost(x, y) {
|
||||
|
|
@ -2255,7 +2588,7 @@ export function generateMap(seedInput = 114514) {
|
|||
if (!inside(x, y)) continue;
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i] || !prefectureMask[i]) continue;
|
||||
const barrier = mode === "road" ? 0 : mountainBarrierPenalty(x, y, mode === "express" ? "express" : "rail");
|
||||
const barrier = mode === "road" ? mountainBarrierPenalty(x, y, "road") : mountainBarrierPenalty(x, y, mode === "express" ? "express" : "rail");
|
||||
if (barrier >= INF) continue;
|
||||
const density = densityValue(x, y);
|
||||
const densityTerm = mode === "rail" ? density * 0.75 : mode === "express" ? midDensityAffinity(x, y) * 0.72 : density * 0.28 + midDensityAffinity(x, y) * 0.22;
|
||||
|
|
@ -2287,15 +2620,17 @@ export function generateMap(seedInput = 114514) {
|
|||
|
||||
function softRingRailCost(x, y) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i] || elevation[i] > 0.82) return INF;
|
||||
const barrier = mountainBarrierPenalty(x, y, "rail");
|
||||
if (sea[i] || barrier >= INF) return INF;
|
||||
const density = densityValue(x, y);
|
||||
return Math.max(0.38, 1 + slope[i] * 12 + Math.max(0, elevation[i] - 0.58) * 16 + (river[i] > 0.5 ? 1.3 : river[i] * 0.6) - density * 0.62 - plain[i] * 0.18 + normalEdgePenalty(x, y) + hash2(x, y, seed + 7222) * 0.05);
|
||||
return Math.max(0.38, 1 + slope[i] * 14 + barrier + Math.max(0, elevation[i] - 0.56) * 22 + (river[i] > 0.5 ? 1.3 : river[i] * 0.6) - density * 0.62 - plain[i] * 0.20 + normalEdgePenalty(x, y) + hash2(x, y, seed + 7222) * 0.05);
|
||||
}
|
||||
|
||||
function softRingExpressCost(x, y) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i] || elevation[i] > 0.82) return INF;
|
||||
return Math.max(0.38, 1 + slope[i] * 11 + Math.max(0, elevation[i] - 0.6) * 14 + (river[i] > 0.5 ? 1.0 : river[i] * 0.5) - midDensityAffinity(x, y) * 0.42 - plain[i] * 0.12 + normalEdgePenalty(x, y) + hash2(x, y, seed + 7444) * 0.05);
|
||||
const barrier = mountainBarrierPenalty(x, y, "express");
|
||||
if (sea[i] || barrier >= INF) return INF;
|
||||
return Math.max(0.38, 1 + slope[i] * 13 + barrier + Math.max(0, elevation[i] - 0.58) * 20 + (river[i] > 0.5 ? 1.0 : river[i] * 0.5) - midDensityAffinity(x, y) * 0.42 - plain[i] * 0.14 + normalEdgePenalty(x, y) + hash2(x, y, seed + 7444) * 0.05);
|
||||
}
|
||||
|
||||
function addEnvironmentalRing(city, mode, bucket, baseCost, existingPaths, targetRadius) {
|
||||
|
|
@ -2371,18 +2706,18 @@ export function generateMap(seedInput = 114514) {
|
|||
const expressRadius = roadRadius + 3 + rand(seed, city.x * 71 + city.y * 73) * 3;
|
||||
const railRadius = Math.max(8, roadRadius - 4);
|
||||
addEnvironmentalRing(city, "road", ringRoads, roadCost, [...nationalRoads, ...expressways, ...railways, ...branchRailways], roadRadius);
|
||||
const expressRingSegments = addEnvironmentalRing(city, "express", ringExpressways, expresswayCost, [...expressways, ...nationalRoads, ...railways, ...branchRailways], expressRadius);
|
||||
// Expressway rings are intentionally disabled; expressways stay as sparse interurban corridors.
|
||||
const railRingSegments = addEnvironmentalRing(city, "rail", ringRailways, railCost, [...railways, ...branchRailways, ...nationalRoads, ...expressways], railRadius);
|
||||
// Expressway rings should be rare; do not force a fallback ring when terrain rejects it.
|
||||
if (railRingSegments === 0) addLooseEnvironmentalRing(city, ringRailways, softRingRailCost, railRadius);
|
||||
}
|
||||
compactPathArray(ringExpressways, { minLength: 10, maxOverlap: 0.22, maxCount: 2 });
|
||||
ringExpressways.length = 0;
|
||||
compactPathArray(ringRoads, { minLength: 8, maxOverlap: 0.32, maxCount: 18 });
|
||||
compactPathArray(ringRailways, { minLength: 8, maxOverlap: 0.26, maxCount: 8 });
|
||||
|
||||
const gatewayCandidates = [];
|
||||
for (let x = 0; x < MAP_W; x++) for (const y of [0, MAP_H - 1]) { const i = indexOf(x, y); if (!sea[i]) gatewayCandidates.push({ x, y, side: y === 0 ? "N" : "S", score: plain[i] + agriculture[i] + (1 - slope[i]) * 0.5 }); }
|
||||
for (let y = 0; y < MAP_H; y++) for (const x of [0, MAP_W - 1]) { const i = indexOf(x, y); if (!sea[i]) gatewayCandidates.push({ x, y, side: x === 0 ? "W" : "E", score: plain[i] + agriculture[i] + (1 - slope[i]) * 0.5 }); }
|
||||
for (let x = 0; x < MAP_W; x++) for (const y of [0, MAP_H - 1]) { const i = indexOf(x, y); if (!sea[i]) gatewayCandidates.push({ x, y, side: y === 0 ? "N" : "S", score: plain[i] + agriculture[i] + (1 - slope[i]) * 0.5 + coastalLowland[i] * 0.2 - Math.max(0, elevation[i] - 0.56) * 1.6 - ridgeField[i] * 0.35 }); }
|
||||
for (let y = 0; y < MAP_H; y++) for (const x of [0, MAP_W - 1]) { const i = indexOf(x, y); if (!sea[i]) gatewayCandidates.push({ x, y, side: x === 0 ? "W" : "E", score: plain[i] + agriculture[i] + (1 - slope[i]) * 0.5 + coastalLowland[i] * 0.2 - Math.max(0, elevation[i] - 0.56) * 1.6 - ridgeField[i] * 0.35 }); }
|
||||
|
||||
let externalGateways = pickEntities(gatewayCandidates, {
|
||||
max: 2 + Math.floor(rand(seed, 1201) * 3),
|
||||
|
|
@ -2395,10 +2730,12 @@ export function generateMap(seedInput = 114514) {
|
|||
return (x, y) => {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) return INF;
|
||||
const barrier = mountainBarrierPenalty(x, y, "road");
|
||||
if (barrier >= INF) return INF;
|
||||
const borderPenalty = nearMapEdge(x, y, 1) && !(Math.abs(x - goal.x) <= 2 && Math.abs(y - goal.y) <= 2) ? 7 : nearMapEdge(x, y, 3) ? 1.5 : 0;
|
||||
const density = densityValue(x, y);
|
||||
const nodeAvoid = distanceToNearest(townAvoidNodes, x, y) < 2.2 ? 2.0 : 0;
|
||||
return Math.max(0.35, 1 + slope[i] * 10 + nodeAvoid + (river[i] > 0.45 ? 0.9 : 0) + floodplain[i] * 0.24 - density * 0.3 - plain[i] * 0.22 + borderPenalty + hash2(x, y, seed + 333) * 0.06);
|
||||
return Math.max(0.35, 1 + slope[i] * 13 + barrier + Math.max(0, elevation[i] - 0.54) * 7.5 + nodeAvoid + (river[i] > 0.45 ? 0.9 : 0) + floodplain[i] * 0.24 - density * 0.3 - plain[i] * 0.24 + borderPenalty + hash2(x, y, seed + 333) * 0.06);
|
||||
};
|
||||
}
|
||||
function externalExpresswayCost(goal) {
|
||||
|
|
@ -2476,13 +2813,13 @@ export function generateMap(seedInput = 114514) {
|
|||
if (paths[i].some(([x, y]) => elevation[indexOf(x, y)] > threshold)) paths.splice(i, 1);
|
||||
}
|
||||
}
|
||||
for (const paths of [railways, branchRailways, ringRailways, externalRailways, expressways, ringExpressways, externalExpressways]) pruneHighMountainTransport(paths, 0.82);
|
||||
for (const paths of [railways, branchRailways, ringRailways, externalRailways, expressways, externalExpressways]) pruneHighMountainTransport(paths, 0.82);
|
||||
|
||||
const expressInfluence = influenceFromPaths([...expressways, ...externalExpressways, ...ringExpressways], 6);
|
||||
const roadInfluence = influenceFromPaths([...nationalRoads, ...ringRoads, ...expressways, ...ringExpressways, ...externalRoads, ...externalExpressways], 4);
|
||||
const expressInfluence = influenceFromPaths([...expressways, ...externalExpressways], 6);
|
||||
const roadInfluence = influenceFromPaths([...nationalRoads, ...ringRoads, ...expressways, ...externalRoads, ...externalExpressways], 4);
|
||||
|
||||
const icCandidates = [];
|
||||
for (const path of [...expressways, ...ringExpressways, ...externalExpressways]) {
|
||||
for (const path of [...expressways, ...externalExpressways]) {
|
||||
icCandidates.push(...samplePath(path, 11 + Math.floor(rand(seed, path.length + 333) * 5)).map((p) => ({ ...p, score: 0.62 + plain[indexOf(p.x, p.y)] * 0.24 + midDensityAffinity(p.x, p.y) * 0.16, kind: "Interchange" })));
|
||||
for (const city of modernCities) {
|
||||
let best = null;
|
||||
|
|
@ -2599,8 +2936,10 @@ export function generateMap(seedInput = 114514) {
|
|||
|
||||
function minorRoadCost(x, y) {
|
||||
const i = indexOf(x, y);
|
||||
if (sea[i]) return INF;
|
||||
return Math.max(0.3, 1 + slope[i] * 7.2 + floodplain[i] * 0.18 + (river[i] > 0.5 ? 1.0 : 0.18 * river[i]) - plain[i] * 0.22 - valleyField[i] * 0.34 - coastalLowland[i] * 0.12 + ridgeField[i] * 0.4 - roadNetInfluence[i] * 0.35 + normalEdgePenalty(x, y) + hash2(x, y, seed + 555) * 0.15);
|
||||
if (sea[i] || elevation[i] > 0.72) return INF;
|
||||
const barrier = mountainBarrierPenalty(x, y, "minor");
|
||||
if (barrier >= INF) return INF;
|
||||
return Math.max(0.3, 1 + slope[i] * 8.4 + barrier * 0.55 + Math.max(0, elevation[i] - 0.58) * 4.4 + floodplain[i] * 0.18 + (river[i] > 0.5 ? 1.0 : 0.18 * river[i]) - plain[i] * 0.24 - valleyField[i] * 0.36 - coastalLowland[i] * 0.12 + ridgeField[i] * 0.58 - roadNetInfluence[i] * 0.35 + normalEdgePenalty(x, y) + hash2(x, y, seed + 555) * 0.15);
|
||||
}
|
||||
|
||||
const connectedPairs = new Set();
|
||||
|
|
@ -2831,6 +3170,7 @@ export function generateMap(seedInput = 114514) {
|
|||
adminCentersRaw.push(...extra.filter((p) => adminCentersRaw.every((q) => Math.hypot(p.x - q.x, p.y - q.y) >= 6)));
|
||||
}
|
||||
const adminId = generateAdminRegions(adminCentersRaw, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse);
|
||||
smoothAdminRegionsTerrainAware(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse, 7);
|
||||
|
||||
function lockUrbanClusterToMunicipality(city, radius, allowSuburban = true) {
|
||||
if (!city || !prefectureMask[indexOf(city.x, city.y)]) return;
|
||||
|
|
@ -2887,7 +3227,27 @@ export function generateMap(seedInput = 114514) {
|
|||
}
|
||||
lockSmallUrbanComponentsToMunicipality(adminId, prefectureMask, sea, landuse, populationDensity, 520);
|
||||
lockSmallUrbanComponentsToMunicipality(adminId, prefectureMask, sea, landuse, populationDensity, 620);
|
||||
mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, [...modernCities, ...satelliteCities], 260);
|
||||
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...satelliteCities], 180);
|
||||
snapAdminBoundariesToTerrain(adminId, prefectureMask, sea, elevation, slope, river, ridgeField, valleyField, populationDensity, landuse, 5);
|
||||
removeMunicipalExclaves(adminId, prefectureMask, sea, adminCentersRaw, [...modernCities, ...satelliteCities], 360);
|
||||
mergeTinyMunicipalities(adminId, prefectureMask, sea, populationDensity, [...modernCities, ...satelliteCities], 220);
|
||||
const adminBorders = extractAdminBorderSegments(adminId, prefectureMask);
|
||||
|
||||
// Final population pass after land-use cleanup, satellite municipality locking, and isolated urban deletion.
|
||||
// This keeps population figures proportional to the actually rendered urbanized area.
|
||||
recalculatePopulationAfterLanduse(modernCities, satelliteCities, populationDensity, landuse, prefectureMask, sea, stationInfluence, roadInfluence, railInfluence2);
|
||||
for (const city of modernCities) {
|
||||
if (city.isPrefecturalCapital) continue;
|
||||
const cap = cityPopulationCap(city);
|
||||
if (cap < INF && (city.population || 0) > cap) {
|
||||
city.population = Math.round(cap / 1000) * 1000;
|
||||
city.urbanRadius = clamp(5.0 + Math.sqrt(city.population) / 100, 6, 16);
|
||||
city.coreRadius = clamp(1.8 + Math.sqrt(city.population) / 400, 2.2, 5.2);
|
||||
city.urbanWeight = clamp(0.72 + Math.log10(Math.max(10000, city.population)) * 0.30, 1.0, 2.0);
|
||||
}
|
||||
}
|
||||
|
||||
function makeHarborWorks(ports) {
|
||||
const out = [];
|
||||
for (const port of ports) {
|
||||
|
|
@ -2984,7 +3344,7 @@ export function generateMap(seedInput = 114514) {
|
|||
premodernRoads,
|
||||
minorRoads,
|
||||
modernCities,
|
||||
prefecturalCapital: modernCities.find((city) => city.isPrefecturalCapital) || modernCities[0] || null,
|
||||
prefecturalCapital: modernCities.find((city) => city.isPrefecturalCapital && prefectureMask[indexOf(city.x, city.y)]) || null,
|
||||
totalPopulation: [...modernCities, ...satelliteCities].reduce((sum, city) => sum + (city.population || 0), 0),
|
||||
populationDensity,
|
||||
railways,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue