map/mapOutput.js

341 lines
13 KiB
JavaScript
Raw Normal View History

2026-05-21 13:20:19 +09:00
import { createNameDebug } from "./names.js";
import { CELL_SIZE, INF, MAP_H, MAP_W, clamp, indexOf, inside, rand } from "./mapUtils.js";
2026-05-23 18:06:01 +09:00
import { applyOutputOptions, attachIdsAndNames, recalculatePopulationAfterLanduse, tagInsidePrefecture } from "./mapGeneratorHelpers.js";
2026-05-21 13:20:19 +09:00
2026-05-24 17:38:51 +09:00
const MUNICIPAL_SUFFIX_RE = /[市町村区]$/u;
function municipalitySuffixForCenter(center, fields, seed, ordinal = 0) {
const i = inside(center?.x || 0, center?.y || 0) ? indexOf(center.x, center.y) : 0;
const density = fields.populationDensity?.[i] || 0;
const land = fields.landuse?.[i] ?? 0;
const urban = density > 0.36 || [2, 3, 4, 7, 8].includes(land) || center?.protectedSatellite;
const rural = (fields.elevation?.[i] || 0) > 0.58 || (fields.slope?.[i] || 0) > 0.40 || (fields.ridgeField?.[i] || 0) > 0.46;
if (urban) return "市";
if (rural && rand(seed + ordinal * 17, 9021) < 0.58) return "村";
return "町";
}
function municipalityNameFromRoot(root, center, fields, seed, ordinal = 0) {
let value = String(root || center?.name || "").trim();
if (!value) value = `自治${ordinal + 1}`;
value = value.replace(/[駅港城跡宿]$/u, "");
if (Array.from(value).length < 2) value = `${value}${String(center?.generatedMunicipalityName || "里")}`.slice(0, 3);
if (MUNICIPAL_SUFFIX_RE.test(value)) return value;
return `${value}${municipalitySuffixForCenter(center, fields, seed, ordinal)}`;
}
2026-05-21 13:20:19 +09:00
export function finishMapOutput({
seed,
options,
2026-05-21 22:03:14 +09:00
terrainTemplate,
2026-05-22 14:13:35 +09:00
seaLevel,
2026-05-21 13:20:19 +09:00
cityPopulationCap,
stationInfluence,
roadInfluence,
railInfluence2,
elevation,
moisture,
slope,
sea,
2026-05-21 22:03:14 +09:00
ocean,
lake,
2026-05-21 13:20:19 +09:00
river,
floodplain,
plain,
agriculture,
settlementCluster,
ridgeField,
valleyField,
2026-05-23 18:06:01 +09:00
visibleRavineField,
surfaceTextureField,
2026-05-21 13:20:19 +09:00
basinField,
coastalLowland,
flowAccum,
erosionField,
depositionField,
2026-05-21 22:03:14 +09:00
arcSpineField,
branchRidgeField,
depositionalLowland,
alluvialFanField,
deltaField,
naturalBarrierScore,
2026-05-21 13:20:19 +09:00
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,
2026-05-22 02:11:18 +09:00
transportDebug,
2026-05-21 13:20:19 +09:00
prefectureMask,
prefectureBorder,
prefectureRegionId,
regionalDebug,
2026-05-22 02:11:18 +09:00
terrainDebug,
2026-05-21 13:20:19 +09:00
regionalPrefectureBorders,
}) {
2026-05-24 19:33:09 +09:00
const outputProgress = (step) => options?.onProgress?.({ status: "output-step", key: "output", label: `Output: ${step}`, step });
outputProgress("population recalculation");
2026-05-21 13:20:19 +09:00
// Final population pass after land-use cleanup, satellite municipality locking, and isolated urban deletion.
2026-05-24 17:38:51 +09:00
// Use all generated prefecture regions for human-geography density, not only the focused prefecture.
const humanRegionMask = new Uint8Array(MAP_W * MAP_H);
for (let i = 0; i < humanRegionMask.length; i++) {
humanRegionMask[i] = !sea[i] && ((prefectureRegionId?.[i] ?? -1) >= 0 || prefectureMask[i]) ? 1 : 0;
}
recalculatePopulationAfterLanduse(modernCities, satelliteCities, populationDensity, landuse, humanRegionMask, sea, stationInfluence, roadInfluence, railInfluence2);
2026-05-21 13:20:19 +09:00
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);
}
}
2026-05-24 19:33:09 +09:00
outputProgress("harbor works");
2026-05-21 13:20:19 +09:00
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;
}
// 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);
2026-05-23 18:06:01 +09:00
const abandonedRailways = branchRailways.filter((_, i) => i % 3 === 0);
2026-05-21 13:20:19 +09:00
let castleRuins = castles.filter((_, i) => i % 2 === 1).map((c) => ({ ...c, kind: "Castle Ruins" }));
2026-05-23 18:06:01 +09:00
const preservedOldRoads = premodernRoads.filter((_, i) => i % 2 === 0);
2026-05-21 13:20:19 +09:00
const nameFields = { elevation, slope, sea, river, plain, agriculture, ridgeField, valleyField, basinField, coastalLowland, flowAccum, landuse, populationDensity };
const usedNames = new Set();
const nameDebug = createNameDebug();
2026-05-24 19:33:09 +09:00
outputProgress("feature naming");
2026-05-21 13:20:19 +09:00
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);
2026-05-24 19:33:09 +09:00
outputProgress("municipality naming");
2026-05-24 17:38:51 +09:00
const adminCenters = attachIdsAndNames(tagInsidePrefecture(adminCentersRaw, humanRegionMask), "admin", seed, "Municipal Center", nameFields, usedNames, nameDebug);
2026-05-21 22:03:14 +09:00
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 })),
2026-05-24 17:38:51 +09:00
].filter((p) => p.name && (p.insidePrefecture || humanRegionMask[indexOf(p.x, p.y)]));
2026-05-21 22:03:14 +09:00
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;
}
2026-05-24 17:38:51 +09:00
center.generatedMunicipalityName = center.generatedMunicipalityName || center.name;
2026-05-21 22:03:14 +09:00
if (best) {
center.representativeFeatureId = best.id;
center.representativeFeatureName = best.name;
2026-05-24 17:38:51 +09:00
center.municipalityRootName = best.name;
} else {
center.municipalityRootName = center.generatedMunicipalityName;
2026-05-21 22:03:14 +09:00
}
}
const usedAdminNames = new Set();
2026-05-24 17:38:51 +09:00
for (const [index, center] of adminCenters.entries()) {
2026-05-24 19:33:09 +09:00
center.adminNumericId = index;
center.municipalityId = index;
2026-05-24 17:38:51 +09:00
let candidate = municipalityNameFromRoot(center.municipalityRootName || center.generatedMunicipalityName || center.name, center, nameFields, seed, index);
const generated = municipalityNameFromRoot(center.generatedMunicipalityName || center.name, center, nameFields, seed + 177, index);
2026-05-22 02:11:18 +09:00
if (usedAdminNames.has(candidate) && Array.from(generated).length >= 2 && !usedAdminNames.has(generated)) {
candidate = generated;
2026-05-21 22:03:14 +09:00
}
2026-05-24 17:38:51 +09:00
if (usedAdminNames.has(candidate)) {
const suffix = municipalitySuffixForCenter(center, nameFields, seed + 313, index);
const base = String(center.generatedMunicipalityName || center.municipalityRootName || center.name || `自治${index + 1}`).replace(/[市町村区]$/u, "");
candidate = `${base}${index + 1}${suffix}`;
}
2026-05-21 22:03:14 +09:00
center.name = candidate;
2026-05-24 17:38:51 +09:00
center.labelName = candidate;
center.municipalityName = candidate;
2026-05-21 22:03:14 +09:00
usedAdminNames.add(center.name);
}
2026-05-22 13:57:52 +09:00
nameDebug.maxDerivedPerBase = 0;
2026-05-21 13:20:19 +09:00
2026-05-24 19:33:09 +09:00
outputProgress("final package");
2026-05-21 13:20:19 +09:00
const entitiesForNames = [
...modernCities,
...ports,
...markets,
...castles,
...stations,
...industrialZones,
...interchanges,
...logisticsParks,
...satelliteCities,
...newTowns,
...passes,
...crossings,
2026-05-21 22:03:14 +09:00
...adminCenters,
2026-05-21 13:20:19 +09:00
...externalGateways,
].filter((p) => p.insidePrefecture || p.kind === "External Gateway");
return applyOutputOptions({
width: MAP_W,
height: MAP_H,
cellSize: CELL_SIZE,
2026-05-21 22:03:14 +09:00
terrainTemplate,
2026-05-22 14:13:35 +09:00
seaLevel,
2026-05-21 13:20:19 +09:00
prefectureMask,
2026-05-24 17:38:51 +09:00
humanRegionMask,
2026-05-21 13:20:19 +09:00
prefectureBorder,
prefectureRegionId,
regionalDebug,
2026-05-22 02:11:18 +09:00
terrainDebug,
2026-05-21 13:20:19 +09:00
regionalPrefectureBorders,
elevation,
moisture,
slope,
sea,
2026-05-21 22:03:14 +09:00
ocean,
lake,
2026-05-21 13:20:19 +09:00
river,
floodplain,
plain,
agriculture,
settlementCluster,
ridgeField,
valleyField,
2026-05-23 18:06:01 +09:00
visibleRavineField,
surfaceTextureField,
2026-05-21 13:20:19 +09:00
basinField,
coastalLowland,
flowAccum,
erosionField,
depositionField,
2026-05-21 22:03:14 +09:00
arcSpineField,
branchRidgeField,
depositionalLowland,
alluvialFanField,
deltaField,
naturalBarrierScore,
2026-05-21 13:20:19 +09:00
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,
2026-05-22 02:11:18 +09:00
transportDebug,
2026-05-21 13:20:19 +09:00
entitiesForNames,
nameDebug,
}, options);
}