import { createNameDebug } from "./names.js"; import { CELL_SIZE, INF, MAP_H, MAP_W, clamp, indexOf, inside, rand } from "./mapUtils.js"; import { applyOutputOptions, attachIdsAndNames, influenceFromPaths, tagInsidePrefecture } from "./mapGeneratorHelpers.js"; const MUNICIPAL_SUFFIX_RE = /[市町村区]$/u; function stripMunicipalSuffix(name) { return String(name || "").replace(/[市町村区]$/u, "").trim(); } 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(/[市町村区駅港城跡宿]$/gu, ""); const fallback = String(center?.generatedMunicipalityName || "里").replace(/[市町村区駅港城跡宿]$/gu, ""); if (Array.from(value).length < 2) value = `${value}${fallback || "里"}`; // Municipality roots should be at most two toponymic elements. The admin // suffix is separate; avoid direction+root+suffix three-element names. value = Array.from(value).slice(0, 2).join(""); return `${value}${municipalitySuffixForCenter(center, fields, seed, ordinal)}`; } function assignMunicipalityPopulations(adminCenters, adminId, fields, settlementFeatures = []) { if (!adminCenters?.length || !adminId) return; const totals = new Float64Array(adminCenters.length); const settlementTotals = new Float64Array(adminCenters.length); for (let i = 0; i < adminId.length; i++) { const id = adminId[i]; if (id < 0 || id >= totals.length || fields.sea?.[i]) continue; const density = fields.populationDensity?.[i] || 0; const lu = fields.landuse?.[i] ?? 0; const plain = fields.plain?.[i] || 0; const agri = fields.agriculture?.[i] || 0; const builtWeight = lu === 3 ? 520 : lu === 2 ? 360 : lu === 4 || lu === 7 || lu === 8 ? 260 : lu === 5 || lu === 6 ? 160 : lu === 1 ? 82 : 22; const ruralFloor = lu === 1 ? 10 + agri * 18 + plain * 10 : 0; totals[id] += density * builtWeight + ruralFloor; } // Population-bearing generated settlements are canonical entities, so add // their explicit populations exactly once to the municipality containing the // point. Some towns are promoted to cities later, so the same coordinate can // appear in both `markets` and `modernCities`; keep only the strongest record // per coordinate to avoid double counting. const uniqueSettlementByCell = new Map(); for (const feature of settlementFeatures || []) { if (!feature || !Number.isFinite(feature.population) || feature.population <= 0) continue; if (!inside(feature.x, feature.y)) continue; const i = indexOf(feature.x, feature.y); if (fields.sea?.[i]) continue; const key = `${Math.round(feature.x)}:${Math.round(feature.y)}`; const current = uniqueSettlementByCell.get(key); const priority = (feature.isPrefecturalCapital ? 4_000_000 : 0) + (feature.isRegionalCapital ? 1_000_000 : 0) + (feature.population || 0); if (!current || priority > current.priority) uniqueSettlementByCell.set(key, { feature, priority, i }); } let skippedDuplicateSettlementPopulation = 0; for (const { feature, i } of uniqueSettlementByCell.values()) { const id = adminId[i]; if (id < 0 || id >= totals.length) continue; settlementTotals[id] += feature.population; } for (const feature of settlementFeatures || []) { if (!feature || !Number.isFinite(feature.population) || feature.population <= 0 || !inside(feature.x, feature.y)) continue; const key = `${Math.round(feature.x)}:${Math.round(feature.y)}`; const kept = uniqueSettlementByCell.get(key)?.feature; if (kept && kept !== feature) skippedDuplicateSettlementPopulation += feature.population || 0; } for (let id = 0; id < adminCenters.length; id++) { const raw = (totals[id] || 0) + (settlementTotals[id] || 0); const rounded = raw >= 10000 ? Math.round(raw / 1000) * 1000 : Math.round(raw / 100) * 100; adminCenters[id].municipalityPopulation = Math.max(0, rounded); adminCenters[id].municipalitySettlementPopulation = Math.max(0, Math.round((settlementTotals[id] || 0) / 100) * 100); adminCenters[id].municipalitySkippedDuplicateSettlementPopulation = Math.max(0, Math.round(skippedDuplicateSettlementPopulation / 100) * 100); } } function promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCities, markets, adminCenters, fields, seed, minPopulation = 200000) { if (!prefectureRegionId) return 0; const prefIds = new Set(); for (let i = 0; i < prefectureRegionId.length; i++) { const id = prefectureRegionId[i]; if (!sea[i] && id >= 0) prefIds.add(id); } let promoted = 0; function prefAt(p) { if (!p || !inside(p.x, p.y)) return -1; return prefectureRegionId[indexOf(p.x, p.y)] ?? -1; } for (const prefId of [...prefIds].sort((a, b) => a - b)) { const cities = (modernCities || []).filter((p) => prefAt(p) === prefId); let target = cities.slice().sort((a, b) => ((b.isRegionalCapital ? 800000 : 0) + (b.population || 0)) - ((a.isRegionalCapital ? 800000 : 0) + (a.population || 0)) )[0]; if (!target) { const market = (markets || []).filter((p) => prefAt(p) === prefId).sort((a, b) => (b.population || 0) - (a.population || 0))[0]; if (market) { target = { ...market, kind: "Regional Capital", rank: "Regional Capital", promotedFromMarketTown: true, labelPriorityBase: 900, }; modernCities.push(target); } } if (!target) { const center = (adminCenters || []).filter((p) => prefAt(p) === prefId).sort((a, b) => (b.municipalityPopulation || 0) - (a.municipalityPopulation || 0))[0]; if (center) { target = { ...center, kind: "Regional Capital", rank: "Regional Capital", promotedFromMunicipalCenter: true, labelPriorityBase: 880, }; modernCities.push(target); } } if (!target) continue; const promotedPopulation = Math.round((minPopulation + rand(seed + 52000, prefId * 37 + 11) * 120000) / 1000) * 1000; if ((target.population || 0) < promotedPopulation) { target.population = promotedPopulation; promoted++; } target.isPrefecturalCapital = true; target.isRegionalCapital = true; target.rank = target.rank || "Regional Capital"; target.kind = target.kind === "Market Town" || target.kind === "Port Town" || target.kind === "Valley Market Town" ? "Regional Capital" : (target.kind || "Regional Capital"); target.urbanRadius = clamp(6.2 + Math.sqrt(target.population) / 80, 9, 30); target.coreRadius = clamp(2.0 + Math.sqrt(target.population) / 390, 2.4, 6.4); target.sprawlRadius = clamp(target.urbanRadius * 1.34, target.urbanRadius + 2, 38); target.urbanWeight = clamp(1.05 + Math.log10(Math.max(10000, target.population)) * 0.30, 1.25, 2.55); } return promoted; } function buildPrefectureRegions(prefectureRegionId, sea, fields, seed, usedNames, nameDebug, options = {}) { if (!prefectureRegionId) return []; const { modernCities = [], adminCenters = [] } = options; const byId = new Map(); for (let i = 0; i < prefectureRegionId.length; i++) { const id = prefectureRegionId[i]; if (sea[i] || id < 0) continue; if (!byId.has(id)) byId.set(id, { id, area: 0, sx: 0, sy: 0, cells: [] }); const row = byId.get(id); const x = i % MAP_W; const y = Math.floor(i / MAP_W); row.area++; row.sx += x; row.sy += y; row.cells.push(i); } const regions = []; const capitalNameByPref = new Map(); for (const city of modernCities || []) { if (!city || !inside(city.x, city.y) || !city.name) continue; const prefId = prefectureRegionId[indexOf(city.x, city.y)]; if (prefId < 0) continue; const current = capitalNameByPref.get(prefId); const score = (city.isPrefecturalCapital ? 2_000_000 : 0) + (city.isRegionalCapital ? 500_000 : 0) + (city.population || 0); if (!current || score > current.score) capitalNameByPref.set(prefId, { name: stripMunicipalSuffix(city.name), score, x: city.x, y: city.y, population: city.population || 0, source: "city" }); } for (const center of adminCenters || []) { if (!center || !inside(center.x, center.y) || !center.name) continue; const prefId = prefectureRegionId[indexOf(center.x, center.y)]; if (prefId < 0 || capitalNameByPref.has(prefId)) continue; capitalNameByPref.set(prefId, { name: stripMunicipalSuffix(center.name), score: center.municipalityPopulation || 0, x: center.x, y: center.y, population: center.municipalityPopulation || 0, source: "admin" }); } for (const row of [...byId.values()].sort((a, b) => a.id - b.id)) { const cx = row.sx / Math.max(1, row.area); const cy = row.sy / Math.max(1, row.area); let bestI = row.cells[0]; let bestScore = -INF; for (const i of row.cells) { const x = i % MAP_W; const y = Math.floor(i / MAP_W); const score = (fields.populationDensity?.[i] || 0) * 1.6 + (fields.plain?.[i] || 0) * 0.28 + (fields.basinField?.[i] || 0) * 0.22 + (fields.coastalLowland?.[i] || 0) * 0.16 - (fields.slope?.[i] || 0) * 0.36 - (fields.ridgeField?.[i] || 0) * 0.30 - Math.hypot(x - cx, y - cy) * 0.08; if (score > bestScore) { bestScore = score; bestI = i; } } let x = bestI % MAP_W; let y = Math.floor(bestI / MAP_W); const capitalInfo = capitalNameByPref.get(row.id); if (capitalInfo && inside(capitalInfo.x, capitalInfo.y)) { const targetRing = clamp(5.5 + Math.sqrt(row.area) / 52, 6, 15); let labelI = bestI; let labelScore = -INF; for (const i of row.cells) { const lx = i % MAP_W; const ly = Math.floor(i / MAP_W); const d = Math.hypot(lx - capitalInfo.x, ly - capitalInfo.y); if (d < 2 || d > Math.max(19, targetRing * 2.2)) continue; const lu = fields.landuse?.[i] ?? 0; const builtPenalty = lu === 3 ? 1.2 : lu === 2 || lu === 4 || lu === 7 || lu === 8 ? 0.70 : 0; const score = -Math.abs(d - targetRing) * 0.38 - (fields.populationDensity?.[i] || 0) * 1.1 - builtPenalty + (fields.plain?.[i] || 0) * 0.22 + (fields.basinField?.[i] || 0) * 0.14 + (fields.coastalLowland?.[i] || 0) * 0.10 - (fields.slope?.[i] || 0) * 0.30 - (fields.ridgeField?.[i] || 0) * 0.24; if (score > labelScore) { labelScore = score; labelI = i; } } x = labelI % MAP_W; y = Math.floor(labelI / MAP_W); } regions.push({ id: row.id, x, y, area: row.area, kind: row.id === 0 ? "Current Prefecture" : "Prefecture", labelPriorityBase: 900 + Math.sqrt(row.area), capitalX: capitalInfo?.x, capitalY: capitalInfo?.y }); } const named = attachIdsAndNames(regions, "prefecture", seed + 41000, null, fields, usedNames, nameDebug) .map((region, index) => ({ ...region, featureId: region.id, id: regions[index].id, labelName: region.name })); const usedPrefNames = new Set(); for (const region of named) { const capital = capitalNameByPref.get(region.id)?.name; let candidate = capital && Array.from(capital).length >= 2 ? capital : stripMunicipalSuffix(region.name); if (!candidate) candidate = stripMunicipalSuffix(region.name) || `県域${region.id + 1}`; if (usedPrefNames.has(candidate)) candidate = `${candidate}${region.id + 1}`; candidate = `${String(candidate).replace(/[都道府県]$/u, "")}県`; region.name = candidate; region.labelName = candidate; region.prefectureCapitalDerivedName = Boolean(capital); usedPrefNames.add(candidate); } return named; } export function finishMapOutput({ seed, options, terrain, features, admin, }) { const { terrainTemplate, seaLevel, elevation, moisture, slope, sea, ocean, lake, river, floodplain, plain, agriculture, ridgeField, valleyField, visibleRavineField, surfaceTextureField, basinField, coastalLowland, flowAccum, erosionField, depositionField, arcSpineField, branchRidgeField, depositionalLowland, alluvialFanField, deltaField, naturalBarrierScore, riverPaths, mainRivers, tributaryRivers, smallStreams, prefectureMask, prefectureBorder, terrainDebug, } = terrain; const { cityPopulationCap, stationInfluence, roadInfluence, railInfluence2, settlementCluster, villages: inputVillages, ports: inputPorts, crossings: inputCrossings, passes: inputPasses, markets: inputMarkets, castles: inputCastles, castleTowns: inputCastleTowns, premodernRoads, minorRoads, modernCities: inputModernCities, populationDensity, railways, branchRailways, ringRailways, externalRailways, stations: inputStations, industrialZones: inputIndustrialZones, nationalRoads, ringRoads, expressways, ringExpressways, icAccessRoads, externalRoads, externalExpressways, interchanges: inputInterchanges, logisticsParks: inputLogisticsParks, satelliteCities: inputSatelliteCities, newTowns: inputNewTowns, landuse, externalGateways: inputExternalGateways, transportDebug, } = features; const { adminCentersRaw, adminId, adminBorders, adminDebug, prefectureRegionId, municipalityToPrefectureId, regionalPrefectureBorders: adminRegionalPrefectureBorders, regionalDebug, naturalCompartmentId, naturalCompartments, } = admin; let villages = inputVillages; let ports = inputPorts; let crossings = inputCrossings; let passes = inputPasses; let markets = inputMarkets; let castles = inputCastles; let castleTowns = inputCastleTowns; let modernCities = inputModernCities; let stations = inputStations; let industrialZones = inputIndustrialZones; let interchanges = inputInterchanges; let logisticsParks = inputLogisticsParks; let satelliteCities = inputSatelliteCities; let newTowns = inputNewTowns; let externalGateways = inputExternalGateways; const outputProgress = (step) => options?.onProgress?.({ status: "output-step", key: "output", label: `Output: ${step}`, step }); outputProgress("final packaging"); // Use all generated prefecture regions for human-geography masks, not only // the focused prefecture. Population density itself is already generated in // the human stage and is not rebuilt here. 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; } // Population density is generated directly in the human stage. Do not rebuild // it here from land-use or municipal offices; output should only package and // name features. for (const city of modernCities) { 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); } } let castleRuins = castles.filter((_, i) => i % 2 === 1).map((c) => ({ ...c, kind: "Castle Ruins" })); 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.canonicalSettlementId = best.id; center.canonicalSettlementName = 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.canonicalSettlementName || 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 rootSource = String(center.generatedMunicipalityName || center.name || center.municipalityRootName || `自治${index + 1}`).replace(/[市町村区駅港城跡宿]$/gu, ""); const chars = Array.from(rootSource || "里郷"); const alternates = [ chars.slice(0, 2).join(""), chars.slice(-2).join(""), `${chars[0] || "里"}${["里", "郷", "野", "田", "川", "原", "浜", "浦"][(seed + index) % 8]}`, `${["東", "西", "南", "北", "上", "下", "中"][(seed + index) % 7]}${chars[0] || "里"}`, ].filter((v) => Array.from(v).length >= 2); for (let attempt = 0; attempt < alternates.length + 12 && usedAdminNames.has(candidate); attempt++) { const root = attempt < alternates.length ? alternates[attempt] : `第${(index + attempt) % 10}`; candidate = `${Array.from(root).slice(0, 2).join("")}${suffix}`; } } center.name = candidate; center.labelName = candidate; center.municipalityName = candidate; usedAdminNames.add(center.name); } const promotedPrefectureCapitals = promotePrefectureCapitalPopulations(prefectureRegionId, sea, modernCities, markets, adminCenters, nameFields, seed, 220000); assignMunicipalityPopulations(adminCenters, adminId, nameFields, [ ...modernCities, ...markets, ...villages, ...satelliteCities, ...newTowns, ]); function addMunicipalCenterLocalAccess() { if (!transportDebug) return; const debugLayers = transportDebug.layers || (transportDebug.layers = { repairedSegments: [], unservedSettlements: [] }); debugLayers.repairedSegments ||= []; debugLayers.unservedSettlements ||= []; const accessInfluence = influenceFromPaths([...nationalRoads, ...railways, ...externalRoads, ...minorRoads], 7); const localPenalty = influenceFromPaths(minorRoads, 4); const candidates = adminCenters .filter((p) => inside(p.x, p.y) && !sea[indexOf(p.x, p.y)] && accessInfluence[indexOf(p.x, p.y)] < 0.16) .sort((a, b) => (b.municipalityPopulation || 0) - (a.municipalityPopulation || 0)) .slice(0, 45); function routeAccess(start) { const maxSteps = 72; const out = []; const visited = new Set([`${start.x},${start.y}`]); let x = start.x; let y = start.y; let lastKey = ""; for (let step = 0; step < maxSteps; step++) { const i = indexOf(x, y); if (step > 3 && (accessInfluence[i] > 0.20 || localPenalty[i] > 0.10)) return out; let best = null; let bestScore = INF; for (let dy = -1; dy <= 1; dy++) { for (let dx = -1; dx <= 1; dx++) { if (!dx && !dy) continue; const nx = x + dx; const ny = y + dy; if (!inside(nx, ny)) continue; const ni = indexOf(nx, ny); if (sea[ni]) continue; if (visited.has(`${nx},${ny}`)) continue; const cost = 1.1 + slope[ni] * 1.4 + ridgeField[ni] * 0.55 + Math.max(0, elevation[ni] - 0.70) * 1.2 - Math.max(roadInfluence[ni], railInfluence2[ni]) * 2.8 - plain[ni] * 0.28 - valleyField[ni] * 0.18 - coastalLowland[ni] * 0.16 + localPenalty[ni] * 0.55; if (cost < bestScore) { bestScore = cost; best = [nx, ny]; } } } if (!best) break; x = best[0]; y = best[1]; const key = `${x},${y}`; visited.add(key); if (key !== lastKey) { out.push([x, y]); lastKey = key; } } return []; } for (const center of candidates) { const path = routeAccess(center); debugLayers.unservedSettlements.push({ x: center.x, y: center.y, kind: "Municipal Center", mode: "municipal-access", repaired: path.length >= 4 }); if (path.length < 4) continue; minorRoads.push(path); debugLayers.repairedSegments.push({ mode: "municipal-local", path, from: "municipal-center", to: "network" }); } } addMunicipalCenterLocalAccess(); nameDebug.maxDerivedPerBase = 0; const prefectureRegions = buildPrefectureRegions(prefectureRegionId, sea, nameFields, seed, usedNames, nameDebug, { modernCities, adminCenters }); const regionalPrefectureBorders = adminRegionalPrefectureBorders || []; if (regionalDebug) { regionalDebug.finalRegionalPrefectureBorderCount = regionalPrefectureBorders.length; regionalDebug.regionalPrefectureBordersRebuiltFromFinalId = true; regionalDebug.promotedPrefectureCapitals = promotedPrefectureCapitals; } 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: regionalPrefectureBorders.length ? regionalPrefectureBorders : prefectureBorder, prefectureRegionId, municipalityToPrefectureId, prefectureRegions, 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, naturalCompartmentId, naturalCompartments, 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, landuse, adminCenters, adminId, adminBorders, adminDebug, castleRuins, riverPaths, mainRivers, tributaryRivers, smallStreams, externalGateways, transportDebug, entitiesForNames, nameDebug, }, options); }