This commit is contained in:
33333-33333 2026-05-12 22:19:44 +09:00
commit 8ff52b3fbd
2 changed files with 69 additions and 43 deletions

106
script.js
View file

@ -668,8 +668,8 @@ class Simulation {
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;
w.pheromone[from] += (w.tradeRoute[from] ? 0.14 : 0.42) * depositScale;
w.pheromone[to] += (w.tradeRoute[to] ? 0.10 : 0.30) * depositScale;
a.settled = Math.max(0, a.settled - 1);
} else {
a.movedThisStep = false;
@ -720,6 +720,7 @@ class Simulation {
if (farming <= 0.02) return;
if (agent.settled > 0) {
agent.farmingWork = clamp((agent.farmingWork || 0) + farming * 0.015, 0, 1);
agent.tech.farming = clamp(agent.tech.farming + farming * this.farmabilityAt(tile) * 0.00018, 0, 1);
}
}
@ -729,7 +730,7 @@ class Simulation {
const settledFactor = clamp((agent.settled || 0) / 12, 0, 1);
if ((agent.tech?.farming || 0) <= 0.02) {
const farmingBase = 0.000090;
const farmingBase = 0.00016;
const farmability = this.farmabilityAt(tile);
const sedentary = getSedentary(agent.traits);
const farmingChance =
@ -737,11 +738,11 @@ class Simulation {
(0.25 + farmability) *
(0.4 + sedentary) *
(0.3 + settledFactor);
if (this.rng.next() < farmingChance) this.grantTechnologyAround(agent, "farming", 0.24);
if (this.rng.next() < farmingChance) this.grantTechnologyAround(agent, "farming", 0.32);
}
if ((agent.tech?.metallurgy || 0) <= 0.02) {
const metallurgyBase = 0.000055;
const metallurgyBase = 0.00011;
const mineral = clamp(w.mineral[tile], 0, 1);
const mineralTerrainBonus =
w.terrain[tile] === Terrain.MINERAL || w.terrain[tile] === Terrain.MOUNTAIN
@ -752,7 +753,7 @@ class Simulation {
(0.2 + mineral) *
mineralTerrainBonus *
(0.5 + settledFactor);
if (this.rng.next() < metallurgyChance) this.grantTechnologyAround(agent, "metallurgy", 0.20);
if (this.rng.next() < metallurgyChance) this.grantTechnologyAround(agent, "metallurgy", 0.28);
}
}
@ -773,7 +774,7 @@ class Simulation {
this.ensureAgentTech(agent);
const farming = agent.tech.farming || 0;
const metallurgy = agent.tech.metallurgy || 0;
const cost = farming * 0.010 + metallurgy * 0.022;
const cost = farming * 0.004 + metallurgy * 0.010;
if (cost <= 0) return;
if (agent.resources >= cost) {
@ -783,7 +784,7 @@ class Simulation {
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);
const decay = clamp(0.006 + shortage * 0.025, 0.006, 0.06);
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;
@ -810,7 +811,7 @@ class Simulation {
ethnocentrism * 0.0012;
if (sameEthnicity) chance += 0.004;
if (w.tradeRoute[tile]) chance += 0.003;
if (w.tradeRoute[tile]) chance += 0.006;
if (w.city[tile] >= 0) chance += 0.003;
chance = clamp(chance, 0.0008, 0.028);
@ -850,14 +851,14 @@ class Simulation {
}
}
if (farmingCount >= 3) {
if (farmingCount >= 2) {
const avgFarming = farmingSum / farmingCount;
agent.tech.farming = clamp(agent.tech.farming + 0.0012 * farmingCount * avgFarming, 0, 1);
agent.tech.farming = clamp(agent.tech.farming + 0.0024 * farmingCount * avgFarming, 0, 1);
}
if (metallurgyCount >= 3) {
if (metallurgyCount >= 2) {
const avgMetallurgy = metallurgySum / metallurgyCount;
agent.tech.metallurgy = clamp(agent.tech.metallurgy + 0.00085 * metallurgyCount * avgMetallurgy, 0, 1);
agent.tech.metallurgy = clamp(agent.tech.metallurgy + 0.0018 * metallurgyCount * avgMetallurgy, 0, 1);
}
}
@ -885,6 +886,9 @@ class Simulation {
metallurgyGatherMultiplier(agent, tile) {
const metallurgyLevel = agent.tech?.metallurgy || 0;
if (metallurgyLevel > 0.02) {
agent.tech.metallurgy = clamp(agent.tech.metallurgy + metallurgyLevel * clamp(this.world.mineral[tile], 0, 1) * 0.00012, 0, 1);
}
return 1 + metallurgyLevel * clamp(this.world.mineral[tile], 0, 1);
}
@ -929,7 +933,10 @@ class Simulation {
const childResources = a.resources * childShare;
a.resources -= childResources;
const childTraits = mutateTraits(a.traits, this.rng, 0.035);
offspring.push(this.makeAgent(a.x, a.y, a.ethnicity, childTraits, childResources));
const child = this.makeAgent(a.x, a.y, a.ethnicity, childTraits, childResources);
child.tech.farming = clamp((a.tech?.farming || 0) * this.rng.range(0.75, 0.95), 0, 1);
child.tech.metallurgy = clamp((a.tech?.metallurgy || 0) * this.rng.range(0.70, 0.92), 0, 1);
offspring.push(child);
}
}
@ -1029,7 +1036,7 @@ class Simulation {
const w = this.world;
for (let i = 0; i < w.count; i++) {
w.resource[i] = Math.min(58, w.resource[i] + w.regen[i] * (1 + w.farmland[i] * 1.15));
w.pheromone[i] *= 0.992;
w.pheromone[i] *= 0.996;
w.pressure[i] *= 0.88;
}
}
@ -1061,20 +1068,20 @@ class Simulation {
}
let foundedThisTick = 0;
const canFoundCities = this.year >= years(180) && this.year % years(30) === 0;
const foundingLimit = canFoundCities ? 1 + Math.floor(this.year / years(1200)) : 0;
const canFoundCities = this.year >= years(80) && this.year % years(10) === 0;
const foundingLimit = canFoundCities ? 1 + Math.floor(this.year / years(600)) : 0;
const candidateEntries = [...candidates].sort((a, b) => {
const sedentaryA = a[1].sedentary / Math.max(1, a[1].count);
const sedentaryB = b[1].sedentary / Math.max(1, b[1].count);
return (b[1].count * (0.7 + sedentaryB) + b[1].resources * 0.02) - (a[1].count * (0.7 + sedentaryA) + a[1].resources * 0.02);
});
for (const [i, group] of candidateEntries) {
if (group.count < 6) continue;
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 < 50) {
const foundingChance = clamp((avgSedentary - 0.28) * 1.55 * 3, 0, 0.98);
if (avgSedentary < 0.34 || this.rng.next() > foundingChance) continue;
if (!city && canFoundCities && foundedThisTick < foundingLimit && this.cities.length < 80) {
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);
this.cities.push(city);
foundedThisTick++;
@ -1097,9 +1104,14 @@ class Simulation {
this.cities = this.cities.filter(c => {
c.age++;
c.strength *= 0.996;
if (c.population < 10) c.strength -= 0.02;
if (c.population <= 0 || c.strength <= 0.12) return false;
c.strength *= 0.992;
const foodPerCapita = c.storedResources / Math.max(1, c.population);
if (c.age > 20) {
if (c.activeVisitors < 2 && foodPerCapita < 0.04) c.strength -= 0.035;
else if (c.activeVisitors < 5 && foodPerCapita < 0.02) c.strength -= 0.015;
if (c.population < 25 && foodPerCapita < 0.05) c.strength -= 0.018;
}
if (c.population <= 0 || c.strength <= 0.06) return false;
const radius = c.agriculturalRadius;
for (let dy = -radius; dy <= radius; dy++) {
for (let dx = -radius; dx <= radius; dx++) {
@ -1121,7 +1133,7 @@ class Simulation {
}
createCity(x, y, seedGroup = null) {
const seedPopulation = seedGroup ? Math.max(8, seedGroup.count * 3) : 8;
const seedPopulation = seedGroup ? Math.max(14, seedGroup.count * 5) : 10;
const seedSedentary = seedGroup ? seedGroup.sedentary / Math.max(1, seedGroup.count) : 0.5;
const composition = new Map();
if (seedGroup) {
@ -1136,14 +1148,14 @@ class Simulation {
x,
y,
population: seedPopulation,
storedResources: 24 + (seedGroup?.resources || 0) * 0.4,
storedResources: 34 + (seedGroup?.resources || 0) * 0.55,
ethnicityComposition: composition,
pheromoneOutput: 0,
agriculturalRadius: 2,
tradeLinks: new Set(),
activeVisitors: 0,
age: 0,
strength: 2,
strength: 3,
sedentaryCulture: seedSedentary,
polityId: null,
loyalty: 0.5,
@ -1202,13 +1214,14 @@ class Simulation {
const extraction = Math.min(w.resource[i], (0.07 + w.fertility[i] * 0.18 + w.mineral[i] * 0.045) * pull);
w.resource[i] -= extraction;
w.farmland[i] = Math.max(w.farmland[i], pull);
w.pheromone[i] += city.pheromoneOutput * pull * 0.04;
w.pheromone[i] += city.pheromoneOutput * pull * 0.09;
harvested += extraction;
}
}
city.storedResources += harvested;
const upkeep = city.population * 0.010;
const supportRatio = city.activeVisitors / Math.max(1, city.population);
const upkeep = city.population * (0.010 + Math.max(0, 0.025 - supportRatio) * 0.09);
city.storedResources -= upkeep;
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);
@ -1217,6 +1230,12 @@ class Simulation {
city.storedResources -= births * 0.55;
addBirthsToComposition(city.ethnicityComposition, births);
}
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;
removeFromComposition(city.ethnicityComposition, attrition);
city.strength -= city.activeVisitors === 0 ? 0.030 : 0.012;
}
if (city.storedResources < 0) {
const deficit = Math.abs(city.storedResources);
const dominant = dominantComposition(city.ethnicityComposition);
@ -1797,7 +1816,7 @@ class Simulation {
reinforcePolityTradeRoutes() {
const w = this.world;
for (const polity of this.polities) {
if (this.rng.next() > 0.32) continue;
if (this.rng.next() > 0.48) continue;
const center = this.getCityById(polity.centerCityId);
if (!center) continue;
const candidates = this.getPolityCities(polity)
@ -1844,24 +1863,24 @@ class Simulation {
const c1 = this.cities[a];
const c2 = this.cities[b];
const d = Math.abs(c1.x - c2.x) + Math.abs(c1.y - c2.y);
if (d > 34) continue;
if (d > 44) continue;
const path = this.findTerrainRoute(c1.x, c1.y, c2.x, c2.y);
if (!this.isValidRoutePath(path, c2.x, c2.y)) continue;
const strength = this.routeStrengthForPath(path);
if (strength < 0.12) continue;
if (strength < 0.08) continue;
if (!this.canPayRouteConstructionCost(c1, c2, path)) continue;
candidates.push({ c1, c2, strength, path });
}
}
candidates.sort((a, b) => b.strength - a.strength);
const maxLinks = Math.max(1, Math.floor(this.cities.length / 3));
const maxLinks = Math.max(1, Math.floor(this.cities.length / 2));
const supportedRoutes = new Set();
for (const candidate of candidates) {
if (this.tradeLinks.length >= maxLinks) break;
const { c1, c2, strength, path } = candidate;
const c1Limit = c1.population > 90 ? 2 : 1;
const c2Limit = c2.population > 90 ? 2 : 1;
const c1Limit = c1.population > 90 ? 3 : 2;
const c2Limit = c2.population > 90 ? 3 : 2;
if (c1.tradeLinks.size >= c1Limit || c2.tradeLinks.size >= c2Limit) continue;
if (!this.payRouteConstructionCost(c1, c2, path)) continue;
c1.tradeLinks.add(c2.id);
@ -1870,12 +1889,12 @@ class Simulation {
this.exchangeCityResources(c1, c2, strength, path);
for (const tile of path) {
supportedRoutes.add(tile);
w.tradeRoute[tile] = Math.min(255, w.tradeRoute[tile] + 12);
w.tradeRoute[tile] = Math.min(255, w.tradeRoute[tile] + 18);
}
}
for (let i = 0; i < w.count; i++) {
if (w.tradeRoute[i] && !supportedRoutes.has(i)) w.tradeRoute[i] = Math.max(0, w.tradeRoute[i] - 10);
if (w.tradeRoute[i] && !supportedRoutes.has(i)) w.tradeRoute[i] = Math.max(0, w.tradeRoute[i] - 4);
}
}
@ -2475,10 +2494,15 @@ function renderTooltip() {
${ethnicity ? `<span><b>Lineage pop</b><em>${ethnicity.population}</em></span>` : ""}
`;
els.tooltip.hidden = false;
const left = Math.min(hoverState.left, hoverState.width - 280);
const top = Math.min(hoverState.top, hoverState.height - 230);
els.tooltip.style.left = `${Math.max(8, left)}px`;
els.tooltip.style.top = `${Math.max(8, top)}px`;
const margin = 8;
const width = els.tooltip.offsetWidth;
const height = els.tooltip.offsetHeight;
let left = hoverState.left;
let top = hoverState.top;
if (left + width + margin > hoverState.width) left = hoverState.left - width - 32;
if (top + height + margin > hoverState.height) top = hoverState.height - height - margin;
els.tooltip.style.left = `${clamp(left, margin, Math.max(margin, hoverState.width - width - margin))}px`;
els.tooltip.style.top = `${clamp(top, margin, Math.max(margin, hoverState.height - height - margin))}px`;
}
function hideTooltip() {

View file

@ -284,7 +284,9 @@ canvas#world {
position: absolute;
z-index: 3;
min-width: 180px;
max-width: 260px;
max-width: min(300px, calc(100% - 16px));
max-height: calc(100% - 16px);
overflow: auto;
padding: 8px 10px;
border: 1px solid #435057;
border-radius: 6px;
@ -292,7 +294,7 @@ canvas#world {
color: var(--text);
font-size: 11px;
line-height: 1.35;
pointer-events: none;
pointer-events: auto;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.28);
}