graph
This commit is contained in:
parent
9f49359384
commit
20cdd0ae7c
1 changed files with 231 additions and 9 deletions
240
script.js
240
script.js
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue