From 7fa3696d27577d873a4dec10e6bbe87d99375252 Mon Sep 17 00:00:00 2001 From: 33333-33333 Date: Tue, 12 May 2026 21:11:53 +0900 Subject: [PATCH] tech --- index.html | 4 +- script.js | 468 ++++++++++++++++++++++++++++++++++++++++++----------- 2 files changed, 378 insertions(+), 94 deletions(-) diff --git a/index.html b/index.html index e0cbcf9..5f90e99 100644 --- a/index.html +++ b/index.html @@ -55,6 +55,7 @@ + @@ -62,13 +63,14 @@
Date
0y 1m
-
Total population
0
Active groups
0
Urban population
0
Ethnicities
0
Cities
0
States
0
Trade routes
0
+
Farming knowledge
0.000
+
Metallurgy knowledge
0.000
Collapse deaths
0
Frame cost
0ms
diff --git a/script.js b/script.js index f741e7c..cd3d9f2 100644 --- a/script.js +++ b/script.js @@ -38,13 +38,14 @@ const els = { worldSize: document.getElementById("worldSize"), viewMode: document.getElementById("viewMode"), year: document.getElementById("year"), - population: document.getElementById("population"), activeGroups: document.getElementById("activeGroups"), urbanPopulation: document.getElementById("urbanPopulation"), ethnicities: document.getElementById("ethnicities"), cities: document.getElementById("cities"), polities: document.getElementById("polities"), routes: document.getElementById("routes"), + farmingKnowledge: document.getElementById("farmingKnowledge"), + metallurgyKnowledge: document.getElementById("metallurgyKnowledge"), deaths: document.getElementById("deaths"), frameCost: document.getElementById("frameCost"), ethnicityList: document.getElementById("ethnicityList"), @@ -388,6 +389,7 @@ class Simulation { this.deaths = 0; this.maxAgents = Math.max(initialAgents * 1.25, 30000); this.tileEthnicities = new Map(); + this.tileAgents = new Map(); this.spawnInitialAgents(initialAgents); this.rebuildOccupancy(); this.updateEthnicStats(); @@ -465,7 +467,14 @@ class Simulation { alive: true, foreignContact: 0, contactEthnicity: ethnicity, - settled: 0 + settled: 0, + movedThisStep: false, + tech: { + farming: 0, + metallurgy: 0 + }, + farmingWork: 0, + lastFarmTile: -1 }; } @@ -530,7 +539,14 @@ class Simulation { for (const a of this.agents) { if (!a.alive) continue; this.moveAgent(a); + } + this.rebuildOccupancy(); + + for (const a of this.agents) { + if (!a.alive) continue; + this.updateAgentTechnology(a); this.gatherConsumeReproduce(a, offspring); + if (a.alive) this.payTechnologyCostOrForget(a); this.resolveAssimilation(a); } @@ -543,13 +559,15 @@ class Simulation { this.updateWorldFields(); this.rebuildOccupancy(); - if (this.year % years(5) === 0) { + if (this.year % years(1) === 0) { this.updateCities(); + } + if (this.year % years(5) === 0) { this.updateTradeRoutes(); } this.updatePolities(); this.updateEthnicStats(); - if (this.year % years(90) === 0) { + if (this.year % years(45) === 0) { this.splitDivergentEthnicities(); this.updateEthnicStats(); } @@ -560,10 +578,17 @@ class Simulation { const w = this.world; w.pressure.fill(0); this.tileEthnicities.clear(); + this.tileAgents.clear(); for (const a of this.agents) { if (!a.alive) continue; const i = w.idx(a.x, a.y); w.pressure[i]++; + let agents = this.tileAgents.get(i); + if (!agents) { + agents = []; + this.tileAgents.set(i, agents); + } + agents.push(a); let counts = this.tileEthnicities.get(i); if (!counts) { counts = new Map(); @@ -587,6 +612,7 @@ class Simulation { const waterAdaptation = ethnicClimate?.climateHumidity ?? 0.45; if (localPressure < 7 && this.rng.next() < sedentary * 0.58) { a.settled++; + a.movedThisStep = false; return; } const radius = a.traits.mobility > 0.62 && sedentary < 0.52 && this.rng.next() < 0.16 ? 2 : 1; @@ -638,18 +664,234 @@ class Simulation { a.y = bestY; const to = w.idx(a.x, a.y); if (from !== to) { + a.movedThisStep = true; + a.farmingWork = 0; + a.lastFarmTile = to; const depositScale = w.terrain[to] === Terrain.WATER ? 0.35 : 1; w.pheromone[from] += (w.tradeRoute[from] ? 0.08 : 0.22) * depositScale; w.pheromone[to] += (w.tradeRoute[to] ? 0.06 : 0.15) * depositScale; a.settled = Math.max(0, a.settled - 1); } else { + a.movedThisStep = false; a.settled++; } } + ensureAgentTech(a) { + a.tech ??= { farming: 0, metallurgy: 0 }; + a.tech.farming ??= 0; + a.tech.metallurgy ??= 0; + a.tech.farming = clamp(a.tech.farming, 0, 1); + a.tech.metallurgy = clamp(a.tech.metallurgy, 0, 1); + a.farmingWork ??= 0; + a.lastFarmTile ??= -1; + a.movedThisStep ??= false; + } + + farmabilityAt(tile) { + const t = this.world.terrain[tile]; + if (t === Terrain.FERTILE) return 1.0; + if (t === Terrain.PLAINS) return 0.75; + if (t === Terrain.FOREST) return 0.55; + if (t === Terrain.DESERT) return 0.25; + return 0.0; + } + + updateAgentTechnology(agent, payMaintenance = false) { + if (!agent.alive) return; + this.ensureAgentTech(agent); + this.updateFarmingWork(agent); + this.tryInventTechnology(agent); + this.spreadTechnology(agent); + this.improveTechnologyFromDensity(agent); + if (payMaintenance) this.payTechnologyCostOrForget(agent); + } + + updateFarmingWork(agent) { + const w = this.world; + const tile = w.idx(agent.x, agent.y); + if (agent.lastFarmTile !== tile) { + agent.farmingWork = 0; + agent.lastFarmTile = tile; + } + if (agent.movedThisStep) return; + + const farming = agent.tech?.farming || 0; + if (farming <= 0.02) return; + if (agent.settled > 0) { + agent.farmingWork = clamp((agent.farmingWork || 0) + farming * 0.015, 0, 1); + } + } + + tryInventTechnology(agent) { + const w = this.world; + const tile = w.idx(agent.x, agent.y); + const settledFactor = clamp((agent.settled || 0) / 12, 0, 1); + + if ((agent.tech?.farming || 0) <= 0.02) { + const farmingBase = 0.000090; + const farmability = this.farmabilityAt(tile); + const sedentary = getSedentary(agent.traits); + const farmingChance = + farmingBase * + (0.25 + farmability) * + (0.4 + sedentary) * + (0.3 + settledFactor); + if (this.rng.next() < farmingChance) this.grantTechnologyAround(agent, "farming", 0.24); + } + + if ((agent.tech?.metallurgy || 0) <= 0.02) { + const metallurgyBase = 0.000055; + const mineral = clamp(w.mineral[tile], 0, 1); + const mineralTerrainBonus = + w.terrain[tile] === Terrain.MINERAL || w.terrain[tile] === Terrain.MOUNTAIN + ? 1.8 + : 1.0; + const metallurgyChance = + metallurgyBase * + (0.2 + mineral) * + mineralTerrainBonus * + (0.5 + settledFactor); + if (this.rng.next() < metallurgyChance) this.grantTechnologyAround(agent, "metallurgy", 0.20); + } + } + + grantTechnologyAround(agent, techName, amount) { + this.ensureAgentTech(agent); + for (const other of this.localAgentsNear(agent.x, agent.y, 2)) { + if (!other.alive) continue; + this.ensureAgentTech(other); + const distance = Math.abs(agent.x - other.x) + Math.abs(agent.y - other.y); + if (distance > 2) continue; + const gain = amount * (other === agent ? 1 : 0.55); + other.tech[techName] = clamp(Math.max(other.tech[techName] || 0, gain), 0, 1); + } + } + + payTechnologyCostOrForget(agent) { + if (!agent.alive) return; + this.ensureAgentTech(agent); + const farming = agent.tech.farming || 0; + const metallurgy = agent.tech.metallurgy || 0; + const cost = farming * 0.010 + metallurgy * 0.022; + if (cost <= 0) return; + + if (agent.resources >= cost) { + agent.resources -= cost; + return; + } + + const shortage = cost - Math.max(0, agent.resources); + agent.resources = Math.max(0, agent.resources - cost); + const decay = clamp(0.015 + shortage * 0.04, 0.015, 0.12); + agent.tech.farming = Math.max(0, farming - decay); + agent.tech.metallurgy = Math.max(0, metallurgy - decay * 1.15); + if (agent.tech.farming < 0.02) agent.tech.farming = 0; + if (agent.tech.metallurgy < 0.02) agent.tech.metallurgy = 0; + if (agent.tech.farming <= 0) agent.farmingWork = 0; + } + + spreadTechnology(agent) { + const w = this.world; + const tile = w.idx(agent.x, agent.y); + const assimilation = agent.traits.assimilation || 0; + const ethnocentrism = agent.traits.ethnocentrism || 0; + let checked = 0; + + for (const other of this.localAgentsNear(agent.x, agent.y, 1)) { + if (other === agent || !other.alive) continue; + if (Math.abs(agent.x - other.x) + Math.abs(agent.y - other.y) > 1) continue; + if (++checked > 12) break; + this.ensureAgentTech(other); + const sameEthnicity = agent.ethnicity === other.ethnicity; + let chance = + 0.0035 + + assimilation * 0.006 - + ethnocentrism * 0.0012; + + if (sameEthnicity) chance += 0.004; + if (w.tradeRoute[tile]) chance += 0.003; + if (w.city[tile] >= 0) chance += 0.003; + chance = clamp(chance, 0.0008, 0.028); + + this.learnTechnologyFrom(agent, other, "farming", chance); + this.learnTechnologyFrom(agent, other, "metallurgy", chance); + } + } + + learnTechnologyFrom(agent, other, techName, chance) { + const current = agent.tech[techName] || 0; + const otherLevel = other.tech?.[techName] || 0; + if (otherLevel > current + 0.04 && this.rng.next() < chance) { + agent.tech[techName] = clamp(current + (otherLevel - current) * 0.14, 0, 1); + } + } + + improveTechnologyFromDensity(agent) { + let farmingCount = 0; + let metallurgyCount = 0; + let farmingSum = 0; + let metallurgySum = 0; + + for (const other of this.localAgentsNear(agent.x, agent.y, 2)) { + if (!other.alive) continue; + this.ensureAgentTech(other); + const distance = Math.abs(agent.x - other.x) + Math.abs(agent.y - other.y); + if (distance > 2) continue; + const farming = other.tech.farming || 0; + const metallurgy = other.tech.metallurgy || 0; + if (farming > 0.1) { + farmingCount++; + farmingSum += farming; + } + if (metallurgy > 0.1) { + metallurgyCount++; + metallurgySum += metallurgy; + } + } + + if (farmingCount >= 3) { + const avgFarming = farmingSum / farmingCount; + agent.tech.farming = clamp(agent.tech.farming + 0.0012 * farmingCount * avgFarming, 0, 1); + } + + if (metallurgyCount >= 3) { + const avgMetallurgy = metallurgySum / metallurgyCount; + agent.tech.metallurgy = clamp(agent.tech.metallurgy + 0.00085 * metallurgyCount * avgMetallurgy, 0, 1); + } + } + + localAgentsNear(x, y, radius) { + const w = this.world; + const found = []; + for (let dy = -radius; dy <= radius; dy++) { + for (let dx = -radius; dx <= radius; dx++) { + const tx = x + dx; + const ty = y + dy; + if (tx < 0 || ty < 0 || tx >= w.size || ty >= w.size) continue; + const agents = this.tileAgents.get(w.idx(tx, ty)); + if (agents) found.push(...agents); + } + } + return found; + } + + farmingGatherMultiplier(agent, tile) { + const farmingLevel = agent.tech?.farming || 0; + const farmingWork = agent.farmingWork || 0; + const farmability = this.farmabilityAt(tile); + return clamp(1 + farmingLevel * farmingWork * farmability * 2, 1, 3); + } + + metallurgyGatherMultiplier(agent, tile) { + const metallurgyLevel = agent.tech?.metallurgy || 0; + return 1 + metallurgyLevel * clamp(this.world.mineral[tile], 0, 1); + } + gatherConsumeReproduce(a, offspring) { const w = this.world; const i = w.idx(a.x, a.y); + this.ensureAgentTech(a); const ethnicity = this.ethnicities.get(a.ethnicity); const climateMismatch = this.climateMismatch(a.ethnicity, i); const climateFit = clamp(1 - climateMismatch * 1.35, 0.18, 1); @@ -658,7 +900,11 @@ class Simulation { const desertForage = drylandAdapted && w.terrain[i] === Terrain.DESERT ? 0.55 : 0; const productivity = 0.45 + w.fertility[i] * 1.35 + w.mineral[i] * 0.45 + w.farmland[i] * 0.72 + cityMarket + desertForage; const pressurePenalty = 1 / (1 + Math.max(0, w.pressure[i] - 2) * 0.18); - const gathered = Math.min(w.resource[i], productivity * climateFit * pressurePenalty * this.rng.range(0.45, 1.2)); + const baseGatherAmount = productivity * climateFit * pressurePenalty * this.rng.range(0.45, 1.2); + const gathered = Math.min( + w.resource[i], + baseGatherAmount * this.farmingGatherMultiplier(a, i) * this.metallurgyGatherMultiplier(a, i) + ); w.resource[i] -= gathered; a.resources += gathered; const waterAdaptation = ethnicity?.climateHumidity ?? 0.45; @@ -735,8 +981,8 @@ class Simulation { a.foreignContact++; const pressure = dominant.count / Math.max(1, dominant.total); - const chance = a.traits.assimilation * pressure * Math.min(1, a.foreignContact / 120); - if (this.rng.next() < chance * 0.035) { + const chance = a.traits.assimilation * pressure * Math.min(1, a.foreignContact / 30); + if (this.rng.next() < chance * 0.14) { a.ethnicity = dominant.id; a.traits = blendTraits(a.traits, dominant.averageTraits, 0.08 + a.traits.assimilation * 0.22); a.foreignContact = 0; @@ -761,6 +1007,16 @@ class Simulation { } } } + for (const city of this.cities) { + const distance = Math.abs(city.x - x) + Math.abs(city.y - y); + if (distance > 5 || !city.ethnicityComposition?.size) continue; + const influence = clamp((6 - distance) / 6, 0, 1) * clamp(Math.sqrt(city.population) / 8, 0.5, 8); + for (const [id, count] of city.ethnicityComposition) { + const weighted = Math.max(1, Math.round(count * influence * 0.08)); + total += weighted; + counts.set(id, (counts.get(id) || 0) + weighted); + } + } let best = null; for (const [id, count] of counts) { if (id !== self && (!best || count > best.count)) best = { id, count, total }; @@ -817,7 +1073,7 @@ class Simulation { 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 < 50) { - const foundingChance = clamp((avgSedentary - 0.28) * 1.55, 0, 0.92); + const foundingChance = clamp((avgSedentary - 0.28) * 1.55 * 3, 0, 0.98); if (avgSedentary < 0.34 || this.rng.next() > foundingChance) continue; city = this.createCity(i % w.size, Math.floor(i / w.size), group); this.cities.push(city); @@ -899,18 +1155,18 @@ class Simulation { if (!this.cities.length) return; const absorbed = []; for (const a of this.agents) { - if (!a.alive || a.settled < 9) { + if (!a.alive || a.settled < 5) { absorbed.push(a); continue; } - const city = this.findCityNear(a.x, a.y, 5); + const city = this.findCityNear(a.x, a.y, 7); const sedentary = getSedentary(a.traits); - if (!city || this.rng.next() > sedentary * sedentary * 0.75) { + if (!city || this.rng.next() > sedentary * 0.85) { absorbed.push(a); continue; } - const migrants = 1 + Math.floor(Math.min(5, a.resources / 10)); - const urbanWeight = clamp((sedentary - 0.2) * 1.35, 0.05, 1); + const migrants = 1 + Math.floor(Math.min(8, a.resources / 8)); + const urbanWeight = clamp((sedentary - 0.14) * 1.5, 0.08, 1); city.population += migrants; city.storedResources += Math.max(0, a.resources) * 0.65; const urbanCount = this.weightedUrbanContribution(migrants, urbanWeight); @@ -952,12 +1208,13 @@ class Simulation { } city.storedResources += harvested; - const upkeep = city.population * 0.012; + const upkeep = city.population * 0.010; city.storedResources -= upkeep; - if (city.storedResources > city.population * 0.24 && city.population > 0) { - const births = Math.max(1, Math.floor(city.population * 0.004)); + if (city.storedResources > city.population * 0.16 && city.population > 0) { + const prosperity = clamp(city.storedResources / Math.max(1, city.population) - 0.12, 0, 0.8); + const births = Math.max(1, Math.floor(city.population * (0.012 + prosperity * 0.012))); city.population += births; - city.storedResources -= births * 0.7; + city.storedResources -= births * 0.55; addBirthsToComposition(city.ethnicityComposition, births); } if (city.storedResources < 0) { @@ -1062,11 +1319,11 @@ class Simulation { id, centerCityId: centerCity.id, cityIds: new Set([centerCity.id]), - treasury: 0, + treasury: Math.max(0, centerCity.storedResources * 0.12), color: hslToRgb((id * 0.38196601125) % 1, 0.58, 0.62), founded: this.year, - legitimacy: this.rng.range(0.55, 0.9), - cohesion: this.rng.range(0.45, 0.85), + legitimacy: this.rng.range(0.68, 0.94), + cohesion: this.rng.range(0.58, 0.9), crisis: 0, lastCrisisYear: this.year }; @@ -1096,10 +1353,6 @@ class Simulation { return this.polityHistory.get(polity.id); } - totalPolityPopulation(polity) { - return this.getPolityCities(polity).reduce((sum, city) => sum + city.population, 0); - } - averagePolityLoyalty(polity) { const cities = this.getPolityCities(polity); const subordinates = cities.filter(city => city.id !== polity.centerCityId); @@ -1179,7 +1432,7 @@ class Simulation { const center = this.getCityById(polity.centerCityId); if (!center || cities.length <= 1) return 0; - const cityCountPressure = Math.max(0, cities.length - 3) * 0.18; + const cityCountPressure = Math.max(0, cities.length - 5) * 0.08; let distanceSum = 0; let count = 0; for (const city of cities) { @@ -1189,7 +1442,7 @@ class Simulation { } const avgDistance = count ? distanceSum / count : 0; - const distancePressure = Math.max(0, avgDistance - 18) * 0.018; + const distancePressure = Math.max(0, avgDistance - 28) * 0.008; return clamp(cityCountPressure + distancePressure, 0, 2); } @@ -1200,13 +1453,13 @@ class Simulation { let poor = 0; for (const city of cities) { const perCapita = city.storedResources / Math.max(1, city.population); - if (perCapita < 0.08) poor++; + if (perCapita < 0.06) 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); + const treasuryStress = treasuryPerCity < 2 ? (2 - treasuryPerCity) / 2 : 0; + return clamp(povertyRate * 0.55 + treasuryStress * 0.28, 0, 1.5); } polityEthnicFragmentation(polity) { @@ -1232,14 +1485,14 @@ class Simulation { 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; + const legitimacyBuffer = (polity.legitimacy ?? 0.7) * 0.85; + const cohesionBuffer = (polity.cohesion ?? 0.6) * 0.62; return clamp( - agePressure * 0.55 + - overextension * 0.35 + - resourceStress * 0.55 + - fragmentation * 0.35 + - (polity.crisis || 0) * 0.65 - + agePressure * 0.35 + + overextension * 0.24 + + resourceStress * 0.38 + + fragmentation * 0.22 + + (polity.crisis || 0) * 0.42 - legitimacyBuffer - cohesionBuffer, 0, @@ -1249,22 +1502,22 @@ class Simulation { foundPolities() { for (const city of this.cities) { - if (city.polityId !== null || city.population < 80 || city.storedResources < 30) continue; + if (city.polityId !== null || city.population < 55 || city.storedResources < 18) continue; const centerInfluence = this.cityInfluence(city); let absorbed = 0; for (const other of this.cities) { - if (absorbed >= 2) break; + if (absorbed >= 3) break; if (other === city || other.polityId !== null) continue; - if (this.distanceBetweenCities(city, other) > 28) continue; + if (this.distanceBetweenCities(city, other) > 34) continue; const targetInfluence = this.cityInfluence(other); - if (centerInfluence <= targetInfluence * 1.25) continue; + if (centerInfluence <= targetInfluence * 1.08) continue; const proximity = 1 / (1 + this.distanceBetweenCities(city, other) * 0.08); const routeBonus = this.hasDirectTradeConnection(city, other) ? 1.5 : 1.0; const dominanceScore = (centerInfluence / (targetInfluence + 1)) * proximity * routeBonus; - if (dominanceScore > 0.9 && this.rng.next() < 0.18) { + if (dominanceScore > 0.68 && this.rng.next() < 0.32) { const polity = city.polityId === null ? this.createPolity(city) : this.getPolityById(city.polityId); - this.addCityToPolity(other, polity, 0.55); + this.addCityToPolity(other, polity, 0.68); absorbed++; } } @@ -1278,18 +1531,18 @@ class Simulation { const centerInfluence = this.cityInfluence(center); let absorbed = 0; for (const city of this.cities) { - if (absorbed >= 1) break; + if (absorbed >= 2) break; if (city.polityId !== null || city.id === center.id) continue; const distance = this.effectiveDistance(center, city); - if (distance > 32) continue; + if (distance > 44) continue; const targetInfluence = this.cityInfluence(city); - if (centerInfluence <= targetInfluence) continue; + if (centerInfluence <= targetInfluence * 0.9) continue; const dominanceScore = (centerInfluence / (targetInfluence + 1)) * (1 / (1 + distance * 0.08)) * (this.hasDirectTradeConnection(center, city) ? 1.5 : 1.0); - if (dominanceScore > 0.85 && this.rng.next() < 0.12) { - this.addCityToPolity(city, polity, 0.48); + if (dominanceScore > 0.62 && this.rng.next() < 0.24) { + this.addCityToPolity(city, polity, 0.62); absorbed++; } } @@ -1303,24 +1556,24 @@ class Simulation { for (const city of cities) city.receivedAid = false; for (const city of cities) { if (city.id === centerId) continue; - const tax = city.storedResources * 0.04; + const tax = city.storedResources * 0.035; 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; + cities.length * 0.24 + + overextension * 1.1 + + agePressure * cities.length * 0.18; 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); + polity.crisis = clamp((polity.crisis || 0) + deficit * 0.012, 0, 1.5); } - const maintenance = Math.pow(cities.length, 1.15) * 0.18; + const maintenance = Math.pow(cities.length, 1.12) * 0.10; if (maintenance > 0) { const center = this.getCityById(centerId); const treasuryPayment = Math.min(polity.treasury, maintenance); @@ -1330,7 +1583,7 @@ class Simulation { const paidByCenter = this.drainCityResources(center, unpaid); if (paidByCenter < unpaid) { for (const city of cities) { - if (city.id !== centerId) city.loyalty = clamp(city.loyalty - 0.025, 0, 1); + if (city.id !== centerId) city.loyalty = clamp(city.loyalty - 0.012, 0, 1); } } } @@ -1360,47 +1613,47 @@ class Simulation { const resourceStress = this.polityResourceStress(polity); const overextension = this.polityOverextension(polity); const erosion = - 0.004 + - agePressure * 0.006 + - resourceStress * 0.006 + - overextension * 0.003; + 0.0015 + + agePressure * 0.003 + + resourceStress * 0.003 + + overextension * 0.0015; 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.legitimacy = clamp(polity.legitimacy + 0.008, 0, 1); + polity.cohesion = clamp(polity.cohesion + 0.005, 0, 1); } - polity.crisis = clamp((polity.crisis || 0) * 0.92, 0, 1.5); + polity.crisis = clamp((polity.crisis || 0) * 0.88, 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; + if (age < 140) continue; + if ((this.year - (polity.lastCrisisYear ?? 0)) / MONTHS_PER_YEAR < 160) continue; const instability = this.polityInstability(polity); const agePressure = this.polityAgePressure(polity); - const chance = 0.015 + agePressure * 0.035 + instability * 0.025; + const chance = 0.006 + agePressure * 0.018 + instability * 0.014; 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 + 0.08 + + agePressure * this.rng.range(0.08, 0.20) + + instability * this.rng.range(0.05, 0.16), + 0.05, + 0.36 ); 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); + polity.legitimacy = clamp((polity.legitimacy ?? 0.7) - severity * 0.30, 0, 1); + polity.cohesion = clamp((polity.cohesion ?? 0.6) - severity * 0.18, 0, 1); const center = this.getCityById(polity.centerCityId); for (const city of this.getPolityCities(polity)) { @@ -1448,18 +1701,18 @@ class Simulation { const perCapita = city.storedResources / Math.max(1, city.population); const distance = this.effectiveDistance(city, center); let delta = 0; - delta += clamp((perCapita - 0.12) * 0.08, -0.04, 0.04); - delta += this.sameDominantEthnicity(city, center) ? 0.025 : -0.025; - delta += clamp(0.035 - distance * 0.0015, -0.045, 0.035); - if (this.hasDirectTradeConnection(city, center)) delta += 0.015; + 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); + if (this.hasDirectTradeConnection(city, center)) delta += 0.025; 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; + delta -= 0.004; + if (city.storedResources < city.population * 0.05) delta -= 0.035; + delta -= instability * 0.020; + delta -= agePressure * 0.010; + delta -= overextension * clamp(distance / 48, 0, 1) * 0.014; + if ((polity.legitimacy ?? 0.7) < 0.25) delta -= 0.020; + if ((polity.crisis || 0) > 0.6) delta -= 0.014; city.loyalty = clamp(city.loyalty + delta, 0, 1); } } @@ -1469,13 +1722,13 @@ class Simulation { 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); + const threshold = clamp(0.14 + instability * 0.05 + agePressure * 0.03, 0.14, 0.28); for (const city of this.getPolityCities(polity)) { if (city.id === polity.centerCityId || city.loyalty >= threshold) continue; const chance = - (threshold - city.loyalty) * 0.55 + - instability * 0.025 + - agePressure * 0.02; + (threshold - city.loyalty) * 0.28 + + instability * 0.012 + + agePressure * 0.010; if (this.rng.next() < chance) this.removeCityFromPolity(city); } } @@ -1818,23 +2071,23 @@ class Simulation { splitDivergentEthnicities() { for (const e of this.ethnicities.values()) { - if (e.activeTraitPopulation < 35 || e.population < 55 || e.diversity < 0.22) continue; + if (e.activeTraitPopulation < 24 || e.population < 40 || e.diversity < 0.14) continue; const candidates = []; let tempSum = 0; let humidSum = 0; for (const a of this.agents) { if (a.ethnicity !== e.id) continue; - const far = traitDistance(a.traits, e.averageTraits) > e.diversity * 1.12; - const spatial = Math.hypot(a.x - e.centroidX, a.y - e.centroidY) > this.world.size * 0.12; + const far = traitDistance(a.traits, e.averageTraits) > e.diversity * 0.95; + const spatial = Math.hypot(a.x - e.centroidX, a.y - e.centroidY) > this.world.size * 0.09; const tile = this.world.idx(a.x, a.y); - const climate = this.climateMismatch(e.id, tile) > 0.26; - if ((far || spatial || climate) && this.rng.next() < 0.58) { + const climate = this.climateMismatch(e.id, tile) > 0.18; + if ((far || spatial || climate) && this.rng.next() < 0.72) { candidates.push(a); tempSum += this.world.temperature[tile]; humidSum += this.world.humidity[tile]; } } - if (candidates.length >= 10) { + if (candidates.length >= 6) { const newId = this.createEthnicity(e.id, { temperature: tempSum / candidates.length, humidity: humidSum / candidates.length @@ -1928,6 +2181,7 @@ class Simulation { sim.world.pressure.set(unpackArray(state.world.pressure, Float32Array)); sim.agents = state.agents || []; + for (const a of sim.agents) sim.ensureAgentTech(a); sim.ethnicities = new Map((state.ethnicities || []).map(e => [e.id, { id: e.id, parent: e.parent, @@ -2028,6 +2282,8 @@ function render() { const polity = city && city.polityId !== null ? sim.getPolityById(city.polityId) : null; if (polity) color = mix(color, polity.color, 0.5); } + } else if (mode === "technology") { + color = mix(terrainInfo[w.terrain[i]].color, [44, 42, 48], 0.35); } else { color = terrainInfo[w.terrain[i]].color; } @@ -2107,6 +2363,15 @@ function drawAgentsAndCities(mode) { if (!e) continue; ctx.fillStyle = `rgb(${e.color[0]},${e.color[1]},${e.color[2]})`; ctx.fillRect(a.x, a.y, 1, 1); + } else if (mode === "technology") { + sim.ensureAgentTech(a); + const farming = a.tech.farming || 0; + const metallurgy = a.tech.metallurgy || 0; + const tech = clamp(Math.max(farming, metallurgy), 0, 1); + if (tech <= 0.01) continue; + const color = mix([95, 171, 91], [202, 169, 102], metallurgy / Math.max(0.001, farming + metallurgy)); + ctx.fillStyle = `rgba(${color[0]},${color[1]},${color[2]},${clamp(0.32 + tech * 0.68, 0.32, 1)})`; + ctx.fillRect(a.x, a.y, 1, 1); } else { ctx.fillStyle = "#eeeccf"; ctx.fillRect(a.x, a.y, 1, 1); @@ -2204,6 +2469,8 @@ function renderTooltip() { ${agent ? `Climate mismatch${mismatch.toFixed(2)}` : ""} ${agent ? `Sedentary${getSedentary(agent.traits).toFixed(2)}` : ""} ${agent ? `Ethnocentrism${getEthnocentrism(agent.traits).toFixed(2)}` : ""} + ${agent ? `Farming${(agent.tech?.farming || 0).toFixed(3)} / work ${(agent.farmingWork || 0).toFixed(2)}` : ""} + ${agent ? `Metallurgy${(agent.tech?.metallurgy || 0).toFixed(3)}` : ""} ${agent ? `Stored${agent.resources.toFixed(1)}` : ""} ${ethnicity ? `Lineage pop${ethnicity.population}` : ""} `; @@ -2250,14 +2517,27 @@ function updateStats(force = false) { lastStatsAt = now; const livingEthnicities = [...sim.ethnicities.values()].filter(e => e.population > 0); const urbanPopulation = sim.cities.reduce((sum, c) => sum + c.population, 0); + let farmingTotal = 0; + let metallurgyTotal = 0; + let farmingHolders = 0; + let metallurgyHolders = 0; + for (const a of sim.agents) { + sim.ensureAgentTech(a); + farmingTotal += a.tech.farming || 0; + metallurgyTotal += a.tech.metallurgy || 0; + if ((a.tech.farming || 0) > 0.02) farmingHolders++; + if ((a.tech.metallurgy || 0) > 0.02) metallurgyHolders++; + } + const agentCount = Math.max(1, sim.agents.length); els.year.textContent = formatSimDate(sim.year); - els.population.textContent = Math.floor(sim.agents.length + urbanPopulation).toLocaleString(); els.activeGroups.textContent = sim.agents.length.toLocaleString(); els.urbanPopulation.textContent = Math.floor(urbanPopulation).toLocaleString(); els.ethnicities.textContent = livingEthnicities.length.toLocaleString(); els.cities.textContent = sim.cities.length.toLocaleString(); els.polities.textContent = sim.polities.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})`; els.deaths.textContent = sim.deaths.toLocaleString(); const top = livingEthnicities.sort((a, b) => b.population - a.population).slice(0, 9); @@ -2282,6 +2562,8 @@ function setLegend() { 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") { + els.legend.innerHTML = "Farming knowledgeMetallurgy knowledge"; } else { els.legend.innerHTML = "Regenerating local resource stock"; }