import { createNameDebug } from "./names.js"; import { CELL_SIZE, INF, MAP_H, MAP_W, clamp, indexOf, inside, rand } from "./mapUtils.js"; import { applyOutputOptions, attachIdsAndNames, recalculatePopulationAfterLanduse, tagInsidePrefecture } from "./mapGeneratorHelpers.js"; 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)}`; } export function finishMapOutput({ seed, options, terrainTemplate, seaLevel, cityPopulationCap, stationInfluence, roadInfluence, railInfluence2, elevation, moisture, slope, sea, ocean, lake, river, floodplain, plain, agriculture, settlementCluster, ridgeField, valleyField, visibleRavineField, surfaceTextureField, 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, }) { const outputProgress = (step) => options?.onProgress?.({ status: "output-step", key: "output", label: `Output: ${step}`, step }); outputProgress("population recalculation"); // Final population pass after land-use cleanup, satellite municipality locking, and isolated urban deletion. // 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); 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); } } outputProgress("harbor works"); 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); const abandonedRailways = branchRailways.filter((_, i) => i % 3 === 0); let castleRuins = castles.filter((_, i) => i % 2 === 1).map((c) => ({ ...c, kind: "Castle Ruins" })); const 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(); outputProgress("feature naming"); 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); outputProgress("municipality naming"); const adminCenters = attachIdsAndNames(tagInsidePrefecture(adminCentersRaw, humanRegionMask), "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.name && (p.insidePrefecture || humanRegionMask[indexOf(p.x, p.y)])); 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; } center.generatedMunicipalityName = center.generatedMunicipalityName || center.name; if (best) { center.representativeFeatureId = best.id; center.representativeFeatureName = best.name; center.municipalityRootName = best.name; } else { center.municipalityRootName = center.generatedMunicipalityName; } } const usedAdminNames = new Set(); for (const [index, center] of adminCenters.entries()) { center.adminNumericId = index; center.municipalityId = index; 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); if (usedAdminNames.has(candidate) && Array.from(generated).length >= 2 && !usedAdminNames.has(generated)) { candidate = generated; } 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}`; } center.name = candidate; center.labelName = candidate; center.municipalityName = candidate; usedAdminNames.add(center.name); } nameDebug.maxDerivedPerBase = 0; outputProgress("final package"); const entitiesForNames = [ ...modernCities, ...ports, ...markets, ...castles, ...stations, ...industrialZones, ...interchanges, ...logisticsParks, ...satelliteCities, ...newTowns, ...passes, ...crossings, ...adminCenters, ...externalGateways, ].filter((p) => p.insidePrefecture || p.kind === "External Gateway"); return applyOutputOptions({ width: MAP_W, height: MAP_H, cellSize: CELL_SIZE, terrainTemplate, seaLevel, prefectureMask, humanRegionMask, prefectureBorder, prefectureRegionId, regionalDebug, terrainDebug, regionalPrefectureBorders, elevation, moisture, slope, sea, ocean, lake, river, floodplain, plain, agriculture, settlementCluster, ridgeField, valleyField, visibleRavineField, surfaceTextureField, 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, entitiesForNames, nameDebug, }, options); }