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"; export function finishMapOutput({ seed, options, terrainTemplate, 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, prefectureMask, prefectureBorder, prefectureRegionId, regionalDebug, 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; } // 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(); 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 adminNamePrefixes = ["\u6771", "\u897F", "\u5357", "\u5317", "\u4E0A", "\u4E0B", "\u65B0", "\u65E7", "\u4E2D", "\u5916"]; const adminNameCounts = new Map(); for (const center of adminCenters) adminNameCounts.set(center.name, (adminNameCounts.get(center.name) || 0) + 1); const duplicateOrdinal = new Map(); for (const center of adminCenters) { if ((adminNameCounts.get(center.name) || 0) <= 1) continue; const n = duplicateOrdinal.get(center.name) || 0; duplicateOrdinal.set(center.name, n + 1); const prefix = adminNamePrefixes[(Math.floor(center.x / Math.max(1, MAP_W / 3)) + Math.floor(center.y / Math.max(1, MAP_H / 3)) * 3 + n) % adminNamePrefixes.length]; if (!String(center.name).startsWith(prefix)) center.name = `${prefix}${center.name}`; } const usedAdminNames = new Set(); for (const center of adminCenters) { let candidate = center.name; let guard = 0; while (usedAdminNames.has(candidate) && guard < adminNamePrefixes.length) { candidate = `${adminNamePrefixes[(guard + Math.floor(center.x / 12) + Math.floor(center.y / 12)) % adminNamePrefixes.length]}${center.name}`; guard++; } center.name = candidate; usedAdminNames.add(center.name); } 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, prefectureMask, prefectureBorder, prefectureRegionId, regionalDebug, 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, entitiesForNames, nameDebug, }, options); }