This commit is contained in:
33333-33333 2026-05-29 15:49:09 +09:00
commit 27ceb6568a
7 changed files with 940 additions and 132 deletions

View file

@ -1,6 +1,7 @@
import { createNameDebug } from "./names.js";
import { CELL_SIZE, INF, MAP_H, MAP_W, MinHeap, clamp, indexOf, inside, rand, xyOf } from "./mapUtils.js";
import { applyOutputOptions, attachIdsAndNames, influenceFromPaths, tagInsidePrefecture } from "./mapGeneratorHelpers.js";
import { reconcileMunicipalMetadata } from "./mapMunicipalCoherence.js";
import { routeQualityAcceptable } from "./mapTransport.js";
const MUNICIPAL_SUFFIX_RE = /[市町村区]$/u;
@ -30,15 +31,33 @@ function municipalityNameFromRoot(root, center, fields, seed, ordinal = 0) {
}
function centerMunicipalityId(center, fallback = -1) {
for (const key of ["adminId", "municipalityId", "adminNumericId"]) {
const value = center?.[key];
if (Number.isFinite(value) && value >= 0) return Math.floor(value);
}
return fallback;
}
function assignMunicipalityPopulations(adminCenters, adminId, fields, settlementFeatures = []) {
if (!adminCenters?.length || !adminId) return;
const totals = new Float64Array(adminCenters.length);
const settlementTotals = new Float64Array(adminCenters.length);
const landCells = new Uint32Array(adminCenters.length);
const inhabitedCells = new Uint32Array(adminCenters.length);
const centerById = new Map();
let maxId = -1;
for (const center of adminCenters) {
const id = centerMunicipalityId(center);
if (id >= 0 && !centerById.has(id)) {
centerById.set(id, center);
maxId = Math.max(maxId, id);
}
}
for (let i = 0; i < adminId.length; i++) if (adminId[i] >= 0) maxId = Math.max(maxId, adminId[i]);
const totals = new Float64Array(maxId + 1);
const settlementTotals = new Float64Array(maxId + 1);
const landCells = new Uint32Array(maxId + 1);
const inhabitedCells = new Uint32Array(maxId + 1);
for (let i = 0; i < adminId.length; i++) {
const id = adminId[i];
if (id < 0 || id >= totals.length || fields.sea?.[i]) continue;
if (id < 0 || fields.sea?.[i]) continue;
landCells[id]++;
const density = fields.populationDensity?.[i] || 0;
const lu = fields.landuse?.[i] ?? 0;
@ -68,7 +87,7 @@ function assignMunicipalityPopulations(adminCenters, adminId, fields, settlement
let skippedDuplicateSettlementPopulation = 0;
for (const { feature, i } of uniqueSettlementByCell.values()) {
const id = adminId[i];
if (id < 0 || id >= totals.length) continue;
if (id < 0 || id >= settlementTotals.length) continue;
settlementTotals[id] += feature.population;
}
for (const feature of settlementFeatures || []) {
@ -77,7 +96,7 @@ function assignMunicipalityPopulations(adminCenters, adminId, fields, settlement
const kept = uniqueSettlementByCell.get(key)?.feature;
if (kept && kept !== feature) skippedDuplicateSettlementPopulation += feature.population || 0;
}
for (let id = 0; id < adminCenters.length; id++) {
for (const [id, center] of centerById) {
const raw = (totals[id] || 0) + (settlementTotals[id] || 0);
const minimumResidentPopulation = landCells[id] > 0
? Math.round(Math.max(100, Math.min(3800, 80 + landCells[id] * 9 + inhabitedCells[id] * 16)) / 100) * 100
@ -85,13 +104,13 @@ function assignMunicipalityPopulations(adminCenters, adminId, fields, settlement
const adjustedRaw = Math.max(raw, minimumResidentPopulation);
const rounded = adjustedRaw >= 10000 ? Math.round(adjustedRaw / 1000) * 1000 : Math.max(minimumResidentPopulation, Math.round(adjustedRaw / 100) * 100);
const safePopulation = Math.max(landCells[id] > 0 ? 100 : 0, rounded);
adminCenters[id].municipalityPopulation = safePopulation;
center.municipalityPopulation = safePopulation;
// Some consumers still read the generic `population` field from municipal
// centers. Mirror the municipality total there so no municipality is shown
// as 0人 merely because it is not a canonical city/market entity.
adminCenters[id].population = Math.max(adminCenters[id].population || 0, safePopulation);
adminCenters[id].municipalitySettlementPopulation = Math.max(0, Math.round((settlementTotals[id] || 0) / 100) * 100);
adminCenters[id].municipalitySkippedDuplicateSettlementPopulation = Math.max(0, Math.round(skippedDuplicateSettlementPopulation / 100) * 100);
center.population = Math.max(center.population || 0, safePopulation);
center.municipalitySettlementPopulation = Math.max(0, Math.round((settlementTotals[id] || 0) / 100) * 100);
center.municipalitySkippedDuplicateSettlementPopulation = Math.max(0, Math.round(skippedDuplicateSettlementPopulation / 100) * 100);
}
}
@ -467,7 +486,20 @@ export function finishMapOutput({
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 municipalCoherence = reconcileMunicipalMetadata({
adminId,
prefectureRegionId,
sea,
adminCenters: adminCentersRaw,
municipalityToPrefectureId,
fields: nameFields,
width: MAP_W,
height: MAP_H,
seed,
});
if (adminDebug) adminDebug.municipalCoherence = municipalCoherence.debug;
const coherentMunicipalityToPrefectureId = municipalCoherence.municipalityToPrefectureId || municipalityToPrefectureId;
const adminCenters = attachIdsAndNames(tagInsidePrefecture(municipalCoherence.adminCenters, 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 })),
@ -515,25 +547,27 @@ export function finishMapOutput({
}
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);
const municipalId = centerMunicipalityId(center, index);
center.adminId = municipalId;
center.adminNumericId = municipalId;
center.municipalityId = municipalId;
let candidate = municipalityNameFromRoot(center.municipalityRootName || center.canonicalSettlementName || center.generatedMunicipalityName || center.name, center, nameFields, seed, municipalId);
const generated = municipalityNameFromRoot(center.generatedMunicipalityName || center.name, center, nameFields, seed + 177, municipalId);
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 suffix = municipalitySuffixForCenter(center, nameFields, seed + 313, municipalId);
const rootSource = String(center.generatedMunicipalityName || center.name || center.municipalityRootName || `自治${municipalId + 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] || "里"}`,
`${chars[0] || "里"}${["里", "郷", "野", "田", "川", "原", "浜", "浦"][(seed + municipalId) % 8]}`,
`${["東", "西", "南", "北", "上", "下", "中"][(seed + municipalId) % 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}`;
const root = attempt < alternates.length ? alternates[attempt] : `${(municipalId + attempt) % 10}`;
candidate = `${root}${suffix}`;
}
}
@ -584,7 +618,7 @@ export function finishMapOutput({
return bs - as;
})
.filter((p) => {
const prefId = municipalityToPrefectureId?.[p.municipalityId] ?? -1;
const prefId = coherentMunicipalityToPrefectureId?.[p.municipalityId] ?? -1;
const used = perPrefectureQuota.get(prefId) || 0;
if (used >= 18) return false;
perPrefectureQuota.set(prefId, used + 1);
@ -1233,7 +1267,7 @@ export function finishMapOutput({
humanRegionMask,
prefectureBorder: regionalPrefectureBorders.length ? regionalPrefectureBorders : prefectureBorder,
prefectureRegionId,
municipalityToPrefectureId,
municipalityToPrefectureId: coherentMunicipalityToPrefectureId,
prefectureRegions,
regionalDebug,
terrainDebug,