761 lines
31 KiB
JavaScript
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);
|
|
}
|