This commit is contained in:
33333-33333 2026-09-17 22:32:25 +09:00
commit 8a9fd9b304
252 changed files with 356 additions and 1487 deletions

296
engine.js
View file

@ -360,6 +360,7 @@ this.ethnicities = new Map();
this.cities = [];
this.cityById = new Map();
this.cityBuckets = new Map();
this.polityCitiesCache = new Map();
this.polities = [];
this.polityById = new Map();
this.wars = [];
@ -420,6 +421,8 @@ this.routeCandidateScratch = [];
this.supportedRouteScratch = new Set();
this.populationMoveScratch = [];
this.activePopulationScratch = [];
this.territoryExitCandidates = [];
this.suppressTerritoryExitShock = false;
this.technologySourceScratch = [];
this.routeCostScratch = new Float32Array(this.world.count);
this.routePrevScratch = new Int32Array(this.world.count);
@ -1227,33 +1230,26 @@ step() {
for (const city of this.cities) city.activeVisitors = 0;
this.dominantEthnicityCache.clear();
this.rebuildIndexes();
// World fields are visually and behaviorally slow-moving. Updating them once
// per simulated month halves full-map work while preserving the same annual
// decay/regeneration scale. Annual city processing owns its capacity refresh.
const annualTick = this.year % years(1) === 0;
const weekOfYear = this.year % WEEKS_PER_YEAR;
const fieldTick = this.year % WEEKS_PER_MONTH === 0;
const resourceTick = this.year % 12 === 2;
const pheromoneChanged = fieldTick ? this.updateWorldFields(WEEKS_PER_MONTH, 0) : false;
if (resourceTick) this.regenerateResources(12);
if (!annualTick && pheromoneChanged) this.updatePopulationCapacity();
const resourceTick = weekOfYear === WEEKS_PER_YEAR - 1;
// Pheromone changes update capacity only on touched cells inside updateWorldFields().
// This avoids a full-map capacity pass every simulated month.
if (fieldTick) this.updateWorldFields(WEEKS_PER_MONTH);
// Resources are consumed by the annual economy. Batch the same 48-week
// regeneration immediately before the next annual tick.
if (resourceTick) this.regenerateResources(WEEKS_PER_YEAR);
this.maybeSpawnDisaster();
if (this.year % years(1) === 0) this.maybeSpawnFrontierWave();
if (this.year % years(1) === 0) {
if (annualTick) this.maybeSpawnFrontierWave();
if (annualTick) {
this.updateCities();
this.updateNomadBands();
this.consumeTileResources();
this.growTilePopulation();
this.updatePopulationPressure();
this.diffusePopulation();
this.updatePopulationPressure();
if (this.year % years(2) === 0) {
this.updateTileTechnology();
this.diffuseTileTechnology();
this.cityTechnologyExchange();
}
if (this.year % years(3) === 0) this.updateTileAssimilation();
this.spreadCulturesFromCitiesAndTrade();
this.updateNomadInfluence();
this.clearSparseHumanData();
if (this.year % years(100) === 0 && typeof console !== "undefined" && console.debug) {
const d = this.territoryDebug;
if (d.disconnectedTerritoryRemoved || d.independentCitiesAbsorbedByTerritory || d.cityTerritoryMismatchesFixed || d.invalidCampaignTargetsRejected) {
@ -1261,10 +1257,27 @@ console.debug("territory topology", { year: Math.floor(this.year / WEEKS_PER_YEA
}
}
}
if (this.year % years(5) === 0) {
this.updateTradeRoutesFieldBased();
// Spread expensive annual/periodic field work across the year instead of
// stacking it all on the year boundary. Each population cell still migrates
// once per year, in one of four stable quarterly cohorts.
if (weekOfYear % 12 === 8) {
const cohort = Math.floor(weekOfYear / 12) % 4;
this.updatePopulationCapacity();
this.diffusePopulation(cohort, 4);
this.updatePopulationPressure();
}
this.updatePolities();
if (this.year % years(2) === 12) {
this.updateTileTechnology();
this.diffuseTileTechnology();
this.cityTechnologyExchange();
this.clearSparseHumanData();
}
if (this.year % years(3) === 24) this.updateTileAssimilation();
if (weekOfYear === 36) this.spreadCulturesFromCitiesAndTrade();
if (this.year % years(5) === 20) this.updateTradeRoutes(true);
// State accounting follows the annual city economy, but is offset by one
// month so the two largest annual workloads do not land on the same frame.
if (weekOfYear === 4) this.updatePolities(true);
if (this.year % SimConfig.war.tickWeeks === 0) {
this.maybeStartWars();
this.updateCampaigns();
@ -1272,7 +1285,9 @@ this.updateWars();
}
const historyTick = this.year % 12 === 0;
if (historyTick) this.updateEthnicStats();
if (this.year > 0 && annualTick) {
// Ethnic maintenance is annual in frequency but offset from the year boundary.
if (this.year > 0 && weekOfYear === 40) {
this.updateEthnicStats();
this.absorbSmallEthnicities();
this.updateEthnicStats();
this.splitDivergentEthnicities();
@ -1283,6 +1298,7 @@ this.year++;
}
rebuildIndexes() {
this.cityById.clear();
this.polityCitiesCache?.clear();
for (const city of this.cities) this.cityById.set(city.id, city);
this.polityById.clear();
for (const polity of this.polities) this.polityById.set(polity.id, polity);
@ -1483,10 +1499,20 @@ culture.set(fallbackEthnicity, (culture.get(fallbackEthnicity) || 0) + localPopu
}
}
updateCultureTile(tile) {
const w = this.world;
const sparseThreshold = Math.max(0, SimConfig.population.humanDataMinPopulation ?? 1);
if ((w.population[tile] || 0) < sparseThreshold) {
w.dominantEthnicity[tile] = -1;
w.cultureDiversity[tile] = 0;
w.farmingKnowledge[tile] = 0;
w.metallurgyKnowledge[tile] = 0;
this.tileEthnicMix.delete(tile);
return;
}
const counts = this.tileEthnicMix.get(tile);
if (!counts || !counts.size) {
if ((this.world.population[tile] || 0) <= 0.01) this.world.dominantEthnicity[tile] = -1;
this.world.cultureDiversity[tile] *= 0.98;
w.dominantEthnicity[tile] = -1;
w.cultureDiversity[tile] = 0;
return;
}
let total = 0;
@ -1502,12 +1528,13 @@ dominantCount = count;
this.world.dominantEthnicity[tile] = dominant;
this.world.cultureDiversity[tile] = total > 0 ? 1 - dominantCount / total : 0;
}
diffusePopulation() {
diffusePopulation(cohortIndex = 0, cohortCount = 1) {
const w = this.world;
const moves = this.populationMoveScratch;
moves.length = 0;
const tiles = this.activePopulationSnapshot();
for (const tile of tiles) {
if (cohortCount > 1 && tile % cohortCount !== cohortIndex) continue;
const population = w.population[tile];
const mobile = w.mobilePopulation[tile] || 0;
if (population < 1) continue;
@ -1665,6 +1692,25 @@ const demand = (population / SimConfig.population.populationScale) * (0.0045 + M
w.resource[tile] = Math.max(0, w.resource[tile] - demand);
}
}
clearSparseHumanData() {
const w = this.world;
const threshold = Math.max(0, SimConfig.population.humanDataMinPopulation ?? 1);
if (threshold <= 0) return 0;
let cleared = 0;
for (const tile of this.activePopulationSnapshot()) {
if ((w.population[tile] || 0) >= threshold) continue;
const hadData = w.farmingKnowledge[tile] !== 0 || w.metallurgyKnowledge[tile] !== 0 ||
w.dominantEthnicity[tile] >= 0 || w.cultureDiversity[tile] !== 0 || this.tileEthnicMix.has(tile);
if (!hadData) continue;
w.farmingKnowledge[tile] = 0;
w.metallurgyKnowledge[tile] = 0;
w.dominantEthnicity[tile] = -1;
w.cultureDiversity[tile] = 0;
this.tileEthnicMix.delete(tile);
cleared++;
}
return cleared;
}
updateTileAssimilation() {
const w = this.world;
for (const [tile, mix] of [...this.tileEthnicMix]) {
@ -1820,33 +1866,37 @@ const capacity = w.populationCapacity[i];
w.populationPressure[i] = capacity > 0 ? w.population[i] / capacity : 0;
}
}
updatePopulationCapacity() {
updatePopulationCapacityTile(i) {
const w = this.world;
const { carryingCapacityFarmland } = SimConfig.population;
const maxPheromone = Math.max(1, SimConfig.route.maxPheromone);
const staticCapacity = this.populationCapacityStatic;
const dynamicScale = this.populationCapacityDynamicScale;
for (let i = 0; i < w.count; i++) {
const scale = dynamicScale[i];
const scale = this.populationCapacityDynamicScale[i];
const staticCapacity = this.populationCapacityStatic[i];
if (scale === 0) {
w.populationCapacity[i] = staticCapacity[i];
w.populationPressure[i] = w.population[i] / staticCapacity[i];
continue;
w.populationCapacity[i] = staticCapacity;
w.populationPressure[i] = staticCapacity > 0 ? w.population[i] / staticCapacity : 0;
return;
}
const maxPheromone = Math.max(1, SimConfig.route.maxPheromone);
const routeBonus = w.tradeRoute[i] ? 1.6 : Math.min(1, Math.max(0, w.pheromone[i] / maxPheromone)) * 0.8;
const cityBonus = w.city[i] >= 0 ? 3.5 : w.cityPull[i] * 2.2;
const capacity = staticCapacity[i] + (
w.farmland[i] * carryingCapacityFarmland + routeBonus + cityBonus
const capacity = staticCapacity + (
w.farmland[i] * SimConfig.population.carryingCapacityFarmland + routeBonus + cityBonus
) * scale;
w.populationCapacity[i] = capacity;
w.populationPressure[i] = w.population[i] / capacity;
w.populationPressure[i] = capacity > 0 ? w.population[i] / capacity : 0;
}
updatePopulationCapacity() {
for (let i = 0; i < this.world.count; i++) this.updatePopulationCapacityTile(i);
}
updateTileTechnology() {
const w = this.world;
const tiles = this.activePopulationSnapshot();
const humanThreshold = Math.max(0, SimConfig.population.humanDataMinPopulation ?? 1);
for (const tile of tiles) {
if (w.population[tile] <= 0 || w.terrain[tile] === Terrain.WATER) continue;
if (w.population[tile] < humanThreshold || w.terrain[tile] === Terrain.WATER) {
w.farmingKnowledge[tile] = 0;
w.metallurgyKnowledge[tile] = 0;
continue;
}
const settled = w.settledPopulation[tile] || 0;
const density = clamp(Math.log1p(settled) / Math.log(80), 0, 1.4);
const cityPull = w.city[tile] >= 0 ? 1 : clamp(w.cityPull[tile], 0, 1);
@ -1862,6 +1912,7 @@ diffuseTileTechnology() {
const w = this.world;
const nextFarming = this.scratchFloatA;
const nextMetallurgy = this.scratchFloatB;
const humanThreshold = Math.max(0, SimConfig.population.humanDataMinPopulation ?? 1);
nextFarming.set(w.farmingKnowledge);
nextMetallurgy.set(w.metallurgyKnowledge);
const sources = this.technologySourceScratch;
@ -1877,9 +1928,17 @@ this.visitStamp[tile] = marker;
sources.push(tile);
}
for (const tile of sources) {
if (w.population[tile] <= 0 && !w.tradeRoute[tile]) continue;
if ((w.population[tile] || 0) < humanThreshold) {
nextFarming[tile] = 0;
nextMetallurgy[tile] = 0;
continue;
}
this.forCardinalNeighbors(tile, n => {
if (w.terrain[n] === Terrain.WATER) return;
if (w.terrain[n] === Terrain.WATER || (w.population[n] || 0) < humanThreshold) {
nextFarming[n] = 0;
nextMetallurgy[n] = 0;
return;
}
const routeBoost = w.tradeRoute[tile] || w.tradeRoute[n] ? 1.9 : 1;
const popBoost = clamp(Math.log1p(w.population[n]) / Math.log(60), 0.2, 1.2);
const rate = clamp((0.010 + (w.cityPull[tile] + w.cityPull[n]) * 0.004) * routeBoost * popBoost, 0.002, 0.035);
@ -1935,8 +1994,7 @@ const tempDiff = Math.abs(this.world.temperature[tile] - ethnicity.climateTemp);
const humidDiff = Math.abs(this.world.humidity[tile] - ethnicity.climateHumidity);
return tempDiff * 0.58 + humidDiff * 0.42;
}
updateWorldFields(scale = 1, resourceScale = scale) {
if (resourceScale > 0) this.regenerateResources(resourceScale);
updateWorldFields(scale = 1) {
// Preserve the former biweekly pheromone diffusion semantics while keeping
// pheromone work sparse. Resource regeneration is batched separately by step().
let remaining = Math.max(0, scale);
@ -2009,7 +2067,9 @@ w.city.fill(-1);
w.farmland.fill(0);
w.cityPull.fill(0);
const candidates = new Map();
this.updatePopulationCapacity();
// Keep the previous valid capacity while rebuilding farmland/city influence.
// Recomputing here would scan the whole map only to discard both bonuses; the
// refreshed capacity is calculated once after the rebuilt fields are ready.
const activeTiles = this.activePopulationSnapshot();
for (const tile of activeTiles) {
if (w.terrain[tile] === Terrain.WATER) continue;
@ -2453,6 +2513,22 @@ bestDistance = d;
}
return best;
}
applyTerritoryExitPopulationShock(tile, rate = SimConfig.population?.territoryExitPopulationLossRate ?? 0.12) {
const w = this.world;
if (!w?.population || !w?.terrain || tile == null || tile < 0 || tile >= w.count || w.terrain[tile] === Terrain.WATER) return 0;
const population = w.population[tile] || 0;
if (population <= 0 || rate <= 0) return 0;
return this.removeEthnicPopulationProportionally(tile, population * clamp(rate, 0, 0.95));
}
finishTrackedTerritoryExits() {
const w = this.world;
let removed = 0;
for (const tile of this.territoryExitCandidates) {
if (w.territoryOwner[tile] < 0) removed += this.applyTerritoryExitPopulationShock(tile);
}
this.territoryExitCandidates.length = 0;
return removed;
}
cityInfluence(city) {
if (!city || city.population <= 0 || city.strength <= 0) return 0;
const sedentaryFactor = clamp(0.45 + (city.sedentaryCulture ?? 0.5) * 0.9, 0.45, 1.35);
@ -2461,10 +2537,14 @@ return (Math.sqrt(city.population) * 1.4 + Math.sqrt(Math.max(0, city.storedReso
setTileOwner(tile, polityId, options = {}) {
const w = this.world;
if (tile == null || tile < 0 || tile >= w.count || (w.terrain[tile] === Terrain.WATER && (polityId ?? -1) >= 0)) return false;
const previousOwner = w.territoryOwner[tile];
const owner = polityId ?? -1;
if (owner >= 0 && !this.getPolityById(owner)) return false;
w.territoryOwner[tile] = owner;
w.polity[tile] = owner;
if (previousOwner >= 0 && owner < 0 && !this.suppressTerritoryExitShock && options.populationShock !== false) {
this.applyTerritoryExitPopulationShock(tile);
}
if (owner < 0) {
w.control[tile] = 0;
w.claim[tile] = 0;
@ -2497,9 +2577,21 @@ w.contested.fill(0);
recomputeTerritories(options = {}) {
const w = this.world;
const allowOwnershipChanges = !!options.allowOwnershipChanges;
const trackTerritoryExits = allowOwnershipChanges && !this.suppressTerritoryExitShock;
if (trackTerritoryExits) {
this.territoryExitCandidates.length = 0;
for (const tile of this.activePopulationTiles) {
if (w.territoryOwner[tile] >= 0) this.territoryExitCandidates.push(tile);
}
this.suppressTerritoryExitShock = true;
}
this.clearTerritories();
if (!this.polities.length || !this.cities.length) {
this.enforceTerritoryConnectivity();
if (trackTerritoryExits) {
this.suppressTerritoryExitShock = false;
this.finishTrackedTerritoryExits();
}
return;
}
const best = this.scratchFloatA;
@ -2649,6 +2741,10 @@ this.enforceCityTerritoryAnchors(options);
if (allowOwnershipChanges) this.smoothTerritoryBorders(1);
this.pruneUnpopulatedTerritory();
this.enforceTerritoryConnectivity(options);
if (trackTerritoryExits) {
this.suppressTerritoryExitShock = false;
this.finishTrackedTerritoryExits();
}
}
pruneUnpopulatedTerritory() {
const w = this.world;
@ -2946,15 +3042,17 @@ this.polityById.set(id, polity);
return polity;
}
getPolityCities(polity) {
if (!polity) return [];
this.polityCitiesCache ??= new Map();
const cached = this.polityCitiesCache.get(polity.id);
if (cached) return cached;
const living = [];
for (const id of [...polity.cityIds]) {
const city = this.getCityById(id);
if (!city || city.population <= 0) {
polity.cityIds.delete(id);
} else {
living.push(city);
}
if (!city || city.population <= 0) polity.cityIds.delete(id);
else living.push(city);
}
this.polityCitiesCache.set(polity.id, living);
return living;
}
dominantCityEthnicity(city) {
@ -3256,6 +3354,7 @@ if (polity) polityPopulationBefore.set(id, this.totalPolityPopulation(polity));
}
let affectedCities = 0;
let totalPopulationLoss = 0;
let regionalPopulationLoss = 0;
for (const city of this.cities) {
const d = Math.hypot(city.x - x, city.y - y);
if (d > radius) continue;
@ -3304,6 +3403,17 @@ if (d > radius) continue;
const falloff = 1 - d / radius;
const damage = clamp(scaledIntensity * falloff * falloff, 0, 1);
const tile = w.idx(tx, ty);
if (w.terrain[tile] !== Terrain.WATER && (w.population[tile] || 0) > 0) {
const regionalLossRate = clamp(
damage * (config?.regionalPopulationLossRate ?? 0.42),
0,
config?.regionalPopulationLossMax ?? 0.65
);
const loss = this.removeEthnicPopulationProportionally(tile, w.population[tile] * regionalLossRate);
regionalPopulationLoss += loss;
totalPopulationLoss += loss;
this.deaths += Math.floor(loss * 0.40);
}
w.resource[tile] *= (1 - damage * 0.45);
w.farmland[tile] *= (1 - damage * 0.35);
w.pheromone[tile] *= (1 - damage * 0.25);
@ -3331,6 +3441,7 @@ intensity,
expires: this.year + years(config?.visualDurationYears ?? 18),
affectedCities,
totalPopulationLoss,
regionalPopulationLoss,
affectedPolities
};
this.disasters.push(record);
@ -3478,11 +3589,15 @@ city.polityId = polity.id;
city.loyalty = clamp(initialLoyalty, 0, 1);
city.receivedAid = false;
polity.cityIds.add(city.id);
this.polityCitiesCache?.delete(polity.id);
}
removeCityFromPolity(city) {
if (!city || city.polityId === null) return;
const oldPolity = this.getPolityById(city.polityId);
if (oldPolity) oldPolity.cityIds.delete(city.id);
if (oldPolity) {
oldPolity.cityIds.delete(city.id);
this.polityCitiesCache?.delete(oldPolity.id);
}
city.polityId = null;
city.loyalty = 0.45;
city.receivedAid = false;
@ -3708,22 +3823,25 @@ candidates.push({ tile, score });
}
}
if (!candidates.length) return -1;
candidates.sort((a, b) => b.score - a.score);
const bestScore = candidates[0].score;
let bestScore = -Infinity;
for (const candidate of candidates) if (candidate.score > bestScore) bestScore = candidate.score;
const scoreWindow = Math.max(0.2, SimConfig.polity?.treasuryCitySiteScoreWindow ?? 6.0);
const eligible = candidates.filter(candidate => candidate.score >= bestScore - scoreWindow);
const floor = bestScore - scoreWindow;
let totalWeight = 0;
for (const candidate of eligible) {
for (const candidate of candidates) {
if (candidate.score < floor) continue;
candidate.weight = Math.pow(Math.max(0.20, candidate.score - floor + 0.20), 1.10);
totalWeight += candidate.weight;
}
let roll = this.rng.next() * totalWeight;
for (const candidate of eligible) {
let fallback = candidates[0].tile;
for (const candidate of candidates) {
if (candidate.score < floor) continue;
fallback = candidate.tile;
roll -= candidate.weight;
if (roll <= 0) return candidate.tile;
}
return eligible[eligible.length - 1]?.tile ?? candidates[0].tile;
return fallback;
}
maybeBuildTreasuryCities() {
const cfg = SimConfig.polity || {};
@ -4516,7 +4634,13 @@ control: options.control ?? 0.58,
claim: options.claim ?? 0.70,
reason: options.reason ?? "cityTransfer"
});
if (this.isConquestTransferReason(options.reason)) this.protectCityFromRebellion(city, options.rebellionProtectionYears);
if (this.isConquestTransferReason(options.reason)) {
this.protectCityFromRebellion(city, options.rebellionProtectionYears);
// City occupation also disrupts the surrounding rural population. Keep this
// separate from ordinary battle-zone damage so every conquest-style transfer
// receives the same occupation shock exactly once.
if (fromPolityId !== toPolity.id) this.applyOccupationPopulationShock(tile);
}
return true;
}
releaseCityTerritory(city, fromPolityId, radius = 4, reason = "fragmentation") {
@ -4952,7 +5076,8 @@ const key = `${attacker.id}:${selected.city.id}`;
const memory = (war.cityPressureById?.get(key) || 0) * (SimConfig.war.pressureMemoryDecay ?? 0.88) + pressure * (war.intensity || 0.75) * 1.6;
war.cityPressureById?.set(key, memory);
const captureThreshold = war.goal === "collapse_exploitation" ? 1.55 : war.goal === "city_conquest" ? 1.75 : war.goal === "capital_pressure" ? 1.85 : 2.15;
const chance = clamp((memory - captureThreshold * 0.65 + pressure * 0.35) * 0.45, 0.05, 0.85);
const baseCaptureChance = clamp((memory - captureThreshold * 0.65 + pressure * 0.35) * 0.45, 0.05, 0.85);
const chance = clamp(baseCaptureChance * (SimConfig.war.captureChanceMultiplier ?? 1), 0, 0.85);
const damageRate = clamp(
(SimConfig.war.cityDamageMinRate ?? 0.003) + pressure * 0.030 + memory * 0.018,
SimConfig.war.cityDamageMinRate ?? 0.003,
@ -5256,6 +5381,33 @@ polity.cohesion = clamp((polity.cohesion ?? 0.6) - rate * 1.1, 0, 1);
}
return total;
}
applyOccupationPopulationShock(tile, radius = SimConfig.war.occupationPopulationLossRadius ?? 4, rate = SimConfig.war.occupationPopulationLossRate ?? 0.08) {
const w = this.world;
if (tile == null || tile < 0 || tile >= w.count || radius <= 0 || rate <= 0) return 0;
const cx = tile % w.size;
const cy = Math.floor(tile / w.size);
const damageMultiplier = SimConfig.war.damageMultiplier ?? 1;
let removed = 0;
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
const distance = Math.abs(dx) + Math.abs(dy);
if (distance === 0 || distance > radius) continue;
const x = cx + dx;
const y = cy + dy;
if (x < 0 || y < 0 || x >= w.size || y >= w.size) continue;
const t = w.idx(x, y);
if (w.terrain[t] === Terrain.WATER || w.population[t] <= 0) continue;
const falloff = 1 - distance / (radius + 1);
const amount = w.population[t] * rate * damageMultiplier * falloff;
if (amount <= 0.03) continue;
removed += this.removeEthnicPopulationProportionally(t, amount) || 0;
w.resource[t] = Math.max(0, w.resource[t] - amount * 0.015);
}
}
this.recordWarDeaths(removed, 0.72);
return removed;
}
applyWarZoneTileDamage(tile, rate) {
const w = this.world;
if (tile == null || tile < 0 || tile >= w.count || rate <= 0) return 0;
@ -5690,27 +5842,26 @@ this.releaseCityTerritory(city, oldPolityId, 5, "collapse");
}
}
}
updatePolities() {
if (this.year % years(1) !== 0) return;
updatePolities(force = false) {
if (!force && this.year % years(1) !== 0) return;
this.cleanupPolities();
this.foundPolities();
if (this.reinforcePolityTradeRoutes) this.reinforcePolityTradeRoutes();
this.reinforcePolityTradeRoutes();
this.collectAndRedistributeResources();
if (this.maybeBuildTreasuryCities) this.maybeBuildTreasuryCities();
this.maybeBuildTreasuryCities();
this.detectPolityFamines();
if (this.erodePolityLegitimacy) this.erodePolityLegitimacy();
if (this.updateInstitutionalAging) this.updateInstitutionalAging();
if (this.maybeTriggerInstitutionalReforms) this.maybeTriggerInstitutionalReforms();
if (this.triggerPolityCrises) this.triggerPolityCrises();
if (this.applyOldStateStress) this.applyOldStateStress();
if (this.updatePolityLeaders) this.updatePolityLeaders();
this.erodePolityLegitimacy();
this.updateInstitutionalAging();
this.maybeTriggerInstitutionalReforms();
this.triggerPolityCrises();
this.applyOldStateStress();
this.updatePolityLeaders();
this.updateCityLoyalty();
this.maybeStartCampaigns();
this.splitUnloyalCities();
this.enforcePolityTreasuryCaps();
this.cleanupPolities();
if (this.samplePolityHistories) this.samplePolityHistories();
this.syncTilePopulationCulture();
this.samplePolityHistories();
this.recomputeTerritories({ allowOwnershipChanges: true, rebalanceOwnership: true, reason: "normal" });
this.reconcileCityTerritoryOwnership();
this.campaignPressureOnTerritories();
@ -5741,9 +5892,6 @@ if (!this.payRouteConstructionCost(center, target, path, polity)) continue;
this.registerTradeLink(center, target, 0.22, path, null, 34, 2.4, polity.id);
}
}
updateTradeRoutesFieldBased() {
return this.updateTradeRoutes(true);
}
routeCityLinkLimit(city) {
if (!city) return 1;
const capitalBonus = city.polityId !== null && this.getPolityById(city.polityId)?.centerCityId === city.id ? 1 : 0;
@ -6415,8 +6563,6 @@ e.population = 0;
e.diversity = 0;
e.centroidX = 0;
e.centroidY = 0;
e.activeTraitPopulation = 0;
e.traitSums = emptyTraitSums();
}
for (const band of this.nomadBands || []) {
const e = this.ethnicities.get(band.ethnicityId);