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 }; }