663 lines
28 KiB
JavaScript
663 lines
28 KiB
JavaScript
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);
|
|
}
|