diff --git a/index.html b/index.html
index 4b44b7f..96451c4 100644
--- a/index.html
+++ b/index.html
@@ -67,6 +67,7 @@
Ethnicities0
Cities0
States0
+ Wars0
Trade routes0
Farming knowledge0.000
Metallurgy knowledge0.000
diff --git a/script.js b/script.js
index 78b0d28..08daf48 100644
--- a/script.js
+++ b/script.js
@@ -35,9 +35,14 @@ const SimConfig = Object.freeze({
pressurePenaltyBase: 0.2,
pressureReproductionPenalty: 0.16
}),
+ city: Object.freeze({
+ maxCities: 300
+ }),
render: Object.freeze({
graphThrottleMs: 700,
- statsThrottleMs: 350
+ statsThrottleMs: 350,
+ historyWindowYears: 2000,
+ historySamplePaddingYears: 240
}),
save: Object.freeze({
key: SAVE_KEY,
@@ -84,6 +89,7 @@ const els = {
ethnicities: document.getElementById("ethnicities"),
cities: document.getElementById("cities"),
polities: document.getElementById("polities"),
+ wars: document.getElementById("wars"),
routes: document.getElementById("routes"),
farmingKnowledge: document.getElementById("farmingKnowledge"),
metallurgyKnowledge: document.getElementById("metallurgyKnowledge"),
@@ -429,12 +435,14 @@ class Simulation {
this.ethnicities = new Map();
this.cities = [];
this.polities = [];
+ this.wars = [];
this.polityHistory = new Map();
this.deadPolityHistories = [];
this.tradeLinks = [];
this.nextEthnicity = 1;
this.nextCity = 1;
this.nextPolity = 1;
+ this.nextWar = 1;
this.year = 0;
this.deaths = 0;
this.maxAgents = Math.max(
@@ -1306,7 +1314,7 @@ class Simulation {
if (group.count < 3) continue;
const avgSedentary = group.sedentary / group.count;
let city = this.cities.find(c => Math.abs(c.x - (i % w.size)) + Math.abs(c.y - Math.floor(i / w.size)) < 7);
- if (!city && canFoundCities && foundedThisTick < foundingLimit && this.cities.length < 80) {
+ if (!city && canFoundCities && foundedThisTick < foundingLimit && this.cities.length < SimConfig.city.maxCities) {
const foundingChance = clamp((avgSedentary - 0.18) * 2.4 * 3, 0.04, 0.98);
if (avgSedentary < 0.22 || this.rng.next() > foundingChance) continue;
city = this.createCity(i % w.size, Math.floor(i / w.size), group);
@@ -1652,7 +1660,8 @@ class Simulation {
treasury: polity.treasury || 0,
avgLoyalty: this.averagePolityLoyalty(polity)
});
- while (history.samples.length > 160) history.samples.shift();
+ const maxSamples = SimConfig.render.historyWindowYears + SimConfig.render.historySamplePaddingYears;
+ while (history.samples.length > maxSamples) history.samples.shift();
}
samplePolityHistories() {
@@ -1696,6 +1705,128 @@ class Simulation {
city.receivedAid = false;
}
+ isPolityAtWar(polityId) {
+ return this.wars.some(war => war.ended === null && (war.aPolityId === polityId || war.bPolityId === polityId));
+ }
+
+ getWarBetween(aId, bId) {
+ return this.wars.find(war =>
+ war.ended === null &&
+ ((war.aPolityId === aId && war.bPolityId === bId) || (war.aPolityId === bId && war.bPolityId === aId))
+ ) || null;
+ }
+
+ startWar(a, b) {
+ if (!a || !b || a.id === b.id) return null;
+ if (this.getWarBetween(a.id, b.id)) return null;
+ if (this.isPolityAtWar(a.id) || this.isPolityAtWar(b.id)) return null;
+
+ const id = this.nextWar++;
+ const war = {
+ id,
+ aPolityId: a.id,
+ bPolityId: b.id,
+ started: this.year,
+ lastActionYear: this.year,
+ intensity: this.rng.range(0.45, 1.05),
+ exhaustionA: 0,
+ exhaustionB: 0,
+ ended: null
+ };
+ this.wars.push(war);
+ a.crisis = clamp((a.crisis || 0) + 0.04, 0, 1.5);
+ b.crisis = clamp((b.crisis || 0) + 0.04, 0, 1.5);
+ return war;
+ }
+
+ endWar(war) {
+ if (war && war.ended === null) war.ended = this.year;
+ }
+
+ totalPolityPopulation(polity) {
+ return this.getPolityCities(polity)
+ .reduce((sum, city) => sum + Math.max(0, city.population || 0), 0);
+ }
+
+ averagePolityTechnology(polity) {
+ const cities = this.getPolityCities(polity);
+ if (!cities.length) return 0;
+ let total = 0;
+ let count = 0;
+ for (const city of cities) {
+ if (!city?.knowledge) continue;
+ total += ((city.knowledge.farming || 0) + (city.knowledge.metallurgy || 0)) * 0.5;
+ count++;
+ }
+ return count ? total / count : 0;
+ }
+
+ polityPower(polity) {
+ const cities = this.getPolityCities(polity);
+ if (!cities.length) return 0;
+
+ const population = cities.reduce((sum, c) => sum + Math.max(0, c.population || 0), 0);
+ const treasury = Math.max(0, polity.treasury || 0);
+ const avgLoyalty = this.averagePolityLoyalty ? this.averagePolityLoyalty(polity) : 0.5;
+ const tech = this.averagePolityTechnology(polity);
+ const instability = this.polityInstability ? this.polityInstability(polity) : 0;
+ const agePressure = this.polityAgePressure ? this.polityAgePressure(polity) : 0;
+
+ return Math.max(0,
+ Math.sqrt(population) * 1.35 +
+ Math.sqrt(treasury) * 1.15 +
+ avgLoyalty * 16 +
+ tech * 24 -
+ instability * 12 -
+ agePressure * 6
+ );
+ }
+
+ polityInfluenceRange(polity, wartime = false) {
+ const power = this.polityPower(polity);
+ const base = 18 + Math.sqrt(power) * 2.2;
+ const range = wartime ? base * 1.15 : base * 0.75;
+ return clamp(range, wartime ? 24 : 18, wartime ? 60 : 42);
+ }
+
+ distanceToNearestPolityCity(polity, city) {
+ if (!polity || !city) return Infinity;
+ let best = Infinity;
+ for (const source of this.getPolityCities(polity)) {
+ const distance = this.effectiveDistance ? this.effectiveDistance(source, city) : this.distanceBetweenCities(source, city);
+ if (distance < best) best = distance;
+ }
+ return best;
+ }
+
+ nearestPolityCity(polity, city) {
+ if (!polity || !city) return null;
+ let best = null;
+ let bestDistance = Infinity;
+ for (const source of this.getPolityCities(polity)) {
+ const distance = this.effectiveDistance ? this.effectiveDistance(source, city) : this.distanceBetweenCities(source, city);
+ if (distance < bestDistance) {
+ best = source;
+ bestDistance = distance;
+ }
+ }
+ return best;
+ }
+
+ polityDistance(a, b) {
+ const aCities = this.getPolityCities(a);
+ const bCities = this.getPolityCities(b);
+ if (!aCities.length || !bCities.length) return Infinity;
+ let best = Infinity;
+ for (const aCity of aCities) {
+ for (const bCity of bCities) {
+ const distance = this.effectiveDistance ? this.effectiveDistance(aCity, bCity) : this.distanceBetweenCities(aCity, bCity);
+ if (distance < best) best = distance;
+ }
+ }
+ return best;
+ }
+
polityAge(polity) {
return (this.year - (polity?.founded ?? this.year)) / MONTHS_PER_YEAR;
}
@@ -1853,6 +1984,288 @@ class Simulation {
}
}
+ absorbIndependentCities() {
+ if (this.year % years(5) !== 0) return;
+
+ for (const polity of this.polities) {
+ const cities = this.getPolityCities(polity);
+ if (!cities.length) continue;
+
+ const power = this.polityPower(polity);
+ const range = this.polityInfluenceRange(polity, false);
+ let absorbed = 0;
+
+ const candidates = this.cities
+ .filter(city => city.polityId === null && city.population > 0)
+ .map(city => ({
+ city,
+ distance: this.distanceToNearestPolityCity(polity, city)
+ }))
+ .filter(x => x.distance <= range)
+ .sort((a, b) => a.distance - b.distance);
+
+ for (const { city, distance } of candidates) {
+ if (absorbed >= 1) break;
+
+ const cityInfluence = this.cityInfluence ? this.cityInfluence(city) : Math.sqrt(city.population || 1);
+ const proximity = 1 / (1 + distance * 0.07);
+ const pressure = (power / Math.max(1, cityInfluence)) * proximity;
+
+ if (pressure > 0.75 && this.rng.next() < clamp(pressure * 0.055, 0.01, 0.22)) {
+ this.addCityToPolity(city, polity, 0.42);
+ city.population = Math.max(1, Math.floor(city.population * this.rng.range(0.95, 0.99)));
+ absorbed++;
+ }
+ }
+ }
+ }
+
+ maybeStartWars() {
+ if (this.year % years(10) !== 0) return;
+
+ for (const a of this.polities) {
+ if (this.isPolityAtWar(a.id)) continue;
+
+ const aPower = this.polityPower(a);
+ if (aPower <= 0) continue;
+
+ let started = false;
+
+ for (const b of this.polities) {
+ if (started) break;
+ if (a.id >= b.id) continue;
+ if (this.isPolityAtWar(b.id)) continue;
+ if (this.getWarBetween(a.id, b.id)) continue;
+
+ const distance = this.polityDistance(a, b);
+ if (distance > 46) continue;
+
+ const bPower = this.polityPower(b);
+ if (bPower <= 0) continue;
+
+ const larger = Math.max(aPower, bPower);
+ const smaller = Math.min(aPower, bPower);
+ const advantage = larger / Math.max(1, smaller);
+
+ const proximity = clamp((46 - distance) / 46, 0, 1);
+ const aInstability = this.polityInstability ? this.polityInstability(a) : 0;
+ const bInstability = this.polityInstability ? this.polityInstability(b) : 0;
+ const aAge = this.polityAgePressure ? this.polityAgePressure(a) : 0;
+ const bAge = this.polityAgePressure ? this.polityAgePressure(b) : 0;
+ const aOverextension = this.polityOverextension ? this.polityOverextension(a) : 0;
+ const bOverextension = this.polityOverextension ? this.polityOverextension(b) : 0;
+
+ const asymmetryPressure = clamp((advantage - 1.15) / 2.5, 0, 1);
+ const weakSideInstability = aPower > bPower ? bInstability : aInstability;
+ const weakSideAge = aPower > bPower ? bAge : aAge;
+ const weakSideOverextension = aPower > bPower ? bOverextension : aOverextension;
+ const generalInstability = Math.max(aInstability, bInstability) * 0.04;
+ const borderFriction = this.borderFriction(a, b, distance);
+
+ const chance =
+ 0.002 +
+ proximity * 0.010 +
+ asymmetryPressure * 0.010 +
+ weakSideInstability * 0.020 +
+ weakSideAge * 0.012 +
+ weakSideOverextension * 0.012 +
+ borderFriction * 0.010 +
+ generalInstability;
+
+ if (this.rng.next() < clamp(chance, 0, 0.08)) {
+ this.startWar(a, b);
+ started = true;
+ }
+ }
+ }
+ }
+
+ borderFriction(a, b, distance) {
+ if (!Number.isFinite(distance)) return 0;
+ const proximity = clamp((34 - distance) / 34, 0, 1);
+ const aCities = this.getPolityCities(a).length;
+ const bCities = this.getPolityCities(b).length;
+ const sizePressure = clamp((aCities + bCities - 3) / 8, 0, 1);
+ return proximity * (0.35 + sizePressure * 0.65);
+ }
+
+ updateWars() {
+ if (this.year % years(2) !== 0) return;
+
+ for (const war of this.wars) {
+ if (war.ended !== null) continue;
+
+ const a = this.getPolityById(war.aPolityId);
+ const b = this.getPolityById(war.bPolityId);
+
+ if (!a || !b) {
+ war.ended = this.year;
+ continue;
+ }
+
+ this.applyWarPressure(war, a, b);
+ this.applyWarPressure(war, b, a);
+ this.applyWarExhaustion(war, a, b);
+ this.maybeEndWar(war);
+ }
+
+ this.wars = this.wars.filter(w => w.ended === null);
+ }
+
+ applyWarPressure(war, attacker, defender) {
+ const attackerCities = this.getPolityCities(attacker);
+ const defenderCities = this.getPolityCities(defender);
+ if (!attackerCities.length || !defenderCities.length) return;
+
+ const attackerPower = this.polityPower(attacker);
+ const defenderPower = this.polityPower(defender);
+ if (attackerPower <= defenderPower * 1.05) return;
+
+ const range = this.polityInfluenceRange(attacker, true);
+
+ const candidates = defenderCities
+ .filter(city => city.id !== defender.centerCityId || defenderCities.length <= 2)
+ .map(city => ({
+ city,
+ distance: this.distanceToNearestPolityCity(attacker, city)
+ }))
+ .filter(x => x.distance <= range)
+ .sort((a, b) => a.distance - b.distance);
+
+ if (!candidates.length) return;
+
+ const topCandidates = candidates.slice(0, 3);
+ const selected = topCandidates[this.rng.int(topCandidates.length)];
+ const pressure = this.warAbsorptionPressure(
+ attacker,
+ defender,
+ selected.city,
+ selected.distance,
+ attackerPower,
+ defenderPower
+ );
+
+ const chance = clamp(pressure * 0.065 * (war.intensity || 0.75), 0.01, 0.45);
+
+ if (pressure > 0.95 && this.rng.next() < chance) {
+ this.captureCityInWar(selected.city, attacker, defender, war, pressure);
+ } else {
+ selected.city.loyalty = clamp((selected.city.loyalty ?? 0.5) - pressure * 0.012, 0, 1);
+ }
+ }
+
+ warAbsorptionPressure(attacker, defender, city, distance, attackerPower, defenderPower) {
+ const powerRatio = attackerPower / Math.max(1, defenderPower);
+ const proximity = 1 / (1 + distance * 0.055);
+ const defenderInstability = this.polityInstability ? this.polityInstability(defender) : 0;
+ const defenderAge = this.polityAgePressure ? this.polityAgePressure(defender) : 0;
+ const defenderOverextension = this.polityOverextension ? this.polityOverextension(defender) : 0;
+ const loyaltyWeakness = 1 - clamp(city.loyalty ?? 0.5, 0, 1);
+
+ const techAdvantage = 1 + clamp(
+ this.averagePolityTechnology(attacker) - this.averagePolityTechnology(defender),
+ -0.25,
+ 0.45
+ );
+
+ const nearest = this.nearestPolityCity(attacker, city);
+ const ethnicityFactor = nearest && this.sameDominantEthnicity(city, nearest) ? 1.10 : 0.92;
+ const tradeFactor = nearest && this.hasDirectTradeConnection && this.hasDirectTradeConnection(nearest, city) ? 1.15 : 1.0;
+
+ return (
+ powerRatio *
+ proximity *
+ techAdvantage *
+ ethnicityFactor *
+ tradeFactor *
+ (1 + defenderInstability * 0.32) *
+ (1 + defenderAge * 0.18) *
+ (1 + defenderOverextension * 0.14) *
+ (0.65 + loyaltyWeakness * 0.72)
+ );
+ }
+
+ captureCityInWar(city, attacker, defender, war, pressure) {
+ const lossRate = clamp(
+ 0.04 + pressure * 0.025 + (war.intensity || 0.75) * 0.025,
+ 0.05,
+ 0.22
+ );
+
+ const loss = Math.floor((city.population || 0) * lossRate);
+ if (loss > 0) {
+ city.population = Math.max(1, city.population - loss);
+ if (typeof removeFromComposition === "function" && city.ethnicityComposition) {
+ removeFromComposition(city.ethnicityComposition, loss);
+ }
+ this.deaths += Math.floor(loss * 0.35);
+ }
+
+ this.removeCityFromPolity(city);
+ this.addCityToPolity(city, attacker, 0.28);
+
+ city.loyalty = clamp(city.loyalty ?? 0.28, 0.20, 0.36);
+
+ war.lastActionYear = this.year;
+
+ if (war.aPolityId === attacker.id) {
+ war.exhaustionA = clamp((war.exhaustionA || 0) + 0.04, 0, 1);
+ war.exhaustionB = clamp((war.exhaustionB || 0) + 0.08, 0, 1);
+ } else {
+ war.exhaustionB = clamp((war.exhaustionB || 0) + 0.04, 0, 1);
+ war.exhaustionA = clamp((war.exhaustionA || 0) + 0.08, 0, 1);
+ }
+
+ attacker.treasury = Math.max(0, (attacker.treasury || 0) - loss * 0.015);
+ attacker.crisis = clamp((attacker.crisis || 0) + 0.035, 0, 1.5);
+
+ defender.crisis = clamp((defender.crisis || 0) + 0.12, 0, 1.5);
+ defender.legitimacy = clamp((defender.legitimacy ?? 0.7) - 0.05, 0, 1);
+ }
+
+ applyWarExhaustion(war, a, b) {
+ const intensity = war.intensity || 0.75;
+
+ const costA = 0.25 * intensity + this.getPolityCities(a).length * 0.035;
+ const costB = 0.25 * intensity + this.getPolityCities(b).length * 0.035;
+
+ a.treasury = Math.max(0, (a.treasury || 0) - costA);
+ b.treasury = Math.max(0, (b.treasury || 0) - costB);
+
+ a.crisis = clamp((a.crisis || 0) + 0.004 * intensity, 0, 1.5);
+ b.crisis = clamp((b.crisis || 0) + 0.004 * intensity, 0, 1.5);
+
+ war.exhaustionA = clamp((war.exhaustionA || 0) + 0.006 * intensity, 0, 1);
+ war.exhaustionB = clamp((war.exhaustionB || 0) + 0.006 * intensity, 0, 1);
+ }
+
+ maybeEndWar(war) {
+ const a = this.getPolityById(war.aPolityId);
+ const b = this.getPolityById(war.bPolityId);
+ if (!a || !b) {
+ war.ended = this.year;
+ return;
+ }
+
+ const age = this.year - war.started;
+ const noActionFor = this.year - war.lastActionYear;
+ const exhaustion = Math.max(war.exhaustionA || 0, war.exhaustionB || 0);
+
+ if (age > years(90)) {
+ war.ended = this.year;
+ return;
+ }
+
+ if (age > years(10) && noActionFor > years(18)) {
+ war.ended = this.year;
+ return;
+ }
+
+ if (exhaustion > 0.85 && this.rng.next() < 0.35) {
+ war.ended = this.year;
+ }
+ }
+
collectAndRedistributeResources() {
for (const polity of this.polities) {
const cities = this.getPolityCities(polity);
@@ -2090,13 +2503,21 @@ class Simulation {
this.cleanupPolities();
this.foundPolities();
this.expandPolities();
+
if (this.reinforcePolityTradeRoutes) this.reinforcePolityTradeRoutes();
+
this.collectAndRedistributeResources();
- this.erodePolityLegitimacy();
- this.triggerPolityCrises();
- this.applyOldStateStress();
+
+ if (this.erodePolityLegitimacy) this.erodePolityLegitimacy();
+ if (this.triggerPolityCrises) this.triggerPolityCrises();
+ if (this.applyOldStateStress) this.applyOldStateStress();
+
this.updateCityLoyalty();
+ this.absorbIndependentCities();
+ this.maybeStartWars();
+ this.updateWars();
this.splitUnloyalCities();
+
this.cleanupPolities();
if (this.samplePolityHistories) this.samplePolityHistories();
}
@@ -2431,6 +2852,7 @@ class Simulation {
nextEthnicity: this.nextEthnicity,
nextCity: this.nextCity,
nextPolity: this.nextPolity,
+ nextWar: this.nextWar,
maxAgents: this.maxAgents,
world: {
size: this.world.size,
@@ -2477,6 +2899,7 @@ class Simulation {
crisis: p.crisis,
lastCrisisYear: p.lastCrisisYear
})),
+ wars: this.wars,
polityHistory: [...this.polityHistory.values()],
deadPolityHistories: this.deadPolityHistories
};
@@ -2491,6 +2914,7 @@ class Simulation {
sim.nextEthnicity = state.nextEthnicity || 1;
sim.nextCity = state.nextCity || 1;
sim.nextPolity = state.nextPolity || 1;
+ sim.nextWar = state.nextWar || 1;
sim.maxAgents = state.maxAgents || 30000;
sim.world.terrain.set(unpackArray(state.world.terrain, Uint8Array));
@@ -2559,6 +2983,19 @@ class Simulation {
crisis: p.crisis ?? 0,
lastCrisisYear: p.lastCrisisYear ?? sim.year
}));
+ sim.wars = (state.wars || []).map(w => ({
+ id: w.id,
+ aPolityId: w.aPolityId,
+ bPolityId: w.bPolityId,
+ started: w.started || sim.year,
+ lastActionYear: w.lastActionYear || w.started || sim.year,
+ intensity: clamp(w.intensity ?? 0.75, 0.35, 1.25),
+ exhaustionA: clamp(w.exhaustionA ?? 0, 0, 1),
+ exhaustionB: clamp(w.exhaustionB ?? 0, 0, 1),
+ ended: w.ended ?? null
+ })).filter(w => w.ended === null);
+ const maxWarId = sim.wars.reduce((max, war) => Math.max(max, war.id || 0), 0);
+ sim.nextWar = Math.max(sim.nextWar, maxWarId + 1);
sim.polityHistory = new Map((state.polityHistory || []).map(h => [h.id, {
id: h.id,
color: h.color || hslToRgb((h.id * 0.38196601125) % 1, 0.58, 0.62),
@@ -2615,8 +3052,6 @@ function render() {
} else if (mode === "pressure") {
const v = clamp(w.pressure[i] / 12, 0, 1);
color = mix(terrainInfo[w.terrain[i]].color, [210, 73, 67], v);
- } else if (mode === "cities") {
- color = w.city[i] >= 0 ? mix([74, 105, 58], [226, 198, 121], clamp(w.farmland[i], 0, 1)) : terrainInfo[w.terrain[i]].color;
} else if (mode === "polities") {
color = terrainInfo[w.terrain[i]].color;
if (w.city[i] >= 0) {
@@ -2638,7 +3073,6 @@ function render() {
ctx.putImageData(image, 0, 0);
drawTradeLinks();
- drawFarmlandRings(mode);
drawAgentsAndCities(mode);
maybeRenderStateGraph();
els.frameCost.textContent = `${Math.round(performance.now() - start)}ms`;
@@ -2666,28 +3100,6 @@ function drawTradeLinks() {
ctx.restore();
}
-function drawFarmlandRings(mode) {
- if (mode !== "cities") return;
- const w = sim.world;
- ctx.save();
- ctx.globalAlpha = 0.78;
- ctx.fillStyle = "#d9c45f";
- for (const city of sim.cities) {
- const radius = Math.max(1, city.agriculturalRadius);
- for (let dy = -radius; dy <= radius; dy++) {
- for (let dx = -radius; dx <= radius; dx++) {
- const d = Math.abs(dx) + Math.abs(dy);
- if (d !== radius) continue;
- const x = city.x + dx;
- const y = city.y + dy;
- if (x < 0 || y < 0 || x >= w.size || y >= w.size) continue;
- if (w.terrain[w.idx(x, y)] !== Terrain.WATER) ctx.fillRect(x, y, 1, 1);
- }
- }
- }
- ctx.restore();
-}
-
function drawAgentsAndCities(mode) {
ctx.save();
for (const city of sim.cities) {
@@ -2886,6 +3298,7 @@ function updateStats(force = false) {
els.ethnicities.textContent = livingEthnicities.length.toLocaleString();
els.cities.textContent = sim.cities.length.toLocaleString();
els.polities.textContent = sim.polities.length.toLocaleString();
+ if (els.wars) els.wars.textContent = sim.wars.length.toLocaleString();
els.routes.textContent = sim.tradeLinks.length.toLocaleString();
els.farmingKnowledge.textContent = `${(farmingTotal / agentCount).toFixed(4)} (${farmingHolders})`;
els.metallurgyKnowledge.textContent = `${(metallurgyTotal / agentCount).toFixed(4)} (${metallurgyHolders})`;
@@ -2907,8 +3320,6 @@ function setLegend() {
els.legend.innerHTML = "Color = dominant regional lineage. Mixed tiles brighten toward gray.";
} else if (mode === "pressure") {
els.legend.innerHTML = "High local population pressure";
- } else if (mode === "cities") {
- els.legend.innerHTML = "City nodes, farmland radius, and stored urban population";
} else if (mode === "polities") {
els.legend.innerHTML = "Color = city-centered state. Uncolored cities are independent.";
} else if (mode === "technology") {
@@ -2946,7 +3357,9 @@ function renderStateGraph() {
g.fillStyle = "#101315";
g.fillRect(0, 0, w, h);
- const historyWindowStart = Math.max(0, sim.year - years(10000));
+ const historyWindowSpan = years(SimConfig.render.historyWindowYears);
+ const historyWindowEnd = sim.year;
+ const historyWindowStart = Math.max(0, historyWindowEnd - historyWindowSpan);
const histories = sim.getAllPolityHistories()
.filter(history => (history.ended ?? sim.year) >= historyWindowStart)
.map(history => ({
@@ -2963,13 +3376,11 @@ function renderStateGraph() {
return;
}
- let minYear = Infinity;
- let maxYear = sim.year;
+ let minYear = historyWindowStart;
+ let maxYear = historyWindowEnd;
for (const history of histories) {
minYear = Math.min(minYear, history.visibleFounded);
- maxYear = Math.max(maxYear, history.ended ?? 0, history.visibleSamples[history.visibleSamples.length - 1]?.year ?? 0);
}
- if (!Number.isFinite(minYear)) minYear = 0;
if (els.historyRange) els.historyRange.textContent = `${formatGraphYear(minYear)}-${formatGraphYear(maxYear)}`;
const paddingLeft = 34;