map/mapOutput.js

761 lines
31 KiB
JavaScript

import { createNameDebug } from "./names.js";
import { CELL_SIZE, INF, MAP_H, MAP_W, clamp, indexOf, inside, rand } from "./mapUtils.js";
import { aStar, applyOutputOptions, attachIdsAndNames, recalculatePopulationAfterLanduse, tagInsidePrefecture } from "./mapGeneratorHelpers.js";
export function finishMapOutput({
seed,
options,
terrainTemplate,
seaLevel,
cityPopulationCap,
stationInfluence,
roadInfluence,
railInfluence2,
elevation,
moisture,
slope,
sea,
ocean,
lake,
river,
floodplain,
plain,
agriculture,
settlementCluster,
ridgeField,
valleyField,
basinField,
coastalLowland,
flowAccum,
erosionField,
depositionField,
arcSpineField,
branchRidgeField,
depositionalLowland,
alluvialFanField,
deltaField,
naturalBarrierScore,
villages,
ports,
crossings,
passes,
markets,
castles,
castleTowns,
premodernRoads,
minorRoads,
modernCities,
populationDensity,
railways,
branchRailways,
ringRailways,
externalRailways,
stations,
industrialZones,
nationalRoads,
ringRoads,
expressways,
ringExpressways,
icAccessRoads,
externalRoads,
externalExpressways,
interchanges,
logisticsParks,
satelliteCities,
newTowns,
landuse,
adminCentersRaw,
adminId,
adminBorders,
adminDebug,
riverPaths,
mainRivers,
tributaryRivers,
smallStreams,
externalGateways,
transportDebug,
prefectureMask,
prefectureBorder,
prefectureRegionId,
regionalDebug,
terrainDebug,
regionalPrefectureBorders,
}) {
// 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) {
const parts = [];
const limit = port.portClass === "major" ? 5 : port.portClass === "regional" ? 3 : 1;
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1],[1,1],[-1,1],[1,-1],[-1,-1]]) {
const sx = port.x + dx;
const sy = port.y + dy;
if (!inside(sx, sy) || !sea[indexOf(sx, sy)]) continue;
parts.push([[port.x, port.y], [sx, sy]]);
const wx = sx + dx;
const wy = sy + dy;
if (port.portClass === "major" && inside(wx, wy) && sea[indexOf(wx, wy)] && rand(seed, sx * 101 + sy * 103) > 0.22) parts.push([[sx, sy], [wx, wy]]);
if (parts.length >= limit) break;
}
if (parts.length) out.push({ port, segments: parts, kind: port.portClass === "major" ? "Major Harbor Works" : "Harbor Works" });
}
return out;
}
function generatedNameStem(fullName) {
return String(fullName || "").replace(/[都道府県市町村区]$/u, "");
}
function terrainSettlementScore(x, y, sideBias = null) {
const i = indexOf(x, y);
if (!inside(x, y) || sea[i]) return -INF;
const edgeBias = sideBias === "north" ? (MAP_H - y) / MAP_H
: sideBias === "south" ? y / MAP_H
: sideBias === "west" ? (MAP_W - x) / MAP_W
: sideBias === "east" ? x / MAP_W
: 0;
return settlementCluster[i] * 0.55 + plain[i] * 0.42 + agriculture[i] * 0.22 + basinField[i] * 0.20 + coastalLowland[i] * 0.20 + valleyField[i] * 0.16 + edgeBias * 0.08 - slope[i] * 0.54 - ridgeField[i] * 0.22 - Math.max(0, elevation[i] - 0.62) * 1.25;
}
function chooseSpacedPoints(candidates, count, minDistance, seedOffset = 0) {
const selected = [];
const ordered = candidates
.map((p, n) => ({ ...p, score: (p.score || 0) + rand(seed, seedOffset + n * 17 + p.x * 5 + p.y * 7) * 0.06 }))
.sort((a, b) => b.score - a.score);
for (const p of ordered) {
if (selected.some((q) => Math.hypot(q.x - p.x, q.y - p.y) < minDistance)) continue;
selected.push(p);
if (selected.length >= count) break;
}
return selected;
}
function labelCollisionScore(x, y, avoidPoints) {
let nearest = 99;
for (const p of avoidPoints) {
if (!p) continue;
const d = Math.hypot(x - p.x, y - p.y);
nearest = Math.min(nearest, d);
}
return nearest;
}
const MAX_BRIDGE_CELLS = 7;
function pointPair(p) {
if (Array.isArray(p)) return [p[0], p[1]];
return [p.x, p.y];
}
function sampledWaterRunBetween(a, b) {
if (!a || !b) return 0;
const [ax, ay] = pointPair(a);
const [bx, by] = pointPair(b);
const steps = Math.max(1, Math.ceil(Math.hypot(bx - ax, by - ay) * 1.6));
let run = 0;
let maxRun = 0;
for (let k = 0; k <= steps; k++) {
const t = k / steps;
const x = clamp(Math.round(ax + (bx - ax) * t), 0, MAP_W - 1);
const y = clamp(Math.round(ay + (by - ay) * t), 0, MAP_H - 1);
const isWater = sea[indexOf(x, y)];
if (isWater) {
run++;
maxRun = Math.max(maxRun, run);
} else {
run = 0;
}
}
return maxRun;
}
function pathMaxWaterRun(path) {
if (!path || path.length < 2) return 0;
let maxRun = 0;
for (let k = 1; k < path.length; k++) {
maxRun = Math.max(maxRun, sampledWaterRunBetween(path[k - 1], path[k]));
}
return maxRun;
}
function landDetourCost(x, y) {
if (!inside(x, y)) return INF;
const i = indexOf(x, y);
if (sea[i]) return INF;
return Math.max(
0.35,
1 +
slope[i] * 7.6 +
Math.max(0, elevation[i] - 0.58) * 10.5 +
ridgeField[i] * 2.2 -
plain[i] * 0.45 -
valleyField[i] * 0.54 -
coastalLowland[i] * 0.38
);
}
function compactOutputPath(path) {
const out = [];
let last = "";
for (const p of path || []) {
const [x, y] = pointPair(p);
const key = `${x},${y}`;
if (key === last) continue;
last = key;
out.push([x, y]);
}
return out;
}
function repairLongBridgeSegments(path) {
const compact = compactOutputPath(path);
if (compact.length < 2) return compact.length >= 3 ? compact : [];
const out = [compact[0]];
for (let k = 1; k < compact.length; k++) {
const from = out[out.length - 1];
const to = compact[k];
if (sampledWaterRunBetween(from, to) <= MAX_BRIDGE_CELLS) {
out.push(to);
continue;
}
const detour = compactOutputPath(aStar({ x: from[0], y: from[1] }, { x: to[0], y: to[1] }, landDetourCost));
if (detour.length >= 2 && pathMaxWaterRun(detour) <= MAX_BRIDGE_CELLS) {
out.push(...detour.slice(1));
} else {
return [];
}
}
const repaired = compactOutputPath(out);
return repaired.length >= 3 ? repaired : [];
}
function enforceBridgeLimitList(paths) {
return (paths || [])
.map((path) => pathMaxWaterRun(path) > MAX_BRIDGE_CELLS ? repairLongBridgeSegments(path) : compactOutputPath(path))
.filter((path) => path && path.length >= 3 && pathMaxWaterRun(path) <= MAX_BRIDGE_CELLS);
}
function shoreContactScore(x, y) {
let contacts = 0;
for (let dy = -2; dy <= 2; dy++) {
for (let dx = -2; dx <= 2; dx++) {
if (!dx && !dy) continue;
const nx = x + dx;
const ny = y + dy;
if (!inside(nx, ny)) continue;
if (!sea[indexOf(nx, ny)]) contacts++;
}
}
return contacts;
}
function labelCandidateScore(x, y, avoidPoints, fallback, capital, seedOffset, preferSea = false) {
const i = indexOf(x, y);
const nearest = labelCollisionScore(x, y, avoidPoints);
const capD = capital ? Math.hypot(x - capital.x, y - capital.y) : 24;
const fallbackD = fallback ? Math.hypot(x - fallback.x, y - fallback.y) : 0;
const terrainBias = sea[i]
? 8 + Math.min(18, shoreContactScore(x, y)) * 0.45
: plain[i] * 0.18 + basinField[i] * 0.10 + coastalLowland[i] * 0.10 - slope[i] * 0.08;
const clearance = Math.min(nearest, 24) * 1.55 - Math.max(0, 8 - nearest) * 5.0;
const capitalBias = capital ? -Math.max(0, capD - (preferSea ? 26 : 45)) * 0.09 : 0;
return clearance + terrainBias + capitalBias - fallbackD * 0.012 + rand(seed, seedOffset + x * 17 + y * 19) * 0.42;
}
function pickPrefectureLabelPosition(avoidPoints, fallback, options = {}) {
const {
capital = null,
landPredicate = (x, y, i) => prefectureMask[i] && !sea[i],
areaPredicate = null,
seedOffset = 7461,
} = options;
let best = null;
let bestScore = -INF;
// Prefer open water near the prefectural capital when it reads like a coastal prefecture label.
if (capital) {
const rMax = 30;
for (let dy = -rMax; dy <= rMax; dy += 2) {
for (let dx = -rMax; dx <= rMax; dx += 2) {
const x = clamp(Math.round(capital.x + dx), 0, MAP_W - 1);
const y = clamp(Math.round(capital.y + dy), 0, MAP_H - 1);
if (x < 6 || y < 6 || x > MAP_W - 7 || y > MAP_H - 7) continue;
const i = indexOf(x, y);
if (!sea[i]) continue;
if (areaPredicate && !areaPredicate(x, y)) continue;
const capD = Math.hypot(x - capital.x, y - capital.y);
if (capD < 5 || capD > rMax) continue;
const coastTouch = shoreContactScore(x, y);
if (coastTouch < 3 || coastTouch > 20) continue;
const score = labelCandidateScore(x, y, avoidPoints, fallback, capital, seedOffset + 3000, true) + coastTouch * 0.35;
if (score > bestScore) { bestScore = score; best = { x, y, placement: "sea" }; }
}
}
if (best && bestScore >= 18) return best;
}
best = null;
bestScore = -INF;
for (let y = 6; y < MAP_H - 6; y += 3) {
for (let x = 6; x < MAP_W - 6; x += 3) {
const i = indexOf(x, y);
if (!landPredicate(x, y, i)) continue;
const score = labelCandidateScore(x, y, avoidPoints, fallback, capital, seedOffset, false);
if (score > bestScore) { bestScore = score; best = { x, y, placement: "land" }; }
}
}
if (!best || bestScore < 10) return fallback;
return best;
}
function looseOutsidePath(a, b, salt, keepOutside = true) {
if (!a || !b) return [];
const steps = Math.max(4, Math.ceil(Math.max(Math.abs(a.x - b.x), Math.abs(a.y - b.y))));
const path = [];
let lastKey = "";
const bend = (rand(seed, 7600 + salt) - 0.5) * 5.5;
const sideways = Math.abs(a.x - b.x) > Math.abs(a.y - b.y) ? "y" : "x";
for (let k = 0; k <= steps; k++) {
const t = k / steps;
let x = Math.round(a.x + (b.x - a.x) * t);
let y = Math.round(a.y + (b.y - a.y) * t);
const wave = Math.sin(t * Math.PI) * bend;
if (sideways === "y") y = Math.round(y + wave);
else x = Math.round(x + wave);
x = clamp(x, 0, MAP_W - 1);
y = clamp(y, 0, MAP_H - 1);
const i = indexOf(x, y);
if (sea[i]) continue;
if (keepOutside && prefectureMask[i]) continue;
const key = `${x},${y}`;
if (key === lastKey) continue;
lastKey = key;
path.push([x, y]);
}
return path.length >= 3 ? repairLongBridgeSegments(path) : [];
}
function generatePrefectureIdentity(usedNamesForIdentity, avoidPoints) {
const stemsA = ["青", "白", "黒", "高", "奥", "新", "東", "西", "南", "北", "中", "美", "豊", "若", "真", "清", "瑞", "長", "久", "安", "阿", "葛", "榛", "碓", "那", "鹿", "宇", "志", "遠", "羽"];
const stemsB = ["森", "川", "野", "原", "沢", "島", "浦", "浜", "海", "山", "岳", "谷", "津", "崎", "里", "畑", "橋", "瀬", "井", "沼", "丘", "郷", "城", "坂", "泊", "戸", "湊", "庄"];
const suffixes = ["県", "県", "県", "県", "県", "府"];
const localUsed = new Set(usedNamesForIdentity || []);
function pickGeneratedName(offset) {
const a = stemsA[Math.floor(rand(seed, 7410 + offset * 11) * stemsA.length) % stemsA.length];
const b = stemsB[Math.floor(rand(seed, 7420 + offset * 13) * stemsB.length) % stemsB.length];
const c = suffixes[Math.floor(rand(seed, 7430 + offset * 17) * suffixes.length) % suffixes.length];
return `${a}${b}${c}`;
}
function pickUniqueName(offset, preferred = null) {
let candidate = preferred || pickGeneratedName(offset);
let guard = 0;
while (localUsed.has(candidate) && guard++ < 24) candidate = pickGeneratedName(offset + guard + 3);
localUsed.add(candidate);
if (usedNamesForIdentity) usedNamesForIdentity.add(candidate);
return candidate;
}
const capital = modernCities.find((city) => city.isPrefecturalCapital && prefectureMask[indexOf(city.x, city.y)]);
const capStem = generatedNameStem(capital?.name);
const prefectureSuffix = suffixes[Math.floor(rand(seed, 7433) * suffixes.length) % suffixes.length];
const capitalBased = capStem && rand(seed, 7440) < 0.80 ? `${capStem}${prefectureSuffix}` : null;
const name = pickUniqueName(0, capitalBased);
let sx = 0, sy = 0, n = 0;
for (let y = 0; y < MAP_H; y++) {
for (let x = 0; x < MAP_W; x++) {
const i = indexOf(x, y);
if (!prefectureMask[i] || sea[i]) continue;
sx += x; sy += y; n++;
}
}
const fallback = { x: n ? sx / n : MAP_W / 2, y: n ? sy / n : MAP_H / 2 };
const labelPoint = pickPrefectureLabelPosition(avoidPoints, fallback, {
capital,
landPredicate: (x, y, i) => prefectureMask[i] && !sea[i],
seedOffset: 7461,
});
const label = { name, x: labelPoint.x, y: labelPoint.y, kind: "Prefecture Label", placement: labelPoint.placement || "land" };
const sideSamples = { north: [], south: [], west: [], east: [] };
for (let x = 2; x < MAP_W - 2; x += 4) {
for (let y = 0; y < Math.min(13, MAP_H); y++) if (inside(x, y) && !prefectureMask[indexOf(x, y)] && !sea[indexOf(x, y)]) { sideSamples.north.push({ x, y: 3 }); break; }
for (let y = MAP_H - 1; y >= Math.max(0, MAP_H - 13); y--) if (inside(x, y) && !prefectureMask[indexOf(x, y)] && !sea[indexOf(x, y)]) { sideSamples.south.push({ x, y: MAP_H - 4 }); break; }
}
for (let y = 2; y < MAP_H - 2; y += 4) {
for (let x = 0; x < Math.min(13, MAP_W); x++) if (inside(x, y) && !prefectureMask[indexOf(x, y)] && !sea[indexOf(x, y)]) { sideSamples.west.push({ x: 3, y }); break; }
for (let x = MAP_W - 1; x >= Math.max(0, MAP_W - 13); x--) if (inside(x, y) && !prefectureMask[indexOf(x, y)] && !sea[indexOf(x, y)]) { sideSamples.east.push({ x: MAP_W - 4, y }); break; }
}
const neighbors = [];
const sideOrder = ["north", "east", "south", "west"];
for (let si = 0; si < sideOrder.length; si++) {
const side = sideOrder[si];
const samples = sideSamples[side];
if (!samples.length && rand(seed, 7500 + si) < 0.45) continue;
const nm = pickUniqueName(si + 1);
const p = samples.length ? samples[Math.floor(rand(seed, 7520 + si) * samples.length) % samples.length] : (
side === "north" ? { x: MAP_W * 0.5, y: 3 } : side === "south" ? { x: MAP_W * 0.5, y: MAP_H - 4 } : side === "west" ? { x: 3, y: MAP_H * 0.5 } : { x: MAP_W - 4, y: MAP_H * 0.5 }
);
neighbors.push({ name: nm, side, x: p.x, y: p.y, kind: "Neighbor Prefecture Label" });
}
return { name, label, neighbors };
}
function generateNeighborPrefectureDetails(neighborLabels, usedNamesForNeighbor, nameDebugForNeighbor) {
const allCities = [];
const allAdmins = [];
const allCbds = [];
const allRoads = [];
const allRailways = [];
const details = [];
const sideBand = {
north: (x, y) => y <= Math.floor(MAP_H * 0.30),
south: (x, y) => y >= Math.ceil(MAP_H * 0.70),
west: (x, y) => x <= Math.floor(MAP_W * 0.34),
east: (x, y) => x >= Math.ceil(MAP_W * 0.66),
};
for (let ni = 0; ni < neighborLabels.length; ni++) {
const label = neighborLabels[ni];
const predicate = sideBand[label.side] || (() => true);
const candidates = [];
for (let y = 2; y < MAP_H - 2; y += 2) {
for (let x = 2; x < MAP_W - 2; x += 2) {
const i = indexOf(x, y);
if (prefectureMask[i] || sea[i] || !predicate(x, y)) continue;
const dToLabel = Math.hypot(x - label.x, y - label.y);
const borderAway = label.side === "north" ? y : label.side === "south" ? MAP_H - 1 - y : label.side === "west" ? x : MAP_W - 1 - x;
const score = terrainSettlementScore(x, y, label.side) - dToLabel * 0.006 + Math.min(16, borderAway) * 0.006;
if (score > 0.06) candidates.push({ x, y, score, neighborIndex: ni, neighborName: label.name, side: label.side });
}
}
const rawCities = chooseSpacedPoints(candidates, 2 + Math.floor(rand(seed, 7700 + ni) * 3), 12, 7710 + ni * 100)
.map((p, n) => {
const rank = n === 0 ? "Neighbor Prefectural Capital" : n === 1 ? "Neighbor Regional Center" : "Neighbor City";
const popBase = n === 0 ? 240000 : n === 1 ? 90000 : 36000;
const popSpread = n === 0 ? 620000 : n === 1 ? 220000 : 90000;
const population = Math.round((popBase + popSpread * Math.pow(clamp(p.score + rand(seed, 7730 + ni * 31 + n), 0, 1), 1.8)) / 1000) * 1000;
return { ...p, kind: rank, rank, population, urbanRadius: clamp(7 + Math.sqrt(population) / 105, 7, 21), coreRadius: clamp(2.4 + Math.sqrt(population) / 420, 2.4, 6.2), urbanWeight: 1.0 + Math.log10(Math.max(10000, population)) * 0.23, insidePrefecture: false };
});
const namedCities = attachIdsAndNames(rawCities, `neighborCity${ni}`, seed + ni * 100, null, nameFields, usedNamesForNeighbor, nameDebugForNeighbor);
const adminCandidates = chooseSpacedPoints(candidates.filter((p) => !namedCities.some((c) => Math.hypot(c.x - p.x, c.y - p.y) < 7)), 4 + Math.floor(rand(seed, 7760 + ni) * 5), 8, 7770 + ni * 100)
.map((p) => ({ ...p, kind: "Neighbor Municipal Center", insidePrefecture: false }));
const namedAdmins = attachIdsAndNames(adminCandidates, `neighborAdmin${ni}`, seed + ni * 131, "Neighbor Municipal Center", nameFields, usedNamesForNeighbor, nameDebugForNeighbor);
const cbds = namedCities.map((city, ci) => ({ x: city.x, y: city.y, parentId: city.id, parentName: city.name, name: `${city.name}CBD`, kind: ci === 0 ? "Neighbor Central Business District" : "Neighbor Urban Center", neighborIndex: ni, neighborName: label.name, insidePrefecture: false }));
const nodes = [...namedCities, ...namedAdmins].sort((a, b) => (b.population || 0) - (a.population || 0) || (b.score || 0) - (a.score || 0));
const roads = [];
const rails = [];
for (let i = 1; i < nodes.length; i++) {
const target = nodes[i];
const anchor = nodes.slice(0, i).sort((a, b) => Math.hypot(a.x - target.x, a.y - target.y) - Math.hypot(b.x - target.x, b.y - target.y))[0];
const road = looseOutsidePath(anchor, target, ni * 400 + i * 17, true);
if (road.length >= 3) roads.push(road);
if (i <= 2 && rand(seed, 7790 + ni * 19 + i) > 0.30) {
const rail = looseOutsidePath(anchor, target, ni * 500 + i * 23, true);
if (rail.length >= 4) rails.push(rail);
}
}
if (nodes[0]) {
const gate = { x: label.x, y: label.y };
const gatewayRoad = looseOutsidePath(nodes[0], gate, ni * 600 + 7, true);
if (gatewayRoad.length >= 3) roads.push(gatewayRoad);
}
const labelAvoid = [...namedCities, ...namedAdmins, ...allCities, ...allAdmins];
const labelCandidates = candidates.length ? candidates : [{ x: label.x, y: label.y, score: 0.1 }];
const fallbackLabel = chooseSpacedPoints(labelCandidates, 1, 1, 7810 + ni)[0] || label;
const capitalPoint = namedCities[0] || fallbackLabel;
const betterLabel = pickPrefectureLabelPosition(labelAvoid, fallbackLabel, {
capital: capitalPoint,
landPredicate: (x, y, i) => !prefectureMask[i] && !sea[i] && predicate(x, y),
areaPredicate: predicate,
seedOffset: 7810 + ni * 97,
}) || fallbackLabel;
label.x = betterLabel.x;
label.y = betterLabel.y;
label.placement = betterLabel.placement || "land";
allCities.push(...namedCities);
allAdmins.push(...namedAdmins);
allCbds.push(...cbds);
allRoads.push(...roads);
allRailways.push(...rails);
details.push({ ...label, cities: namedCities, adminCenters: namedAdmins, centralBusinessDistricts: cbds, roads, railways: rails });
}
return { prefectures: details, cities: allCities, adminCenters: allAdmins, centralBusinessDistricts: allCbds, roads: allRoads, railways: allRailways };
}
// Bridge and tunnel icon systems were removed from the visual model.
// Arrays remain empty for backward-compatible tests and downstream code.
const bridges = [];
const tunnels = [];
const harborWorks = makeHarborWorks(ports);
let abandonedRailways = branchRailways.filter((_, i) => i % 3 === 0);
let castleRuins = castles.filter((_, i) => i % 2 === 1).map((c) => ({ ...c, kind: "Castle Ruins" }));
let preservedOldRoads = premodernRoads.filter((_, i) => i % 2 === 0);
const nameFields = { elevation, slope, sea, river, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland, flowAccum, landuse, populationDensity };
const usedNames = new Set();
const nameDebug = createNameDebug();
villages = attachIdsAndNames(tagInsidePrefecture(villages, prefectureMask), "village", seed, null, nameFields, usedNames, nameDebug);
ports = attachIdsAndNames(tagInsidePrefecture(ports, prefectureMask), "port", seed, null, nameFields, usedNames, nameDebug);
crossings = attachIdsAndNames(tagInsidePrefecture(crossings, prefectureMask), "crossing", seed, null, nameFields, usedNames, nameDebug);
passes = attachIdsAndNames(tagInsidePrefecture(passes, prefectureMask), "pass", seed, null, nameFields, usedNames, nameDebug);
markets = attachIdsAndNames(tagInsidePrefecture(markets, prefectureMask), "market", seed, null, nameFields, usedNames, nameDebug);
castles = attachIdsAndNames(tagInsidePrefecture(castles, prefectureMask), "castle", seed, null, nameFields, usedNames, nameDebug);
castleTowns = attachIdsAndNames(tagInsidePrefecture(castleTowns, prefectureMask), "castleTown", seed, null, nameFields, usedNames, nameDebug);
modernCities = attachIdsAndNames(tagInsidePrefecture(modernCities, prefectureMask), "city", seed, null, nameFields, usedNames, nameDebug);
stations = attachIdsAndNames(tagInsidePrefecture(stations, prefectureMask), "station", seed, null, nameFields, usedNames, nameDebug);
industrialZones = attachIdsAndNames(tagInsidePrefecture(industrialZones, prefectureMask), "industrial", seed, null, nameFields, usedNames, nameDebug);
interchanges = attachIdsAndNames(tagInsidePrefecture(interchanges, prefectureMask), "interchange", seed, null, nameFields, usedNames, nameDebug);
logisticsParks = attachIdsAndNames(tagInsidePrefecture(logisticsParks, prefectureMask), "logistics", seed, null, nameFields, usedNames, nameDebug);
satelliteCities = attachIdsAndNames(tagInsidePrefecture(satelliteCities, prefectureMask), "satellite", seed, null, nameFields, usedNames, nameDebug);
newTowns = attachIdsAndNames(tagInsidePrefecture(newTowns, prefectureMask), "newtown", seed, null, nameFields, usedNames, nameDebug);
castleRuins = attachIdsAndNames(tagInsidePrefecture(castleRuins, prefectureMask), "castleRuin", seed, null, nameFields, usedNames, nameDebug);
externalGateways = attachIdsAndNames(tagInsidePrefecture(externalGateways, prefectureMask), "gateway", seed, "External Gateway", nameFields, usedNames, nameDebug);
const adminCenters = attachIdsAndNames(tagInsidePrefecture(adminCentersRaw, prefectureMask), "admin", seed, "Municipal Center", nameFields, usedNames, nameDebug);
const representativeFeatures = [
...modernCities.map((p) => ({ ...p, representativeWeight: 5.0 + (p.population || 0) / 180000 })),
...markets.map((p) => ({ ...p, representativeWeight: 3.2 })),
...ports.map((p) => ({ ...p, representativeWeight: p.portClass === "major" ? 3.8 : 2.4 })),
...villages.map((p) => ({ ...p, representativeWeight: 1.6 })),
].filter((p) => p.insidePrefecture && p.name);
for (const center of adminCenters) {
const centerAdmin = adminId?.[indexOf(center.x, center.y)];
let best = null;
let bestScore = -INF;
for (const sameAdminOnly of [true, false]) {
for (const feature of representativeFeatures) {
const featureAdmin = adminId?.[indexOf(feature.x, feature.y)];
if (sameAdminOnly && centerAdmin >= 0 && featureAdmin >= 0 && featureAdmin !== centerAdmin) continue;
const d = Math.hypot(center.x - feature.x, center.y - feature.y);
const score = feature.representativeWeight - d * 0.11 - (sameAdminOnly ? 0 : 1.2);
if (score > bestScore) {
bestScore = score;
best = feature;
}
}
if (best) break;
}
if (best) {
center.representativeFeatureId = best.id;
center.representativeFeatureName = best.name;
center.generatedMunicipalityName = center.name;
center.name = best.name;
}
}
const usedAdminNames = new Set();
for (const center of adminCenters) {
let candidate = center.name;
const generated = String(center.generatedMunicipalityName || "");
if (usedAdminNames.has(candidate) && Array.from(generated).length >= 2 && !usedAdminNames.has(generated)) {
candidate = generated;
}
center.name = candidate;
usedAdminNames.add(center.name);
}
nameDebug.maxDerivedPerBase = 0;
const entitiesForNames = [
...modernCities,
...ports,
...markets,
...castles,
...stations,
...industrialZones,
...interchanges,
...logisticsParks,
...satelliteCities,
...newTowns,
...passes,
...crossings,
...adminCenters,
...externalGateways,
].filter((p) => p.insidePrefecture || p.kind === "External Gateway");
const avoidForPrefectureLabel = [
...modernCities,
...ports,
...markets,
...castles,
...castleTowns,
...adminCenters,
...stations,
...satelliteCities,
...newTowns,
].filter((p) => p.insidePrefecture && p.name);
const prefectureIdentity = generatePrefectureIdentity(usedNames, avoidForPrefectureLabel);
const neighborPrefectureDetails = generateNeighborPrefectureDetails(prefectureIdentity.neighbors, usedNames, nameDebug);
const bridgeLimitedPathGroups = [
"premodernRoads",
"minorRoads",
"nationalRoads",
"ringRoads",
"expressways",
"ringExpressways",
"icAccessRoads",
"externalRoads",
"externalExpressways",
"railways",
"branchRailways",
"ringRailways",
"externalRailways",
"abandonedRailways",
"preservedOldRoads",
];
premodernRoads = enforceBridgeLimitList(premodernRoads);
minorRoads = enforceBridgeLimitList(minorRoads);
nationalRoads = enforceBridgeLimitList(nationalRoads);
ringRoads = enforceBridgeLimitList(ringRoads);
expressways = enforceBridgeLimitList(expressways);
ringExpressways = enforceBridgeLimitList(ringExpressways);
icAccessRoads = enforceBridgeLimitList(icAccessRoads);
externalRoads = enforceBridgeLimitList(externalRoads);
externalExpressways = enforceBridgeLimitList(externalExpressways);
railways = enforceBridgeLimitList(railways);
branchRailways = enforceBridgeLimitList(branchRailways);
ringRailways = enforceBridgeLimitList(ringRailways);
externalRailways = enforceBridgeLimitList(externalRailways);
abandonedRailways = enforceBridgeLimitList(abandonedRailways);
preservedOldRoads = enforceBridgeLimitList(preservedOldRoads);
for (const pref of neighborPrefectureDetails.prefectures || []) {
pref.roads = enforceBridgeLimitList(pref.roads);
pref.railways = enforceBridgeLimitList(pref.railways);
}
neighborPrefectureDetails.roads = enforceBridgeLimitList(neighborPrefectureDetails.roads);
neighborPrefectureDetails.railways = enforceBridgeLimitList(neighborPrefectureDetails.railways);
const bridgeLimitDebug = {
maxBridgeCells: MAX_BRIDGE_CELLS,
maxWaterRun: Math.max(
0,
...[
...premodernRoads,
...minorRoads,
...nationalRoads,
...ringRoads,
...expressways,
...ringExpressways,
...icAccessRoads,
...externalRoads,
...externalExpressways,
...railways,
...branchRailways,
...ringRailways,
...externalRailways,
...abandonedRailways,
...preservedOldRoads,
...(neighborPrefectureDetails.roads || []),
...(neighborPrefectureDetails.railways || []),
].map(pathMaxWaterRun)
),
enforcedGroups: bridgeLimitedPathGroups,
};
return applyOutputOptions({
width: MAP_W,
height: MAP_H,
cellSize: CELL_SIZE,
// Keep the apparent map scale close to the original 172-cell-wide version
// after increasing logical terrain resolution.
scaleKmPerCell: 172 / MAP_W,
prefectureName: prefectureIdentity.name,
prefectureLabel: prefectureIdentity.label,
neighborPrefectures: prefectureIdentity.neighbors,
neighborPrefectureDetails,
terrainTemplate,
seaLevel,
prefectureMask,
prefectureBorder,
prefectureRegionId,
regionalDebug,
terrainDebug,
regionalPrefectureBorders,
elevation,
moisture,
slope,
sea,
ocean,
lake,
river,
floodplain,
plain,
agriculture,
settlementCluster,
ridgeField,
valleyField,
basinField,
coastalLowland,
flowAccum,
erosionField,
depositionField,
arcSpineField,
branchRidgeField,
depositionalLowland,
alluvialFanField,
deltaField,
naturalBarrierScore,
villages,
ports,
crossings,
passes,
markets,
castles,
castleTowns,
premodernRoads,
minorRoads,
modernCities,
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,
branchRailways,
ringRailways,
externalRailways,
stations,
industrialZones,
nationalRoads,
ringRoads,
expressways,
ringExpressways,
icAccessRoads,
externalRoads,
externalExpressways,
interchanges,
logisticsParks,
satelliteCities,
newTowns,
bridges,
tunnels,
harborWorks,
landuse,
adminCenters,
adminId,
adminBorders,
adminDebug,
abandonedRailways,
castleRuins,
preservedOldRoads,
riverPaths,
mainRivers,
tributaryRivers,
smallStreams,
externalGateways,
transportDebug,
bridgeLimitDebug,
entitiesForNames,
nameDebug,
}, options);
}