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, 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; } // 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 adminNameSuffixes = [ "\u753A\u57DF", "\u5E02\u57DF", "\u90F7\u57DF", "\u6D41\u57DF", "\u6E7E\u5CB8", "\u5C71\u9E93", "\u5E73\u91CE", "\u5730\u533A", ]; const adminNameCounts = new Map(); for (const center of adminCenters) adminNameCounts.set(center.name, (adminNameCounts.get(center.name) || 0) + 1); const baseVariantCounts = new Map(); for (const center of adminCenters) { const base = String(center.name || ""); const count = adminNameCounts.get(base) || 0; if (count <= 1) { baseVariantCounts.set(base, Math.max(baseVariantCounts.get(base) || 0, 1)); continue; } const usedForBase = baseVariantCounts.get(base) || 0; if (usedForBase === 0) { baseVariantCounts.set(base, 1); continue; } if (usedForBase < 2) { const i = indexOf(center.x, center.y); const naturalSuffix = coastalLowland[i] > 0.28 ? "\u6E7E\u5CB8" : basinField[i] > 0.28 ? "\u5E73\u91CE" : ridgeField[i] > 0.42 || slope[i] > 0.34 ? "\u5C71\u9E93" : river[i] > 0.25 || flowAccum[i] > 0.38 ? "\u6D41\u57DF" : adminNameSuffixes[(Math.floor(center.x / 12) + Math.floor(center.y / 12)) % adminNameSuffixes.length]; center.name = `${base}${naturalSuffix}`; center.derivedFromBaseName = base; nameDebug.derivedNameCount++; baseVariantCounts.set(base, usedForBase + 1); } } 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 = Math.max(0, ...baseVariantCounts.values()); 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, 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, entitiesForNames, nameDebug, }, options); }