190 lines
8.8 KiB
JavaScript
190 lines
8.8 KiB
JavaScript
import { MAP_H, MAP_W, clamp, hash2, indexOf, inside, pickEntities, rand } from "./mapUtils.js";
|
|
|
|
export function municipalityCountBoundsForRegion(landCells, meta = {}) {
|
|
// Use the same administrative density curve for the highlighted prefecture
|
|
// and neighboring prefectures. Only clipped slivers get a low floor.
|
|
let min = 1;
|
|
if (landCells >= 360) min = 2;
|
|
if (landCells >= 750) min = 4;
|
|
if (landCells >= 1400) min = 7;
|
|
if (landCells >= 2400) min = 11;
|
|
if (landCells >= 3800) min = 16;
|
|
if (landCells >= 5600) min = 22;
|
|
const max = clamp(Math.round(landCells / 160 + 6), Math.max(min, 4), 72);
|
|
return { min, max };
|
|
}
|
|
|
|
export function computeTargetMunicipalityCount({ prefectureMask, sea, elevation, slope, ridgeField, coastalLowland, basinField, habitability, centrality, accessibility, geographicBarrier, modernCities, markets, ports, satelliteCities, villages, adminRegionMeta = {} }) {
|
|
let landCells = 0;
|
|
let habitableCells = 0;
|
|
let lowlandCells = 0;
|
|
let coastlineComplexity = 0;
|
|
let mountainCells = 0;
|
|
let habitabilitySum = 0;
|
|
let centralitySum = 0;
|
|
let accessibleCells = 0;
|
|
let barrierCells = 0;
|
|
for (let y = 1; y < MAP_H - 1; y++) {
|
|
for (let x = 1; x < MAP_W - 1; x++) {
|
|
const i = indexOf(x, y);
|
|
if (!prefectureMask[i] || sea[i]) continue;
|
|
landCells++;
|
|
if (slope[i] < 0.42 && ridgeField[i] < 0.55 && (!elevation || elevation[i] < 0.72)) habitableCells++;
|
|
if ((coastalLowland[i] > 0.20 || basinField[i] > 0.24) && slope[i] < 0.36 && ridgeField[i] < 0.52) lowlandCells++;
|
|
if (ridgeField[i] > 0.52 || slope[i] > 0.48) mountainCells++;
|
|
habitabilitySum += habitability?.[i] || 0;
|
|
centralitySum += centrality?.[i] || 0;
|
|
if ((accessibility?.[i] || 0) > 0.36 || (centrality?.[i] || 0) > 0.38) accessibleCells++;
|
|
if ((geographicBarrier?.[i] || ridgeField[i]) > 0.58) barrierCells++;
|
|
for (const [nx, ny] of [[x + 1, y], [x - 1, y], [x, y + 1], [x, y - 1]]) {
|
|
const ni = indexOf(nx, ny);
|
|
if (sea[ni]) {
|
|
coastlineComplexity += 1 + coastalLowland[i] * 0.8;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const independentSatellites = (satelliteCities || []).filter((p) => p.municipalityClass === "independentSatelliteMunicipality").length;
|
|
const ruralVillageWeight = Math.sqrt(Math.max(0, villages.length || 0)) * 0.55;
|
|
const settlementWeight = modernCities.length * 1.7 + markets.length * 1.15 + ports.length * 0.8 + independentSatellites * 0.7 + ruralVillageWeight;
|
|
const basinBonus = Math.min(8, [...basinField].filter((v, i) => prefectureMask[i] && !sea[i] && v > 0.34).length / 520);
|
|
const mountainRatio = landCells ? mountainCells / landCells : 0;
|
|
const barrierRatio = landCells ? barrierCells / landCells : 0;
|
|
const avgHabitability = landCells ? habitabilitySum / landCells : 0;
|
|
const avgCentrality = landCells ? centralitySum / landCells : 0;
|
|
const lowlandBonus = Math.min(7, lowlandCells / 430);
|
|
const livingSphereBonus = Math.min(5.5, accessibleCells / 560 + avgCentrality * 3.2 + avgHabitability * 1.6);
|
|
const rawTarget = Math.round(habitableCells / 158 + settlementWeight * 1.02 + coastlineComplexity * 0.032 + basinBonus * 0.70 + lowlandBonus * 1.08 + livingSphereBonus - mountainRatio * 1.70 - barrierRatio * 1.15);
|
|
const { min, max } = municipalityCountBoundsForRegion(landCells, adminRegionMeta);
|
|
return clamp(rawTarget, min, max);
|
|
}
|
|
|
|
export function lowlandAdminSeedScore(i, { elevation, slope, ridgeField, plain, basinField, coastalLowland, settlementScore, populationDensity, roadInfluence, railInfluence2, stationInfluence, landuse, habitability, accessibility, centrality, boundaryAvoidance, adminBoundaryPreference, geographicBarrier }) {
|
|
const lowRelief = clamp((0.70 - elevation[i]) * 1.35) + clamp((0.34 - slope[i]) * 1.55) + clamp((0.48 - ridgeField[i]) * 1.10);
|
|
const landuseFit = [1, 2, 3, 4, 7, 8].includes(landuse[i]) ? 0.40 : landuse[i] === 5 || landuse[i] === 6 ? 0.12 : 0;
|
|
const unifiedNudge = clamp(
|
|
(habitability?.[i] || 0) * 0.05 +
|
|
(accessibility?.[i] || 0) * 0.04 +
|
|
(centrality?.[i] || 0) * 0.04 -
|
|
(adminBoundaryPreference?.[i] || 0) * 0.04 -
|
|
(geographicBarrier?.[i] || 0) * 0.03
|
|
) * 0.20;
|
|
return clamp(
|
|
lowRelief * 0.25 +
|
|
plain[i] * 0.28 +
|
|
basinField[i] * 0.24 +
|
|
coastalLowland[i] * 0.24 +
|
|
settlementScore[i] * 0.30 +
|
|
populationDensity[i] * 0.32 +
|
|
roadInfluence[i] * 0.16 +
|
|
railInfluence2[i] * 0.16 +
|
|
(stationInfluence?.[i] || 0) * 0.18 +
|
|
landuseFit +
|
|
unifiedNudge -
|
|
Math.max(0, elevation[i] - 0.62) * 1.2 -
|
|
Math.max(0, ridgeField[i] - 0.54) * 0.9
|
|
);
|
|
}
|
|
|
|
|
|
export function buildLowlandAdminSeeds({
|
|
seed,
|
|
targetMunicipalityCount,
|
|
prefectureMask,
|
|
sea,
|
|
elevation,
|
|
slope,
|
|
ridgeField,
|
|
plain,
|
|
basinField,
|
|
coastalLowland,
|
|
settlementScore,
|
|
populationDensity,
|
|
roadInfluence,
|
|
railInfluence2,
|
|
stationInfluence,
|
|
landuse,
|
|
habitability,
|
|
accessibility,
|
|
centrality,
|
|
boundaryAvoidance,
|
|
adminBoundaryPreference,
|
|
geographicBarrier,
|
|
modernCities,
|
|
satelliteCities,
|
|
markets,
|
|
ports,
|
|
newTowns,
|
|
stations,
|
|
}) {
|
|
const fields = { elevation, slope, ridgeField, plain, basinField, coastalLowland, settlementScore, populationDensity, roadInfluence, railInfluence2, stationInfluence, landuse, habitability, accessibility, centrality, boundaryAvoidance, adminBoundaryPreference, geographicBarrier };
|
|
function validLowlandPoint(p, strict = true) {
|
|
if (!p || !inside(p.x, p.y)) return false;
|
|
const i = indexOf(p.x, p.y);
|
|
if (!prefectureMask[i] || sea[i]) return false;
|
|
const score = lowlandAdminSeedScore(i, fields);
|
|
const mountain = elevation[i] > 0.70 || slope[i] > 0.52 || ridgeField[i] > 0.62;
|
|
return score >= (strict ? 0.34 : 0.24) && (!mountain || p.isPrefecturalCapital || (p.population || 0) >= 220000 || p.portClass === "major");
|
|
}
|
|
const realSeeds = [];
|
|
for (const city of modernCities || []) {
|
|
if (!validLowlandPoint(city, !city.isPrefecturalCapital)) continue;
|
|
if ((city.population || 0) < 45000) continue;
|
|
const i = indexOf(city.x, city.y);
|
|
realSeeds.push({ x: city.x, y: city.y, score: 1.35 + (city.population || 0) / 650000 + lowlandAdminSeedScore(i, fields), population: city.population || 0, protectedCity: city, seedKind: city.isPrefecturalCapital ? "capital" : "modernCity" });
|
|
}
|
|
for (const city of satelliteCities || []) {
|
|
if (city.municipalityClass !== "independentSatelliteMunicipality" || !validLowlandPoint(city, false)) continue;
|
|
const i = indexOf(city.x, city.y);
|
|
realSeeds.push({ x: city.x, y: city.y, score: 0.92 + (city.population || 0) / 260000 + lowlandAdminSeedScore(i, fields), population: city.population || 0, protectedSatellite: city, seedKind: "satelliteCity" });
|
|
}
|
|
for (const p of [...(markets || []), ...(ports || []), ...(newTowns || [])]) {
|
|
if (!validLowlandPoint(p, true)) continue;
|
|
const i = indexOf(p.x, p.y);
|
|
const portBonus = p.portClass === "major" ? 0.45 : p.portClass === "regional" ? 0.26 : 0;
|
|
realSeeds.push({ x: p.x, y: p.y, score: 0.74 + lowlandAdminSeedScore(i, fields) + portBonus + (p.population || 0) / 420000, population: p.population || 0, portClass: p.portClass, source: p, seedKind: p.portClass ? "port" : "marketTown" });
|
|
}
|
|
const picked = pickEntities(realSeeds, {
|
|
max: targetMunicipalityCount,
|
|
minDistance: 5 + Math.floor(rand(seed, 1302) * 3),
|
|
threshold: 0.62,
|
|
seed: seed + 1300,
|
|
jitter: 0.025,
|
|
});
|
|
const invisibleCandidates = [];
|
|
for (let y = 2; y < MAP_H - 2; y++) {
|
|
for (let x = 2; x < MAP_W - 2; x++) {
|
|
const i = indexOf(x, y);
|
|
if (!prefectureMask[i] || sea[i]) continue;
|
|
const score = lowlandAdminSeedScore(i, fields) + hash2(x, y, seed + 1311) * 0.045;
|
|
if (score < 0.48) continue;
|
|
const insideDenseCore = modernCities.some((city) => (city.population || 0) >= 180000 && Math.hypot(city.x - x, city.y - y) < Math.max(5, (city.coreRadius || 4) * 1.7));
|
|
if (insideDenseCore) continue;
|
|
invisibleCandidates.push({ x, y, score, invisibleLowlandAdminSeed: true, seedKind: "invisibleLowland" });
|
|
}
|
|
}
|
|
if (picked.length < targetMunicipalityCount) {
|
|
const extra = pickEntities(invisibleCandidates, {
|
|
max: targetMunicipalityCount - picked.length,
|
|
minDistance: 5,
|
|
threshold: 0.48,
|
|
seed: seed + 1304,
|
|
jitter: 0.02,
|
|
});
|
|
for (const p of extra) if (picked.every((q) => Math.hypot(p.x - q.x, p.y - q.y) >= 4)) picked.push(p);
|
|
}
|
|
if (picked.length < Math.min(targetMunicipalityCount, 20)) {
|
|
const relaxed = pickEntities(invisibleCandidates, {
|
|
max: Math.min(targetMunicipalityCount, 20) - picked.length,
|
|
minDistance: 4,
|
|
threshold: 0.38,
|
|
seed: seed + 1305,
|
|
jitter: 0.02,
|
|
});
|
|
for (const p of relaxed) if (picked.length < targetMunicipalityCount && picked.every((q) => Math.hypot(p.x - q.x, p.y - q.y) >= 3.5)) picked.push(p);
|
|
}
|
|
return picked.slice(0, targetMunicipalityCount);
|
|
}
|
|
|
|
|