map/mapAdminSeedLifecycle.js

200 lines
9.3 KiB
JavaScript
Raw Normal View History

2026-05-28 00:30:09 +09:00
import { INF, clamp, indexOf, inside } from "./mapUtils.js";
import { applyCompartmentOwners, dominantCompartmentOwners, municipalityAreaById } from "./mapAdminShared.js";
export function absorbSeedCompartments(adminId, compartments, seedLifecycle) {
const owner = dominantCompartmentOwners(compartments, adminId);
const absorbed = new Set(seedLifecycle.filter((seed) => seed.state === "absorbed").map((seed) => seed.id));
let changed = 0;
for (const unit of compartments) {
if (!unit || unit.area === 0 || !absorbed.has(owner[unit.id])) continue;
let bestId = -1, bestScore = -INF;
for (const [neighborId, edge] of unit.adjacent) {
const candidate = owner[neighborId];
if (candidate < 0 || absorbed.has(candidate)) continue;
const neighbor = compartments[neighborId];
const score = edge.count * 2 - (edge.target / Math.max(1, edge.count)) * 2 + (neighbor?.area || 0) * 0.002;
if (score > bestScore) { bestScore = score; bestId = candidate; }
}
if (bestId < 0) continue;
owner[unit.id] = bestId;
changed += unit.area;
}
applyCompartmentOwners(adminId, compartments, owner);
return changed;
}
export function splitOversizedLowlandsWithPendingSeeds(adminId, prefectureMask, sea, fields, compartments, adminCenters, seedLifecycle, settlements = []) {
const { plain, agriculture, basinField, coastalLowland, valleyField, ridgeField, slope, elevation } = fields;
const areaById = municipalityAreaById(adminId, prefectureMask, sea);
const areas = [...areaById.values()].sort((a, b) => a - b);
const median = areas.length ? areas[Math.floor(areas.length / 2)] : 0;
if (!median) return { changedCells: 0, splitMunicipalities: 0, pendingSeedsUsed: 0 };
const owner = dominantCompartmentOwners(compartments, adminId);
const unitsByOwner = new Map();
for (const unit of compartments) {
if (!unit || unit.area === 0 || owner[unit.id] < 0) continue;
if (!unitsByOwner.has(owner[unit.id])) unitsByOwner.set(owner[unit.id], []);
unitsByOwner.get(owner[unit.id]).push(unit);
}
const pending = seedLifecycle.filter((seed) => seed.state === "pending" && !seed.protected);
let changedCells = 0;
let splitMunicipalities = 0;
let pendingSeedsUsed = 0;
for (const [id, units] of unitsByOwner) {
const area = areaById.get(id) || 0;
if (area < Math.max(260, median * 1.45) || units.length < 6) continue;
let lowland = 0, rough = 0;
for (const unit of units) {
for (const i of unit.cells) {
lowland += (plain[i] || 0) * 0.38 + (agriculture[i] || 0) * 0.20 + basinField[i] * 0.22 + coastalLowland[i] * 0.22 + valleyField[i] * 0.10;
rough += ridgeField[i] * 0.48 + slope[i] * 0.34 + Math.max(0, elevation[i] - 0.58) * 0.30;
}
}
if (lowland / area < 0.26 || rough / area > 0.48) continue;
const localPending = pending.filter((seed) => {
const center = adminCenters[seed.id];
if (!center || !inside(center.x, center.y)) return false;
const centerOwner = adminId[indexOf(center.x, center.y)];
return centerOwner === id || Math.hypot(center.x - (adminCenters[id]?.x || center.x), center.y - (adminCenters[id]?.y || center.y)) < 28;
});
const localSettlements = settlements.filter((p) => p && inside(p.x, p.y) && adminId[indexOf(p.x, p.y)] === id);
if (localPending.length === 0 || localPending.length + localSettlements.length < 2) continue;
let municipalitySplit = false;
for (const seed of localPending.slice(0, 3)) {
const center = adminCenters[seed.id];
if (!center) continue;
const targetArea = clamp(95 + (center.score || 0.5) * 60, 90, 180);
let claimed = 0;
const candidates = units
.filter((unit) => owner[unit.id] === id && unit.classId !== 8 && unit.classId !== 9)
.map((unit) => ({
unit,
score: Math.hypot(unit.x - center.x, unit.y - center.y) - (unit.lowlandFitness || 0) * 8 - (unit.urbanWeight || 0) * 3,
}))
.sort((a, b) => a.score - b.score);
if (candidates.length < 2) continue;
for (const { unit } of candidates) {
if (claimed >= targetArea && claimed >= 2) break;
owner[unit.id] = seed.id;
claimed += unit.area;
changedCells += unit.area;
}
if (claimed >= 45) {
seed.state = "survived";
seed.area = claimed;
pendingSeedsUsed++;
municipalitySplit = true;
}
}
if (municipalitySplit) splitMunicipalities++;
}
applyCompartmentOwners(adminId, compartments, owner);
return { changedCells, splitMunicipalities, pendingSeedsUsed };
}
export function promotePendingSeedsForMunicipalityCount(adminId, prefectureMask, sea, fields, compartments, adminCenters, seedLifecycle, targetMinCount = 20) {
const { plain, agriculture, basinField, coastalLowland, valleyField, ridgeField, slope, elevation } = fields;
let areaById = municipalityAreaById(adminId, prefectureMask, sea);
let currentCount = areaById.size;
if (currentCount >= targetMinCount) return { changedCells: 0, promotedSeeds: 0 };
const owner = dominantCompartmentOwners(compartments, adminId);
let changedCells = 0;
let promotedSeeds = 0;
const pending = seedLifecycle
.filter((seed) => seed.state === "pending" && !seed.protected)
.sort((a, b) => (adminCenters[b.id]?.score || 0) - (adminCenters[a.id]?.score || 0));
for (const seed of pending) {
if (currentCount >= targetMinCount) break;
const center = adminCenters[seed.id];
if (!center || !inside(center.x, center.y)) continue;
const existingArea = areaById.get(seed.id) || 0;
if (existingArea >= 12) {
seed.state = "survived";
seed.area = existingArea;
promotedSeeds++;
continue;
}
const candidates = compartments
.filter((unit) => {
if (!unit || unit.area === 0) return false;
const currentOwner = owner[unit.id];
if (currentOwner < 0 || currentOwner === seed.id) return false;
const ownerArea = areaById.get(currentOwner) || 0;
if (ownerArea < 90) return false;
const lowlandFit = (unit.lowlandFitness || 0) + (unit.basinIdentity || 0) * 0.15 + (unit.coastalExposure || 0) * 0.15;
if (lowlandFit < 0.26) return false;
return Math.hypot(unit.x - center.x, unit.y - center.y) < 36;
})
.map((unit) => ({
unit,
score: Math.hypot(unit.x - center.x, unit.y - center.y) - (unit.lowlandFitness || 0) * 6 - (unit.urbanWeight || 0) * 2,
}))
.sort((a, b) => a.score - b.score);
if (candidates.length === 0) continue;
let claimed = 0;
for (const { unit } of candidates) {
const currentOwner = owner[unit.id];
if ((areaById.get(currentOwner) || 0) - unit.area < 70) continue;
owner[unit.id] = seed.id;
areaById.set(currentOwner, (areaById.get(currentOwner) || 0) - unit.area);
areaById.set(seed.id, (areaById.get(seed.id) || 0) + unit.area);
claimed += unit.area;
changedCells += unit.area;
if (claimed >= 55) break;
}
if (claimed >= 25) {
seed.state = "survived";
seed.area = areaById.get(seed.id) || claimed;
promotedSeeds++;
currentCount++;
}
}
applyCompartmentOwners(adminId, compartments, owner);
return { changedCells, promotedSeeds };
}
export function restoreSurvivedSeedsByCompartment(adminId, prefectureMask, sea, compartments, adminCenters, seedLifecycle, targetMinCount = 20) {
const areaById = municipalityAreaById(adminId, prefectureMask, sea);
let currentCount = areaById.size;
if (currentCount >= targetMinCount) return { changedCells: 0, restoredSeeds: 0 };
const owner = dominantCompartmentOwners(compartments, adminId);
let changedCells = 0;
let restoredSeeds = 0;
const missing = seedLifecycle
.filter((seed) => (seed.state === "survived" || seed.protected) && (areaById.get(seed.id) || 0) === 0)
.sort((a, b) => (b.protected ? 1 : 0) - (a.protected ? 1 : 0));
for (const seed of missing) {
if (currentCount >= targetMinCount) break;
const center = adminCenters[seed.id];
if (!center || !inside(center.x, center.y)) continue;
const candidates = compartments
.filter((unit) => {
if (!unit || unit.area === 0 || unit.classId === 8 || unit.classId === 9) return false;
const currentOwner = owner[unit.id];
if (currentOwner < 0 || currentOwner === seed.id) return false;
if ((areaById.get(currentOwner) || 0) - unit.area < 25) return false;
return Math.hypot(unit.x - center.x, unit.y - center.y) < 90 && ((unit.lowlandFitness || 0) > 0.08 || seed.protected);
})
.map((unit) => ({
unit,
score: Math.hypot(unit.x - center.x, unit.y - center.y) - (unit.lowlandFitness || 0) * 6 - (unit.urbanWeight || 0) * 2 + (unit.classId === 8 || unit.classId === 9 ? 20 : 0),
}))
.sort((a, b) => a.score - b.score);
if (candidates.length === 0) continue;
const unit = candidates[0].unit;
const oldOwner = owner[unit.id];
if ((areaById.get(oldOwner) || 0) - unit.area < 25) continue;
owner[unit.id] = seed.id;
const claimed = unit.area;
areaById.set(oldOwner, (areaById.get(oldOwner) || 0) - claimed);
changedCells += claimed;
areaById.set(seed.id, claimed);
seed.area = claimed;
restoredSeeds++;
currentCount++;
}
applyCompartmentOwners(adminId, compartments, owner);
return { changedCells, restoredSeeds };
}