This commit is contained in:
33333-33333 2026-05-16 13:48:08 +09:00
commit ff89ec62fd
4 changed files with 112 additions and 75 deletions

132
engine.js
View file

@ -1488,6 +1488,8 @@ agriculturalRadius: 2,
tradeLinks: new Set(),
activeVisitors: 0,
age: 0,
ageWear: 0,
peakPopulation: seedPopulation,
strength: 3,
sedentaryCulture: seedSedentary,
knowledge: { farming: 0, metallurgy: 0 },
@ -1499,6 +1501,21 @@ tradeValue: 0,
tradeReach: 0
};
}
updateCityAging(city) {
city.ageWear ??= 0;
city.peakPopulation = Math.max(city.peakPopulation || 0, city.population || 0);
const cfg = SimConfig.city;
const ageStart = cfg.agingStartYears ?? 180;
if ((city.age || 0) <= ageStart) return;
city.ageWear = clamp(city.ageWear + (cfg.agingDecayPerYear ?? 0.002), 0, 1);
}
cityAgeCapacityFactor(city) {
return clamp(1 - (city?.ageWear || 0), 0, 1);
}
cityAgePopulationLimit(city) {
const peakPopulation = Math.max(city?.peakPopulation || 0, city?.population || 0, 1);
return Math.max(0, peakPopulation * this.cityAgeCapacityFactor(city));
}
absorbUrbanPopulation() {
if (!this.cities.length) return;
const absorbed = [];
@ -1535,6 +1552,7 @@ return whole + (this.rng.next() < value - whole ? 1 : 0);
processCityEconomies() {
const w = this.world;
for (const city of this.cities) {
this.updateCityAging(city);
city.agriculturalRadius = clamp(Math.floor(1 + Math.sqrt(city.population) / 4.5), 2, 14);
city.pheromoneOutput = clamp(Math.log2(city.population + 1) * 0.09, 0.15, 1.8);
city.knowledge ??= { farming: 0, metallurgy: 0 };
@ -1579,13 +1597,22 @@ city.tradeReach = trade.reach;
city.tradeValue = 0;
city.tradeReach = 0;
}
if (city.storedResources > city.population * 0.16 && city.population > 0) {
const agePopulationLimit = this.cityAgePopulationLimit(city);
if (city.storedResources > city.population * 0.16 && city.population > 0 && city.population < agePopulationLimit) {
const prosperity = clamp(city.storedResources / Math.max(1, city.population) - 0.12 + trade.value * 0.012, 0, 0.8);
const births = Math.max(1, Math.floor(city.population * (0.012 + prosperity * 0.012 + clamp(trade.value, 0, 1.8) * 0.0015)));
const birthRoom = Math.max(0, Math.floor(agePopulationLimit - city.population));
const births = Math.min(birthRoom, Math.max(1, Math.floor(city.population * (0.012 + prosperity * 0.012 + clamp(trade.value, 0, 1.8) * 0.0015))));
city.population += births;
city.storedResources -= births * 0.55;
addBirthsToComposition(city.ethnicityComposition, births, this.rng);
}
if (city.population > agePopulationLimit) {
const overLimit = city.population - agePopulationLimit;
const ageLoss = Math.min(city.population, Math.max(1, Math.ceil(overLimit * (SimConfig.city.agingOverCapAttrition ?? 0.035))));
city.population -= ageLoss;
removeFromComposition(city.ethnicityComposition, ageLoss);
city.strength -= Math.min(0.08, ageLoss / Math.max(1, city.population + ageLoss) * 0.18);
}
if (city.age > 20 && city.activeVisitors < 2 && city.storedResources < city.population * 0.03 && city.population > 0) {
const attrition = Math.max(1, Math.ceil(city.population * (city.activeVisitors === 0 ? 0.025 : 0.010)));
city.population -= attrition;
@ -1733,55 +1760,19 @@ const from = this.tradeLinkEndpointId(link.from ?? link.fromCityId ?? link.cityA
const to = this.tradeLinkEndpointId(link.to ?? link.toCityId ?? link.cityBId ?? link.bId ?? link.targetId ?? link.destinationId ?? link.target ?? link.destination ?? link.b ?? link.cityB);
return from != null && to != null ? [from, to] : null;
}
polityTradeNeighbors(city, polityId) {
if (!city || polityId == null) return [];
const neighbors = [];
for (const link of this.tradeLinks || []) {
const endpoints = this.tradeLinkEndpointIds(link);
if (!endpoints) continue;
const [from, to] = endpoints;
const otherId = from === city.id ? to : to === city.id ? from : null;
if (otherId == null) continue;
const other = this.getCityById(otherId);
if (other?.polityId === polityId) neighbors.push(other);
}
return neighbors;
}
tradeAccessToCenter(city, center, polityId) {
if (!city || !center || polityId == null) return { reachable: false, hops: Infinity, transitCities: [] };
if (city.id === center.id) return { reachable: true, hops: 0, transitCities: [] };
if (this.hasDirectTradeConnection(city, center)) return { reachable: true, hops: 1, transitCities: [] };
const maxDepth = SimConfig.polityAccess?.maxSearchDepth ?? 8;
const visited = new Set([city.id]);
const queue = [{ city, hops: 0, transitCities: [] }];
while (queue.length) {
const current = queue.shift();
if (current.hops >= maxDepth) continue;
for (const neighbor of this.polityTradeNeighbors(current.city, polityId)) {
if (visited.has(neighbor.id)) continue;
const hops = current.hops + 1;
if (neighbor.id === center.id) return { reachable: true, hops, transitCities: current.transitCities };
visited.add(neighbor.id);
queue.push({ city: neighbor, hops, transitCities: [...current.transitCities, neighbor] });
}
}
return { reachable: false, hops: Infinity, transitCities: [] };
}
tradeAccessLoyaltyPenalty(city, center, polity) {
const cfg = SimConfig.polityAccess;
if (!cfg?.enabled || !city || !center || !polity || city.id === center.id || this.hasDirectTradeConnection(city, center)) return 0;
const access = this.tradeAccessToCenter(city, center, polity.id);
if (!access.reachable) return clamp(cfg.noAccessPenalty, 0, 0.07);
let penalty = cfg.indirectPenalty + Math.max(0, access.hops - 1) * cfg.perHopPenalty;
const originEthnicity = this.dominantCityEthnicity(city);
for (const transitCity of access.transitCities) {
if ((transitCity.loyalty ?? 0.5) < 0.35) penalty += cfg.lowLoyaltyTransitPenalty;
const transitEthnicity = this.dominantCityEthnicity(transitCity);
if (originEthnicity !== null && transitEthnicity !== null && transitEthnicity !== originEthnicity) {
penalty += cfg.foreignTransitPenalty;
return clamp(cfg.noAccessPenalty, 0, 0.24);
}
}
return clamp(penalty, 0, 0.07);
rollLeaderCharisma() {
const cfg = SimConfig.polity;
const min = cfg?.charismaMin ?? 0.5;
const max = cfg?.charismaMax ?? 3.0;
const targetAverage = cfg?.charismaAverage ?? 1.0;
const normalizedAverage = clamp((targetAverage - min) / Math.max(0.001, max - min), 0.001, 0.999);
const exponent = (1 / normalizedAverage) - 1;
return min + (max - min) * Math.pow(this.rng.next(), exponent);
}
effectiveDistance(cityA, cityB) {
let distance = this.distanceBetweenCities(cityA, cityB);
@ -1799,7 +1790,7 @@ color: hslToRgb((id * 0.38196601125) % 1, 0.58, 0.62),
founded: this.year,
legitimacy: this.rng.range(0.68, 0.94),
cohesion: this.rng.range(0.58, 0.9),
charisma: this.rng.range(0.5, 1.5),
charisma: this.rollLeaderCharisma(),
leaderStarted: this.year,
leaderTenureYears: this.rng.range(24, 68),
crisis: 0,
@ -1905,17 +1896,19 @@ while (this.graphEvents.length > 160) this.graphEvents.shift();
}
selectNewLeader(polity, forced = false) {
if (!polity) return;
const previousCharisma = clamp(polity.charisma ?? 1, 0.5, 1.5);
const charismaMin = SimConfig.polity?.charismaMin ?? 0.5;
const charismaMax = SimConfig.polity?.charismaMax ?? 3.0;
const previousCharisma = clamp(polity.charisma ?? 1, charismaMin, charismaMax);
const center = this.getCityById(polity.centerCityId);
const centerStability = center ? clamp(center.loyalty ?? 0.5, 0, 1) : 0.5;
const institutionalBias = ((polity.legitimacy ?? 0.7) + (polity.cohesion ?? 0.6) + centerStability) / 3;
const randomLeader = this.rng.range(0.5, 1.5);
const randomLeader = this.rollLeaderCharisma();
const continuity = forced ? 0.18 : 0.34;
const institutionalPull = 0.82 + institutionalBias * 0.36;
const nextCharisma = clamp(
previousCharisma * continuity + randomLeader * (1 - continuity) * institutionalPull,
0.5,
1.5
charismaMin,
charismaMax
);
polity.charisma = nextCharisma;
polity.leaderStarted = this.year;
@ -1927,7 +1920,7 @@ if (forced) polity.crisis = clamp((polity.crisis || 0) + 0.04, 0, 1.5);
}
updatePolityLeaders() {
for (const polity of this.polities) {
polity.charisma = clamp(polity.charisma ?? 1, 0.5, 1.5);
polity.charisma = clamp(polity.charisma ?? 1, SimConfig.polity?.charismaMin ?? 0.5, SimConfig.polity?.charismaMax ?? 3.0);
polity.leaderStarted ??= polity.founded ?? this.year;
polity.leaderTenureYears ??= this.rng.range(24, 68);
const tenureYears = (this.year - polity.leaderStarted) / WEEKS_PER_YEAR;
@ -2194,7 +2187,7 @@ tech * 24 -
instability * 12 -
agePressure * 6
);
return basePower * clamp(polity.charisma ?? 1, 0.5, 1.5);
return basePower * clamp(polity.charisma ?? 1, SimConfig.polity?.charismaMin ?? 0.5, SimConfig.polity?.charismaMax ?? 3.0);
}
polityInfluenceRange(polity, wartime = false) {
const power = this.polityPower(polity);
@ -2679,6 +2672,34 @@ city.receivedAid = true;
city.loyalty = clamp(city.loyalty + 0.05, 0, 1);
}
}
this.restoreAgedPolityCities(polity, cities);
}
}
restoreAgedPolityCities(polity, cities) {
const cfg = SimConfig.city;
if (!polity || !cities?.length || (polity.treasury || 0) <= 0) return;
const minWear = cfg.agingRestoreMinWear ?? 0.08;
let budget = Math.min(polity.treasury, (polity.treasury || 0) * (cfg.agingRestoreBudgetShare ?? 0.18));
if (budget <= 0) return;
const costPerWear = Math.max(1, cfg.agingRestoreCostPerWear ?? 70);
const candidates = [...cities]
.filter(city => (city.ageWear || 0) > minWear && city.population > 0)
.sort((a, b) => {
const aPressure = Math.max(0, (a.population || 0) - this.cityAgePopulationLimit(a));
const bPressure = Math.max(0, (b.population || 0) - this.cityAgePopulationLimit(b));
return bPressure - aPressure || (b.population || 0) - (a.population || 0);
});
for (const city of candidates) {
if (budget <= 0 || polity.treasury <= 0) break;
const wear = city.ageWear || 0;
const maxRestoreCost = Math.min(budget, polity.treasury, wear * costPerWear);
if (maxRestoreCost <= 0) continue;
const restoredWear = maxRestoreCost / costPerWear;
city.ageWear = clamp(wear - restoredWear, 0, 1);
polity.treasury -= maxRestoreCost;
budget -= maxRestoreCost;
city.receivedAid = true;
city.loyalty = clamp((city.loyalty ?? 0.5) + restoredWear * 0.24, 0, 1);
}
}
erodePolityLegitimacy() {
@ -2767,8 +2788,7 @@ delta += clamp((perCapita - 0.10) * 0.07, -0.025, 0.045);
delta += this.sameDominantEthnicity(city, center) ? 0.03 : -0.012;
delta += clamp(0.045 - distance * 0.0011, -0.025, 0.045);
const directCenterTrade = this.hasDirectTradeConnection(city, center);
if (directCenterTrade) delta += 0.025;
else delta -= this.tradeAccessLoyaltyPenalty(city, center, polity);
if (!directCenterTrade) delta -= this.tradeAccessLoyaltyPenalty(city, center, polity);
if (city.receivedAid) delta += 0.04;
delta -= 0.004;
if (city.storedResources < city.population * 0.05) delta -= 0.035;