tweeaaaaaaaaak

This commit is contained in:
33333-33333 2026-05-20 15:02:37 +09:00
commit 47af930e18
8 changed files with 446 additions and 136 deletions

View file

@ -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,