This commit is contained in:
33333-33333 2026-05-12 18:43:27 +09:00
commit 20cdd0ae7c

240
script.js
View file

@ -1064,7 +1064,11 @@ class Simulation {
cityIds: new Set([centerCity.id]),
treasury: 0,
color: hslToRgb((id * 0.38196601125) % 1, 0.58, 0.62),
founded: this.year
founded: this.year,
legitimacy: this.rng.range(0.55, 0.9),
cohesion: this.rng.range(0.45, 0.85),
crisis: 0,
lastCrisisYear: this.year
};
centerCity.polityId = id;
centerCity.loyalty = 1;
@ -1159,6 +1163,90 @@ class Simulation {
city.receivedAid = false;
}
polityAge(polity) {
return (this.year - (polity?.founded ?? this.year)) / MONTHS_PER_YEAR;
}
polityAgePressure(polity) {
const age = this.polityAge(polity);
if (age < 80) return 0;
const x = (age - 80) / 420;
return clamp(1 - Math.exp(-x), 0, 1.35);
}
polityOverextension(polity) {
const cities = this.getPolityCities(polity);
const center = this.getCityById(polity.centerCityId);
if (!center || cities.length <= 1) return 0;
const cityCountPressure = Math.max(0, cities.length - 3) * 0.18;
let distanceSum = 0;
let count = 0;
for (const city of cities) {
if (city.id === center.id) continue;
distanceSum += this.effectiveDistance(center, city);
count++;
}
const avgDistance = count ? distanceSum / count : 0;
const distancePressure = Math.max(0, avgDistance - 18) * 0.018;
return clamp(cityCountPressure + distancePressure, 0, 2);
}
polityResourceStress(polity) {
const cities = this.getPolityCities(polity);
if (!cities.length) return 1;
let poor = 0;
for (const city of cities) {
const perCapita = city.storedResources / Math.max(1, city.population);
if (perCapita < 0.08) poor++;
}
const povertyRate = poor / cities.length;
const treasuryPerCity = (polity.treasury || 0) / Math.max(1, cities.length);
const treasuryStress = treasuryPerCity < 4 ? (4 - treasuryPerCity) / 4 : 0;
return clamp(povertyRate * 0.75 + treasuryStress * 0.45, 0, 1.5);
}
polityEthnicFragmentation(polity) {
const cities = this.getPolityCities(polity);
if (cities.length <= 1) return 0;
const counts = new Map();
for (const city of cities) {
const e = this.dominantCityEthnicity(city);
if (e !== null) counts.set(e, (counts.get(e) || 0) + 1);
}
if (!counts.size) return 0;
let max = 0;
for (const v of counts.values()) max = Math.max(max, v);
const dominantShare = max / cities.length;
return clamp(1 - dominantShare, 0, 1);
}
polityInstability(polity) {
const agePressure = this.polityAgePressure(polity);
const overextension = this.polityOverextension(polity);
const resourceStress = this.polityResourceStress(polity);
const fragmentation = this.polityEthnicFragmentation(polity);
const legitimacyBuffer = (polity.legitimacy ?? 0.7) * 0.65;
const cohesionBuffer = (polity.cohesion ?? 0.6) * 0.45;
return clamp(
agePressure * 0.55 +
overextension * 0.35 +
resourceStress * 0.55 +
fragmentation * 0.35 +
(polity.crisis || 0) * 0.65 -
legitimacyBuffer -
cohesionBuffer,
0,
2.5
);
}
foundPolities() {
for (const city of this.cities) {
if (city.polityId !== null || city.population < 80 || city.storedResources < 30) continue;
@ -1219,6 +1307,19 @@ class Simulation {
city.storedResources -= tax;
polity.treasury += tax;
}
const agePressure = this.polityAgePressure(polity);
const overextension = this.polityOverextension(polity);
const adminCost =
cities.length * 0.45 +
overextension * 2.2 +
agePressure * cities.length * 0.35;
polity.treasury -= adminCost;
if (polity.treasury < 0) {
const deficit = Math.abs(polity.treasury);
polity.treasury = 0;
polity.crisis = clamp((polity.crisis || 0) + deficit * 0.025, 0, 1.5);
}
const maintenance = Math.pow(cities.length, 1.15) * 0.18;
if (maintenance > 0) {
const center = this.getCityById(centerId);
@ -1253,11 +1354,95 @@ class Simulation {
}
}
erodePolityLegitimacy() {
for (const polity of this.polities) {
const agePressure = this.polityAgePressure(polity);
const resourceStress = this.polityResourceStress(polity);
const overextension = this.polityOverextension(polity);
const erosion =
0.004 +
agePressure * 0.006 +
resourceStress * 0.006 +
overextension * 0.003;
polity.legitimacy = clamp((polity.legitimacy ?? 0.7) - erosion, 0, 1);
polity.cohesion = clamp((polity.cohesion ?? 0.6) - erosion * 0.45, 0, 1);
const cities = this.getPolityCities(polity);
if (this.polityAge(polity) < 80 && polity.treasury > cities.length * 10) {
polity.legitimacy = clamp(polity.legitimacy + 0.006, 0, 1);
polity.cohesion = clamp(polity.cohesion + 0.003, 0, 1);
}
polity.crisis = clamp((polity.crisis || 0) * 0.92, 0, 1.5);
}
}
triggerPolityCrises() {
for (const polity of this.polities) {
const age = this.polityAge(polity);
if (age < 60) continue;
if ((this.year - (polity.lastCrisisYear ?? 0)) / MONTHS_PER_YEAR < 96) continue;
const instability = this.polityInstability(polity);
const agePressure = this.polityAgePressure(polity);
const chance = 0.015 + agePressure * 0.035 + instability * 0.025;
if (this.rng.next() >= chance) continue;
polity.lastCrisisYear = this.year;
const severity = clamp(
0.12 +
agePressure * this.rng.range(0.12, 0.28) +
instability * this.rng.range(0.08, 0.22),
0.08,
0.55
);
polity.crisis = clamp((polity.crisis || 0) + severity, 0, 1.5);
polity.legitimacy = clamp((polity.legitimacy ?? 0.7) - severity * 0.45, 0, 1);
polity.cohesion = clamp((polity.cohesion ?? 0.6) - severity * 0.25, 0, 1);
const center = this.getCityById(polity.centerCityId);
for (const city of this.getPolityCities(polity)) {
if (city.id === polity.centerCityId) continue;
const distance = center ? this.effectiveDistance(center, city) : 40;
const distanceFactor = clamp(distance / 40, 0.25, 1);
city.loyalty = clamp(city.loyalty - severity * distanceFactor, 0, 1);
}
}
}
applyOldStateStress() {
for (const polity of this.polities) {
const age = this.polityAge(polity);
if (age < 1000) continue;
const cities = this.getPolityCities(polity);
if (cities.length <= 1) continue;
const oldAge = clamp((age - 1000) / 1000, 0, 1);
polity.legitimacy = clamp((polity.legitimacy ?? 0.7) - oldAge * 0.018, 0, 1);
polity.cohesion = clamp((polity.cohesion ?? 0.6) - oldAge * 0.012, 0, 1);
polity.crisis = clamp((polity.crisis || 0) + oldAge * 0.035, 0, 1.5);
const center = this.getCityById(polity.centerCityId);
for (const city of cities) {
if (city.id === polity.centerCityId) continue;
const distance = center ? this.effectiveDistance(center, city) : 40;
const distanceFactor = clamp(distance / 35, 0.2, 1);
city.loyalty = clamp(city.loyalty - oldAge * distanceFactor * 0.045, 0, 1);
}
}
}
updateCityLoyalty() {
for (const polity of this.polities) {
const center = this.getCityById(polity.centerCityId);
if (!center) continue;
center.loyalty = 1;
const instability = this.polityInstability(polity);
const agePressure = this.polityAgePressure(polity);
const overextension = this.polityOverextension(polity);
for (const city of this.getPolityCities(polity)) {
if (city.id === center.id) continue;
const perCapita = city.storedResources / Math.max(1, city.population);
@ -1270,6 +1455,11 @@ class Simulation {
if (city.receivedAid) delta += 0.04;
delta -= 0.01;
if (city.storedResources < city.population * 0.05) delta -= 0.06;
delta -= instability * 0.035;
delta -= agePressure * 0.018;
delta -= overextension * clamp(distance / 40, 0, 1) * 0.025;
if ((polity.legitimacy ?? 0.7) < 0.25) delta -= 0.035;
if ((polity.crisis || 0) > 0.6) delta -= 0.025;
city.loyalty = clamp(city.loyalty + delta, 0, 1);
}
}
@ -1277,9 +1467,15 @@ class Simulation {
splitUnloyalCities() {
for (const polity of this.polities) {
const instability = this.polityInstability(polity);
const agePressure = this.polityAgePressure(polity);
const threshold = clamp(0.2 + instability * 0.08 + agePressure * 0.05, 0.2, 0.38);
for (const city of this.getPolityCities(polity)) {
if (city.id === polity.centerCityId || city.loyalty >= 0.2) continue;
const chance = (0.2 - city.loyalty) * 0.4;
if (city.id === polity.centerCityId || city.loyalty >= threshold) continue;
const chance =
(threshold - city.loyalty) * 0.55 +
instability * 0.025 +
agePressure * 0.02;
if (this.rng.next() < chance) this.removeCityFromPolity(city);
}
}
@ -1298,11 +1494,19 @@ class Simulation {
center = cities.reduce((best, city) => this.cityInfluence(city) > this.cityInfluence(best) ? city : best, cities[0]);
polity.centerCityId = center.id;
center.loyalty = 1;
polity.treasury *= 0.5;
polity.legitimacy = clamp((polity.legitimacy ?? 0.7) - 0.22, 0, 1);
polity.cohesion = clamp((polity.cohesion ?? 0.6) - 0.16, 0, 1);
polity.crisis = clamp((polity.crisis || 0) + 0.35, 0, 1.5);
polity.lastCrisisYear = this.year;
for (const city of cities) {
if (city.id !== center.id) city.loyalty = clamp(city.loyalty - 0.18, 0, 1);
}
const history = this.ensurePolityHistory(polity);
if (history) history.centerCityId = center.id;
}
if (cities.length === 1) {
this.markPolityEnded(polity, "dissolved");
this.markPolityEnded(polity, "fragmented");
cities[0].polityId = null;
cities[0].loyalty = 0.45;
cities[0].receivedAid = false;
@ -1322,16 +1526,19 @@ class Simulation {
}
updatePolities() {
if (this.year % years(10) !== 0) return;
if (this.year % years(1) !== 0) return;
this.cleanupPolities();
this.foundPolities();
this.expandPolities();
this.reinforcePolityTradeRoutes();
if (this.reinforcePolityTradeRoutes) this.reinforcePolityTradeRoutes();
this.collectAndRedistributeResources();
this.erodePolityLegitimacy();
this.triggerPolityCrises();
this.applyOldStateStress();
this.updateCityLoyalty();
this.splitUnloyalCities();
this.cleanupPolities();
this.samplePolityHistories();
if (this.samplePolityHistories) this.samplePolityHistories();
}
reinforcePolityTradeRoutes() {
@ -1349,6 +1556,13 @@ class Simulation {
const target = candidates[Math.min(candidates.length - 1, this.rng.int(Math.min(3, candidates.length)))];
const path = this.findTerrainRoute(center.x, center.y, target.x, target.y);
if (!this.isValidRoutePath(path, target.x, target.y)) continue;
const cities = this.getPolityCities(polity);
const roadCost = 8 + cities.length * 1.5 + this.polityOverextension(polity) * 3;
if ((polity.treasury || 0) < roadCost) {
polity.crisis = clamp((polity.crisis || 0) + 0.025, 0, 1.5);
continue;
}
polity.treasury -= roadCost;
if (!this.payRouteConstructionCost(center, target, path, polity)) continue;
center.tradeLinks.add(target.id);
@ -1677,7 +1891,11 @@ class Simulation {
cityIds: [...p.cityIds],
treasury: p.treasury,
color: p.color,
founded: p.founded
founded: p.founded,
legitimacy: p.legitimacy,
cohesion: p.cohesion,
crisis: p.crisis,
lastCrisisYear: p.lastCrisisYear
})),
polityHistory: [...this.polityHistory.values()],
deadPolityHistories: this.deadPolityHistories
@ -1746,7 +1964,11 @@ class Simulation {
cityIds: new Set(p.cityIds || []),
treasury: p.treasury || 0,
color: p.color || hslToRgb((p.id * 0.38196601125) % 1, 0.58, 0.62),
founded: p.founded || 0
founded: p.founded || 0,
legitimacy: p.legitimacy ?? 0.7,
cohesion: p.cohesion ?? 0.6,
crisis: p.crisis ?? 0,
lastCrisisYear: p.lastCrisisYear ?? sim.year
}));
sim.polityHistory = new Map((state.polityHistory || []).map(h => [h.id, {
id: h.id,