200 lines
9.3 KiB
JavaScript
200 lines
9.3 KiB
JavaScript
|
|
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 };
|
||
|
|
}
|
||
|
|
|
||
|
|
|