tweak
This commit is contained in:
parent
97659fdd2a
commit
597b276825
2 changed files with 450 additions and 38 deletions
|
|
@ -67,6 +67,7 @@
|
|||
<div><dt>Ethnicities</dt><dd id="ethnicities">0</dd></div>
|
||||
<div><dt>Cities</dt><dd id="cities">0</dd></div>
|
||||
<div><dt>States</dt><dd id="polities">0</dd></div>
|
||||
<div><dt>Wars</dt><dd id="wars">0</dd></div>
|
||||
<div><dt>Trade routes</dt><dd id="routes">0</dd></div>
|
||||
<div><dt>Farming knowledge</dt><dd id="farmingKnowledge">0.000</dd></div>
|
||||
<div><dt>Metallurgy knowledge</dt><dd id="metallurgyKnowledge">0.000</dd></div>
|
||||
|
|
|
|||
487
script.js
487
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 = "<span>Color = dominant regional lineage. Mixed tiles brighten toward gray.</span>";
|
||||
} else if (mode === "pressure") {
|
||||
els.legend.innerHTML = "<span><i style=\"background:#d24943\"></i>High local population pressure</span>";
|
||||
} else if (mode === "cities") {
|
||||
els.legend.innerHTML = "<span><i style=\"background:#e2c679\"></i>City nodes, farmland radius, and stored urban population</span>";
|
||||
} else if (mode === "polities") {
|
||||
els.legend.innerHTML = "<span>Color = city-centered state. Uncolored cities are independent.</span>";
|
||||
} 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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue