This commit is contained in:
33333-33333 2026-05-26 00:45:01 +09:00
commit 5c82bfcab7
13 changed files with 1028 additions and 8801 deletions

View file

@ -950,6 +950,104 @@ function maskLandArea(mask, sea) {
return area;
}
function connectedMaskComponents(mask, sea, minArea = 1) {
const seen = new Uint8Array(SIZE);
const components = [];
for (let i = 0; i < SIZE; i++) {
if (!mask[i] || sea[i] || seen[i]) continue;
const queue = [i];
const cells = [];
seen[i] = 1;
let head = 0;
while (head < queue.length) {
const cur = queue[head++];
cells.push(cur);
const [x, y] = xyOf(cur);
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1]]) {
const nx = x + dx;
const ny = y + dy;
if (!inside(nx, ny)) continue;
const ni = indexOf(nx, ny);
if (!mask[ni] || sea[ni] || seen[ni]) continue;
seen[ni] = 1;
queue.push(ni);
}
}
if (cells.length >= minArea) {
let minX = MAP_W, minY = MAP_H, maxX = 0, maxY = 0;
for (const cell of cells) {
const [x, y] = xyOf(cell);
minX = Math.min(minX, x);
minY = Math.min(minY, y);
maxX = Math.max(maxX, x);
maxY = Math.max(maxY, y);
}
components.push({ cells, area: cells.length, minX, minY, maxX, maxY });
}
}
return components.sort((a, b) => b.area - a.area);
}
function maskFromCells(cells) {
const mask = new Uint8Array(SIZE);
for (const i of cells || []) mask[i] = 1;
return mask;
}
function splitDisconnectedAdminComponents(adminId, humanMask, sea, centers = [], fields = {}) {
let nextId = Math.max(-1, ...adminId) + 1;
let splitCount = 0;
const ids = [...new Set([...adminId].filter((id, i) => id >= 0 && humanMask[i] && !sea[i]))];
for (const id of ids) {
const seen = new Uint8Array(SIZE);
const components = [];
for (let i = 0; i < SIZE; i++) {
if (adminId[i] !== id || !humanMask[i] || sea[i] || seen[i]) continue;
const cells = [];
const queue = [i];
seen[i] = 1;
let head = 0;
while (head < queue.length) {
const cur = queue[head++];
cells.push(cur);
const [x, y] = xyOf(cur);
for (const [dx, dy] of [[1,0],[-1,0],[0,1],[0,-1]]) {
const nx = x + dx;
const ny = y + dy;
if (!inside(nx, ny)) continue;
const ni = indexOf(nx, ny);
if (adminId[ni] !== id || !humanMask[ni] || sea[ni] || seen[ni]) continue;
seen[ni] = 1;
queue.push(ni);
}
}
components.push(cells);
}
if (components.length <= 1) continue;
components.sort((a, b) => b.length - a.length);
for (let c = 1; c < components.length; c++) {
const newId = nextId++;
let bestI = components[c][0];
let bestScore = -INF;
for (const i of components[c]) {
const score =
(fields.populationDensity?.[i] || 0) * 4.0 +
(fields.stationInfluence?.[i] || 0) * 0.8 +
(fields.roadInfluence?.[i] || 0) * 0.45 +
(fields.settlementScore?.[i] || 0) * 0.6 +
(fields.plain?.[i] || 0) * 0.2 -
(fields.slope?.[i] || 0) * 0.2;
if (score > bestScore) { bestScore = score; bestI = i; }
}
const [x, y] = xyOf(bestI);
for (const i of components[c]) adminId[i] = newId;
centers[newId] = { ...(centers[id] || {}), x, y, score: bestScore, seedKind: "splitDisconnectedMunicipality", generatedOfficePoint: true, municipalityOffice: true };
splitCount++;
}
}
return { adminId, centers, splitDisconnectedMunicipalityCount: splitCount };
}
function compactAdminIdsAndCenters(adminId, humanMask, sea, centers = [], fields = {}) {
const activeIds = [...new Set([...adminId].filter((id, i) => id >= 0 && humanMask[i] && !sea[i]))].sort((a, b) => a - b);
@ -968,11 +1066,12 @@ function compactAdminIdsAndCenters(adminId, humanMask, sea, centers = [], fields
const chooseOffice = (newId, oldId) => {
const cells = cellsByNewId[newId] || [];
const current = centers[oldId];
let currentValid = false;
let currentScore = -INF;
if (current && inside(current.x, current.y)) {
const ci = indexOf(current.x, current.y);
if (newAdminId[ci] === newId && humanMask[ci] && !sea[ci]) {
return { ...current, localAdminId: newId, oldAdminId: oldId, municipalityOffice: true };
}
currentValid = newAdminId[ci] === newId && humanMask[ci] && !sea[ci];
if (currentValid) currentScore = (fields.populationDensity?.[ci] || 0) + (fields.stationInfluence?.[ci] || 0) * 0.32 + (fields.roadInfluence?.[ci] || 0) * 0.20;
}
let sx = 0, sy = 0;
for (const i of cells) {
@ -991,19 +1090,20 @@ function compactAdminIdsAndCenters(adminId, humanMask, sea, centers = [], fields
const density = fields.populationDensity?.[i] || 0;
const settlement = fields.settlementScore?.[i] || 0;
const score =
density * 2.25 +
settlement * 0.75 +
urbanBonus +
density * 4.20 +
settlement * 0.72 +
urbanBonus * 1.15 +
(fields.plain?.[i] || 0) * 0.32 +
(fields.basinField?.[i] || 0) * 0.24 +
(fields.coastalLowland?.[i] || 0) * 0.18 +
(fields.roadInfluence?.[i] || 0) * 0.34 +
(fields.stationInfluence?.[i] || 0) * 0.45 -
(fields.roadInfluence?.[i] || 0) * 0.48 +
(fields.stationInfluence?.[i] || 0) * 0.86 -
(fields.slope?.[i] || 0) * 0.52 -
Math.hypot(x - cx, y - cy) * 0.018 +
hash2(x, y, 91337 + newId) * 0.012;
if (score > bestScore) { bestScore = score; bestI = i; }
}
if (currentValid && currentScore >= bestScore * 0.92 && (fields.populationDensity?.[indexOf(current.x, current.y)] || 0) >= 0.16) bestI = indexOf(current.x, current.y);
const [bx, by] = bestI >= 0 ? xyOf(bestI) : [Math.round(cx), Math.round(cy)];
return {
...(current || {}),
@ -1041,11 +1141,17 @@ function discoverAdminRegionIds(prefectureMask, prefectureRegionId, sea) {
export function generateAdminLayout(context) {
const { prefectureMask, prefectureRegionId, sea, populationDensity, plain, slope, adminProgress } = context;
const minFullAdminRegionArea = 1500;
const regionIds = discoverAdminRegionIds(prefectureMask, prefectureRegionId, sea)
.filter((regionId) => regionId !== OUTER_ANCHOR_REGION_ID && maskLandArea(buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId), sea) >= minFullAdminRegionArea);
const minFullAdminRegionArea = 650;
const minComponentArea = 18;
const regionComponents = [];
for (const regionId of discoverAdminRegionIds(prefectureMask, prefectureRegionId, sea)) {
const baseMask = buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId);
const components = connectedMaskComponents(baseMask, sea, minComponentArea);
components.forEach((component, componentIndex) => regionComponents.push({ regionId, componentIndex, component, area: component.area }));
}
const fullComponents = regionComponents.filter((row) => row.area >= minFullAdminRegionArea);
if (!prefectureRegionId || regionIds.length <= 1) return generateAdminLayoutForMask(context);
if (!prefectureRegionId || fullComponents.length <= 1) return generateAdminLayoutForMask(context);
let combinedAdminId = new Int16Array(SIZE);
combinedAdminId.fill(-1);
@ -1055,14 +1161,15 @@ export function generateAdminLayout(context) {
const perRegion = [];
let idOffset = 0;
for (const regionId of regionIds) {
const regionMask = buildRegionMask(prefectureMask, prefectureRegionId, sea, regionId);
const regionArea = maskLandArea(regionMask, sea);
if (regionArea < minFullAdminRegionArea) continue;
const processedCells = new Uint8Array(SIZE);
for (const row of fullComponents) {
const { regionId, componentIndex, component } = row;
const regionMask = maskFromCells(component.cells);
const regionArea = component.area;
const localContext = {
...context,
seed: (context.seed + regionId * 10007) >>> 0,
seed: (context.seed + regionId * 10007 + componentIndex * 9973) >>> 0,
prefectureMask: regionMask,
modernCities: filterPointsForMask(context.modernCities, regionMask, sea),
satelliteCities: filterPointsForMask(context.satelliteCities, regionMask, sea),
@ -1074,9 +1181,11 @@ export function generateAdminLayout(context) {
industrialZones: filterPointsForMask(context.industrialZones, regionMask, sea),
logisticsParks: filterPointsForMask(context.logisticsParks, regionMask, sea),
adminRegionMeta: {
regionId,
regionId: `${regionId}:${componentIndex}`,
sourceRegionId: regionId,
componentIndex,
landArea: regionArea,
isFocusedRegion: true,
isFocusedRegion: regionId === 0,
isOuterAnchorRegion: regionId === OUTER_ANCHOR_REGION_ID,
},
adminProgress,
@ -1121,12 +1230,14 @@ export function generateAdminLayout(context) {
for (let i = 0; i < SIZE; i++) {
if (!regionMask[i] || sea[i]) continue;
combinedHumanMask[i] = 1;
processedCells[i] = 1;
const localId = local.adminId?.[i] ?? -1;
if (localId >= 0) combinedAdminId[i] = localId + idOffset;
}
perRegion.push({
regionId,
componentIndex,
area: regionArea,
centerCount: localCenters.length,
municipalityCount: local.adminDebug?.finalMunicipalityCount || local.adminDebug?.actualMunicipalityCount || new Set([...local.adminId].filter((id, i) => id >= 0 && regionMask[i] && !sea[i])).size,
@ -1141,34 +1252,29 @@ export function generateAdminLayout(context) {
idOffset += localSlotCount;
}
const leftoverByRegion = new Map();
for (let i = 0; i < SIZE; i++) {
if (sea[i]) continue;
const regionId = adminGenerationRegionIdAt(i, prefectureMask, prefectureRegionId);
if ((regionId < 0 && regionId !== OUTER_ANCHOR_REGION_ID) || combinedAdminId[i] >= 0) continue;
if (!leftoverByRegion.has(regionId)) leftoverByRegion.set(regionId, []);
leftoverByRegion.get(regionId).push(i);
const leftoverRows = [];
for (const row of regionComponents) {
const cells = row.component.cells.filter((i) => !sea[i] && combinedAdminId[i] < 0);
if (cells.length) leftoverRows.push({ ...row, cells });
}
for (const [regionId, cells] of leftoverByRegion) {
let sx = 0, sy = 0, bestI = cells[0], bestScore = -INF;
for (const row of leftoverRows) {
const { regionId, componentIndex, cells } = row;
let bestI = cells[0], bestScore = -INF;
for (const i of cells) {
const [x, y] = xyOf(i);
sx += x;
sy += y;
const score = (populationDensity?.[i] || 0) + (plain?.[i] || 0) * 0.2 - (slope?.[i] || 0) * 0.2;
const score = (populationDensity?.[i] || 0) * 2.6 + (plain?.[i] || 0) * 0.22 - (slope?.[i] || 0) * 0.20;
if (score > bestScore) { bestScore = score; bestI = i; }
}
const [cx, cy] = xyOf(bestI);
const id = combinedCenters.length;
combinedCenters.push({ x: cx, y: cy, score: bestScore, regionId, localAdminId: 0, seedKind: "tinyRegionAdminSeed", invisibleLowlandAdminSeed: true });
combinedCenters.push({ x: cx, y: cy, score: bestScore, regionId, componentIndex, localAdminId: 0, seedKind: "tinyRegionAdminSeed", invisibleLowlandAdminSeed: true });
for (const i of cells) {
combinedHumanMask[i] = 1;
combinedAdminId[i] = id;
}
perRegion.push({ regionId, area: cells.length, centerCount: 1, municipalityCount: 1, naturalCompartmentCount: 1, targetNaturalCompartmentCount: 1, averageCompartmentArea: cells.length, maxCompartmentArea: cells.length, maxCompartmentElongation: 1, singleCompartmentMunicipalityRatio: 1, tinyRegionFallback: true });
perRegion.push({ regionId, componentIndex, area: cells.length, centerCount: 1, municipalityCount: 1, naturalCompartmentCount: 1, targetNaturalCompartmentCount: 1, averageCompartmentArea: cells.length, maxCompartmentArea: cells.length, maxCompartmentElongation: 1, singleCompartmentMunicipalityRatio: 1, tinyRegionFallback: true });
}
const compactedAdmin = compactAdminIdsAndCenters(combinedAdminId, combinedHumanMask, sea, combinedCenters, {
const compactFields = {
populationDensity,
plain,
slope,
@ -1178,7 +1284,14 @@ export function generateAdminLayout(context) {
coastalLowland: context.coastalLowland,
roadInfluence: context.roadInfluence,
stationInfluence: context.stationInfluence,
});
};
let compactedAdmin = compactAdminIdsAndCenters(combinedAdminId, combinedHumanMask, sea, combinedCenters, compactFields);
combinedAdminId = compactedAdmin.adminId;
combinedCenters = compactedAdmin.adminCenters;
const splitDisconnected = splitDisconnectedAdminComponents(combinedAdminId, combinedHumanMask, sea, combinedCenters, compactFields);
combinedAdminId = splitDisconnected.adminId;
combinedCenters = splitDisconnected.centers;
compactedAdmin = compactAdminIdsAndCenters(combinedAdminId, combinedHumanMask, sea, combinedCenters, compactFields);
combinedAdminId = compactedAdmin.adminId;
combinedCenters = compactedAdmin.adminCenters;
const adminBorders = extractAdminBorderSegments(combinedAdminId, combinedHumanMask);
@ -1191,6 +1304,11 @@ export function generateAdminLayout(context) {
multiRegionAdmin: true,
adminRegionCount: perRegion.length,
minFullAdminRegionArea,
minComponentArea,
connectedComponentAdmin: true,
fullComponentCount: fullComponents.length,
leftoverComponentCount: leftoverRows.length,
splitDisconnectedMunicipalityCount: splitDisconnected.splitDisconnectedMunicipalityCount,
perRegion,
finalMunicipalityCount: totalMunicipalityCount,
actualMunicipalityCount: totalMunicipalityCount,