booa
This commit is contained in:
parent
79a0c29f84
commit
e2d2dfcb66
7 changed files with 2610 additions and 1271 deletions
13
app-state.js
13
app-state.js
|
|
@ -41,10 +41,11 @@ scale: null
|
|||
};
|
||||
const legendByMode = {
|
||||
terrain: () => terrainInfo.map(t => `<span><i style="background: rgb(${t.color.join(",")})"></i>${t.name}</span>`).join(""),
|
||||
ethnicity: () => "<span>Color = tile-level dominant ethnicity. Stronger color marks higher local population; mixed areas are muted.</span>",
|
||||
pressure: () => "<span><i style=\"background:#d24943\"></i>High local population pressure</span>",
|
||||
polities: () => "<span>Color = territorial state control. Brighter areas are stronger control; pale borders mark frontiers; contested areas are unstable.</span>",
|
||||
technology: () => "<span><i style=\"background:#5fab5b\"></i>Farming knowledge</span><span><i style=\"background:#caa966\"></i>Metallurgy knowledge</span>",
|
||||
pheromone: () => "<span><i style=\"background:#d8b156\"></i>Pheromone strength</span><span><i style=\"background:#ffd75a\"></i>Formal trade routes</span>",
|
||||
resources: () => "<span><i style=\"background:#6bbc55\"></i>Regenerating local resource stock</span>"
|
||||
ethnicity: () => "<span>Ethnicity</span>",
|
||||
pressure: () => "<span>Pressure</span>",
|
||||
polities: () => "<span>States</span>",
|
||||
nomads: () => "<span>Nomads</span>",
|
||||
technology: () => "<span>Technology</span>",
|
||||
pheromone: () => "<span>Routes</span>",
|
||||
resources: () => "<span>Resources</span>"
|
||||
};
|
||||
|
|
|
|||
4
boot.js
4
boot.js
|
|
@ -45,12 +45,12 @@ scheduleLoop();
|
|||
}
|
||||
function reset() {
|
||||
const size = Number(els.worldSize.value);
|
||||
const count = Number(els.agentCount.value);
|
||||
const initialPopulation = Number(els.agentCount.value);
|
||||
els.canvas.width = size;
|
||||
els.canvas.height = size;
|
||||
renderImage = null;
|
||||
renderImageSize = 0;
|
||||
sim = new Simulation(size, count);
|
||||
sim = new Simulation(size, initialPopulation);
|
||||
setLegend();
|
||||
drawFrame(true);
|
||||
renderStateGraph();
|
||||
|
|
|
|||
69
config.js
69
config.js
|
|
@ -17,13 +17,8 @@ const MONTHS_PER_YEAR = 12;
|
|||
const WEEKS_PER_YEAR = MONTHS_PER_YEAR * WEEKS_PER_MONTH;
|
||||
const SimConfig = Object.freeze({
|
||||
population: Object.freeze({
|
||||
maxAgentsFloor: 8000,
|
||||
maxAgentsScale: 1.15,
|
||||
softCapStart: 0.85,
|
||||
hardCapScale: 1.35,
|
||||
offspringCrowdingPenalty: 1.8,
|
||||
minOffspringAcceptance: 0.035,
|
||||
maxOffspringPerStep: 360,
|
||||
carryingCapacityBase: 2.4,
|
||||
carryingCapacityFertility: 7.8,
|
||||
carryingCapacityMineral: 1.8,
|
||||
|
|
@ -75,6 +70,40 @@ maxDurationYears: 25,
|
|||
claimRadius: 8,
|
||||
maxTargetDistance: 48
|
||||
}),
|
||||
nomad: Object.freeze({
|
||||
enabled: true,
|
||||
maxBands: 24,
|
||||
enabledAfterYears: 120,
|
||||
spawnCheckIntervalYears: 25,
|
||||
baseSpawnChance: 0.28,
|
||||
minPopulation: 80,
|
||||
maxPopulation: 420,
|
||||
minRadius: 5,
|
||||
maxRadius: 14,
|
||||
movementIntervalYears: 2,
|
||||
maxMoveDistance: 9,
|
||||
grazingGain: 0.035,
|
||||
overgrazingDamage: 0.018,
|
||||
resourceConsumption: 0.012,
|
||||
herdSoftCapPerPop: 2.4,
|
||||
herdHardCapPerPop: 4.0,
|
||||
resourceSoftCapPerPop: 1.2,
|
||||
resourceHardCapPerPop: 2.5,
|
||||
grazingDiminishingPower: 1.35,
|
||||
urbanAbsorptionControl: 0.55,
|
||||
cityInteractionRadius: 8,
|
||||
tradeChance: 0.22,
|
||||
raidChance: 0.14,
|
||||
invasionChance: 0.025,
|
||||
confederationPopulation: 900,
|
||||
confederationPrestige: 1.8,
|
||||
splitPopulation: 1200,
|
||||
sedentarizationChance: 0.035,
|
||||
collapseCohesion: 0.12,
|
||||
influenceCulturalTrace: 0.006,
|
||||
influenceMobilePopulationTrace: 0.004,
|
||||
visualDurationYears: 20
|
||||
}),
|
||||
polityAccess: Object.freeze({
|
||||
enabled: true,
|
||||
maxSearchDepth: 8,
|
||||
|
|
@ -92,6 +121,13 @@ minimumInfluence: 0.18
|
|||
}),
|
||||
route: Object.freeze({
|
||||
maxRouteLength: 42,
|
||||
adaptiveMaxRouteLength: 72,
|
||||
isolatedCityBootstrap: true,
|
||||
isolatedRouteSearchLimit: 8,
|
||||
isolatedRouteRadius: 4,
|
||||
newRoutePheromoneSeed: 2.5,
|
||||
newRouteGraceBoost: 18,
|
||||
isolatedRouteMinStrength: 0.12,
|
||||
pheromoneDecay: 0.996,
|
||||
pheromoneDiffusion: 0.045,
|
||||
maxPheromone: 18,
|
||||
|
|
@ -99,9 +135,20 @@ routePheromoneBuild: 0.18,
|
|||
routePheromoneMaintain: 0.12,
|
||||
routeUpkeepPerTile: 0.018,
|
||||
routeWeakThreshold: 8,
|
||||
routeUnsupportedDecay: 7,
|
||||
routeUnsupportedDecay: 3,
|
||||
routeMaintainedBoost: 10
|
||||
}),
|
||||
war: Object.freeze({
|
||||
pressureMemoryDecay: 0.88,
|
||||
cityDamageMinRate: 0.003,
|
||||
cityDamageMaxRate: 0.08,
|
||||
captureLossBase: 0.08,
|
||||
captureLossMax: 0.32,
|
||||
exhaustionPopulationLossBase: 0.0007,
|
||||
exhaustionPopulationLossMax: 0.012,
|
||||
warZoneTileLossRate: 0.006,
|
||||
majorSettlementThreshold: 1.25
|
||||
}),
|
||||
disaster: Object.freeze({
|
||||
damageScale: 1.5,
|
||||
majorLossRate: 0.05,
|
||||
|
|
@ -122,13 +169,9 @@ frontierWave: Object.freeze({
|
|||
enabledAfterYears: 300,
|
||||
intervalYears: 80,
|
||||
chance: 0.45,
|
||||
minAgents: 60,
|
||||
maxAgents: 140,
|
||||
maxPopulationRatio: 1.28,
|
||||
edgeBand: 4,
|
||||
mutation: 0.08,
|
||||
minResources: 12,
|
||||
maxResources: 28
|
||||
minPopulation: 60,
|
||||
maxPopulation: 140,
|
||||
edgeBand: 4
|
||||
})
|
||||
});
|
||||
function years(value) {
|
||||
|
|
|
|||
|
|
@ -26,7 +26,7 @@
|
|||
</label>
|
||||
|
||||
<label>
|
||||
Initial groups
|
||||
Initial population
|
||||
<input id="agentCount" type="range" min="500" max="20000" step="500" value="5000">
|
||||
</label>
|
||||
|
||||
|
|
@ -57,7 +57,7 @@
|
|||
<section class="panel stats" aria-live="polite">
|
||||
<dl>
|
||||
<div><dt>Date</dt><dd id="year">0y 1m</dd></div>
|
||||
<div><dt>Active groups</dt><dd id="activeGroups">0</dd></div>
|
||||
<div><dt>Field population</dt><dd id="activeGroups">0</dd></div>
|
||||
<div><dt>Urban population</dt><dd id="urbanPopulation">0</dd></div>
|
||||
<div><dt>Ethnicities</dt><dd id="ethnicities">0</dd></div>
|
||||
<div><dt>Cities</dt><dd id="cities">0</dd></div>
|
||||
|
|
@ -66,7 +66,7 @@
|
|||
<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>
|
||||
<div><dt>Collapse deaths</dt><dd id="deaths">0</dd></div>
|
||||
<div><dt>Deaths</dt><dd id="deaths">0</dd></div>
|
||||
<div><dt>Frame cost</dt><dd id="frameCost">0ms</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
|
|
|
|||
200
render.js
200
render.js
|
|
@ -79,7 +79,13 @@ if (graphState.hoverPolityId !== null && !polityColor.isGraphHover) color = mix(
|
|||
}
|
||||
}
|
||||
} else if (mode === "technology") {
|
||||
color = mix(terrainInfo[w.terrain[i]].color, [44, 42, 48], 0.35);
|
||||
const farming = w.farmingKnowledge?.[i] || 0;
|
||||
const metallurgy = w.metallurgyKnowledge?.[i] || 0;
|
||||
const tech = clamp(Math.max(farming, metallurgy), 0, 1);
|
||||
const techColor = mix([95, 171, 91], [202, 169, 102], metallurgy / Math.max(0.001, farming + metallurgy));
|
||||
color = tech > 0.01
|
||||
? mix(terrainInfo[w.terrain[i]].color, techColor, clamp(0.12 + tech * 0.62, 0.12, 0.74))
|
||||
: mix(terrainInfo[w.terrain[i]].color, [44, 42, 48], 0.35);
|
||||
} else if (mode === "pheromone") {
|
||||
const v = clamp(w.pheromone[i] / SimConfig.route.maxPheromone, 0, 1);
|
||||
color = mix(mix(terrainInfo[w.terrain[i]].color, [18, 20, 18], 0.58), [216, 177, 86], v);
|
||||
|
|
@ -95,8 +101,9 @@ data[p + 3] = 255;
|
|||
ctx.putImageData(image, 0, 0);
|
||||
renderDisasters();
|
||||
drawTradeLinks();
|
||||
drawNomadBands();
|
||||
drawCampaigns();
|
||||
drawAgentsAndCities(mode);
|
||||
drawPopulationAndCities(mode);
|
||||
drawGraphPolityHighlight();
|
||||
maybeRenderStateGraph();
|
||||
els.frameCost.textContent = `${Math.round(performance.now() - start)}ms`;
|
||||
|
|
@ -205,7 +212,39 @@ fillSquare(ctx, x, y, 1);
|
|||
}
|
||||
ctx.restore();
|
||||
}
|
||||
function drawAgentsAndCities(mode) {
|
||||
function drawNomadBands() {
|
||||
if (!sim?.nomadBands?.length) return;
|
||||
const w = sim.world;
|
||||
ctx.save();
|
||||
for (const band of sim.nomadBands) {
|
||||
const ethnicityColor = sim.ethnicities.get(band.ethnicityId)?.color || [216, 177, 86];
|
||||
ctx.globalAlpha = 0.07;
|
||||
ctx.fillStyle = rgba(ethnicityColor, 0.45);
|
||||
for (const tile of band.influenceTiles || []) {
|
||||
ctx.fillRect(tile % w.size, Math.floor(tile / w.size), 1, 1);
|
||||
}
|
||||
ctx.globalAlpha = 0.34;
|
||||
ctx.strokeStyle = band.mode === "raiding" || band.mode === "invading"
|
||||
? "rgba(232, 101, 76, 0.95)"
|
||||
: band.mode === "settling"
|
||||
? "rgba(202, 169, 102, 0.9)"
|
||||
: rgba(ethnicityColor, 0.9, 18);
|
||||
ctx.lineWidth = 1;
|
||||
strokeCircle(ctx, band.x + 0.5, band.y + 0.5, Math.max(2.5, band.radius * 0.45));
|
||||
if (band.mode === "confederating") {
|
||||
ctx.globalAlpha = 0.26;
|
||||
strokeCircle(ctx, band.x + 0.5, band.y + 0.5, Math.max(4, band.radius * 0.68));
|
||||
}
|
||||
ctx.globalAlpha = 0.22;
|
||||
ctx.fillStyle = rgba(ethnicityColor, 0.9, 22);
|
||||
for (const tile of band.trailTiles || []) ctx.fillRect(tile % w.size, Math.floor(tile / w.size), 1, 1);
|
||||
ctx.globalAlpha = 0.82;
|
||||
ctx.fillStyle = rgba(ethnicityColor, 0.95, 36);
|
||||
fillSquare(ctx, Math.round(band.x), Math.round(band.y), 1);
|
||||
}
|
||||
ctx.restore();
|
||||
}
|
||||
function drawPopulationAndCities(mode) {
|
||||
ctx.save();
|
||||
if (mode !== "ethnicity") {
|
||||
for (const city of sim.cities) {
|
||||
|
|
@ -218,23 +257,18 @@ fillSquare(ctx, city.x, city.y, radius);
|
|||
}
|
||||
ctx.globalAlpha = 1;
|
||||
if (mode === "technology") {
|
||||
for (const a of sim.agents) {
|
||||
sim.ensureAgentTech(a);
|
||||
const farming = techLevel(a, "farming");
|
||||
const metallurgy = techLevel(a, "metallurgy");
|
||||
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, clamp(0.32 + tech * 0.68, 0.32, 1));
|
||||
ctx.fillRect(a.x, a.y, 1, 1);
|
||||
}
|
||||
// Tile-level technology is rendered in the base pass.
|
||||
} else if (mode === "ethnicity") {
|
||||
// Phase 2: ethnicity view is driven by tile-level population/culture fields.
|
||||
// Individual agents remain in the simulation but no longer define this layer.
|
||||
// Ethnicity view is driven by tile-level population/culture fields.
|
||||
} else {
|
||||
ctx.fillStyle = "#eeeccf";
|
||||
for (const tile of sim.tileAgents.keys()) {
|
||||
ctx.fillRect(tile % sim.world.size, Math.floor(tile / sim.world.size), 1, 1);
|
||||
const w = sim.world;
|
||||
ctx.fillStyle = "rgba(238, 236, 207, 0.16)";
|
||||
for (let tile = 0; tile < w.count; tile++) {
|
||||
const pop = w.population?.[tile] || 0;
|
||||
if (pop < 6) continue;
|
||||
const alpha = clamp(Math.log1p(pop) / Math.log(160), 0.04, 0.18);
|
||||
ctx.fillStyle = `rgba(238, 236, 207, ${alpha})`;
|
||||
ctx.fillRect(tile % w.size, Math.floor(tile / w.size), 1, 1);
|
||||
}
|
||||
}
|
||||
for (const city of sim.cities) {
|
||||
|
|
@ -329,23 +363,23 @@ return;
|
|||
}
|
||||
const w = sim.world;
|
||||
const i = w.idx(x, y);
|
||||
const agent = findAgentAt(x, y);
|
||||
const city = w.city[i] >= 0 ? sim.getCityById(w.city[i]) : null;
|
||||
const terrain = terrainInfo[w.terrain[i]];
|
||||
const ethnicity = agent ? sim.ethnicities.get(agent.ethnicity) : null;
|
||||
const waterInfluence = waterInfluenceAt(w, x, y).toFixed(2);
|
||||
const cityEthnicity = city ? dominantComposition(city.ethnicityComposition) : null;
|
||||
const mismatch = agent ? sim.climateMismatch(agent.ethnicity, i) : 0;
|
||||
const polity = cityPolity(city);
|
||||
const territoryId = w.polity?.[i] ?? -1;
|
||||
const territoryPolity = territoryId >= 0 ? sim.getPolityById(territoryId) : null;
|
||||
const ethnicMix = sim.tileEthnicMix?.get(i);
|
||||
const tileDominantEthnicity = ethnicMix?.size ? dominantComposition(ethnicMix) : w.dominantEthnicity[i];
|
||||
const regionalEthnicity = tileDominantEthnicity != null && tileDominantEthnicity >= 0 ? `E${tileDominantEthnicity}` : "-";
|
||||
const regionalLineage = tileDominantEthnicity != null && tileDominantEthnicity >= 0 ? sim.ethnicities.get(tileDominantEthnicity) : null;
|
||||
const mismatch = tileDominantEthnicity != null && tileDominantEthnicity >= 0 ? sim.climateMismatch(tileDominantEthnicity, i) : 0;
|
||||
const tilePopulation = w.population?.[i] || 0;
|
||||
const nearbyCampaigns = activeCampaignsNear(i);
|
||||
const nearbyNomad = activeNomadNear(i);
|
||||
els.tooltip.innerHTML = `
|
||||
<strong>${city ? `City #${city.id}` : agent ? "Agent group" : terrain.name}</strong>
|
||||
<strong>${city ? `City #${city.id}` : terrain.name}</strong>
|
||||
${tooltipSection("Tile", [
|
||||
tooltipRow("Position", `${x}, ${y}`),
|
||||
tooltipRow("Terrain", terrain.name),
|
||||
|
|
@ -367,6 +401,16 @@ els.tooltip.innerHTML = `
|
|||
tooltipRow("Dominant ethnicity", regionalEthnicity),
|
||||
tooltipRow("Tile population", tilePopulation >= 10 ? Math.round(tilePopulation).toLocaleString() : tilePopulation.toFixed(1))
|
||||
])}
|
||||
${tooltipSection("Nomads", [
|
||||
tooltipRow("Band", nearbyNomad ? `${nearbyNomad.name} #${nearbyNomad.id}` : "", nearbyNomad),
|
||||
tooltipRow("Mode", nearbyNomad?.mode, nearbyNomad),
|
||||
tooltipRow("Population", nearbyNomad ? Math.round(nearbyNomad.population).toLocaleString() : "", nearbyNomad),
|
||||
tooltipRow("Ethnicity", nearbyNomad ? `E${nearbyNomad.ethnicityId}` : "", nearbyNomad),
|
||||
tooltipRow("Cohesion", nearbyNomad?.cohesion.toFixed(2), nearbyNomad),
|
||||
tooltipRow("Prestige", nearbyNomad?.prestige.toFixed(2), nearbyNomad),
|
||||
tooltipRow("Resources", nearbyNomad?.resources.toFixed(1), nearbyNomad),
|
||||
tooltipRow("Herds", nearbyNomad?.herds.toFixed(1), nearbyNomad)
|
||||
])}
|
||||
${tooltipSection("City & State", [
|
||||
tooltipRow("Population", city?.population.toLocaleString(), city),
|
||||
tooltipRow("Food stock", city?.storedResources.toFixed(1), city),
|
||||
|
|
@ -382,17 +426,15 @@ els.tooltip.innerHTML = `
|
|||
tooltipRow("Treasury", polity?.treasury.toFixed(1), polity),
|
||||
tooltipRow("Capital", polity && city ? polity.centerCityId === city.id ? "yes" : "no" : "", polity)
|
||||
])}
|
||||
${tooltipSection("Culture & Agent", [
|
||||
${tooltipSection("Culture", [
|
||||
tooltipRow("Local culture", `${regionalEthnicity} / ${w.cultureDiversity[i].toFixed(2)}`),
|
||||
tooltipRow("Agent ethnicity", `E${agent?.ethnicity}`, agent),
|
||||
tooltipRow("Lineage pop", ethnicity?.population, ethnicity),
|
||||
tooltipRow("Climate pref", ethnicity ? `${ethnicity.climateTemp.toFixed(2)} / ${ethnicity.climateHumidity.toFixed(2)}` : "", ethnicity),
|
||||
tooltipRow("Climate mismatch", mismatch.toFixed(2), agent),
|
||||
tooltipRow("Sedentary", agent ? agent.traits.sedentary.toFixed(2) : "", agent),
|
||||
tooltipRow("Ethnocentrism", agent ? agent.traits.ethnocentrism.toFixed(2) : "", agent),
|
||||
tooltipRow("Farming", agent ? `${techLevel(agent, "farming").toFixed(3)} / work ${(agent.farmingWork || 0).toFixed(2)}` : "", agent),
|
||||
tooltipRow("Metallurgy", techLevel(agent, "metallurgy").toFixed(3), agent),
|
||||
tooltipRow("Stored", agent?.resources.toFixed(1), agent)
|
||||
tooltipRow("Lineage pop", regionalLineage?.population?.toLocaleString(), regionalLineage),
|
||||
tooltipRow("Climate pref", regionalLineage ? `${regionalLineage.climateTemp.toFixed(2)} / ${regionalLineage.climateHumidity.toFixed(2)}` : "", regionalLineage),
|
||||
tooltipRow("Climate mismatch", mismatch.toFixed(2), regionalLineage),
|
||||
tooltipRow("Settled pop", (w.settledPopulation?.[i] || 0).toFixed(1), tilePopulation > 0),
|
||||
tooltipRow("Mobile pop", (w.mobilePopulation?.[i] || 0).toFixed(1), tilePopulation > 0),
|
||||
tooltipRow("Farming", (w.farmingKnowledge?.[i] || 0).toFixed(3)),
|
||||
tooltipRow("Metallurgy", (w.metallurgyKnowledge?.[i] || 0).toFixed(3))
|
||||
])}
|
||||
`;
|
||||
els.tooltip.hidden = false;
|
||||
|
|
@ -459,9 +501,23 @@ labels.push(`${campaign.type.replaceAll("_", " ")} ${Math.round(clamp(campaign.p
|
|||
}
|
||||
return labels.slice(0, 2).join("; ");
|
||||
}
|
||||
function findAgentAt(x, y) {
|
||||
const directIndex = sim.world.idx(x, y);
|
||||
return sim.tileAgents.get(directIndex)?.[0] || null;
|
||||
function activeNomadNear(tile) {
|
||||
if (!sim?.nomadBands?.length) return null;
|
||||
const w = sim.world;
|
||||
const x = tile % w.size;
|
||||
const y = Math.floor(tile / w.size);
|
||||
let best = null;
|
||||
let bestDistance = Infinity;
|
||||
for (const band of sim.nomadBands) {
|
||||
const distance = Math.abs(Math.round(band.x) - x) + Math.abs(Math.round(band.y) - y);
|
||||
const inInfluence = w.nomadBand?.[tile] === band.id;
|
||||
if (!inInfluence && distance > Math.max(3, Math.floor(band.radius * 0.55))) continue;
|
||||
if (distance < bestDistance) {
|
||||
best = band;
|
||||
bestDistance = distance;
|
||||
}
|
||||
}
|
||||
return best;
|
||||
}
|
||||
function waterInfluenceAt(world, x, y) {
|
||||
let score = 0;
|
||||
|
|
@ -487,25 +543,29 @@ let farmingTotal = 0;
|
|||
let metallurgyTotal = 0;
|
||||
let farmingHolders = 0;
|
||||
let metallurgyHolders = 0;
|
||||
for (const a of sim.agents) {
|
||||
sim.ensureAgentTech(a);
|
||||
farmingTotal += techLevel(a, "farming");
|
||||
metallurgyTotal += techLevel(a, "metallurgy");
|
||||
if (techLevel(a, "farming") > 0.02) farmingHolders++;
|
||||
if (techLevel(a, "metallurgy") > 0.02) metallurgyHolders++;
|
||||
for (let i = 0; i < sim.world.count; i++) {
|
||||
const pop = sim.world.population?.[i] || 0;
|
||||
if (pop <= 0) continue;
|
||||
farmingTotal += (sim.world.farmingKnowledge?.[i] || 0) * pop;
|
||||
metallurgyTotal += (sim.world.metallurgyKnowledge?.[i] || 0) * pop;
|
||||
if ((sim.world.farmingKnowledge?.[i] || 0) > 0.02) farmingHolders++;
|
||||
if ((sim.world.metallurgyKnowledge?.[i] || 0) > 0.02) metallurgyHolders++;
|
||||
}
|
||||
const agentCount = Math.max(1, sim.agents.length);
|
||||
const fieldPopulation = sim.world.population
|
||||
? Array.from(sim.world.population).reduce((sum, value) => sum + value, 0)
|
||||
: 0;
|
||||
const knowledgeDenominator = Math.max(1, fieldPopulation);
|
||||
const stats = {
|
||||
year: formatSimDate(sim.year),
|
||||
activeGroups: sim.agents.length.toLocaleString(),
|
||||
activeGroups: Math.round(fieldPopulation).toLocaleString(),
|
||||
urbanPopulation: Math.floor(urbanPopulation).toLocaleString(),
|
||||
ethnicities: livingEthnicities.length.toLocaleString(),
|
||||
cities: sim.cities.length.toLocaleString(),
|
||||
polities: sim.polities.length.toLocaleString(),
|
||||
wars: sim.wars.length.toLocaleString(),
|
||||
routes: sim.tradeLinks.length.toLocaleString(),
|
||||
farmingKnowledge: `${(farmingTotal / agentCount).toFixed(4)} (${farmingHolders})`,
|
||||
metallurgyKnowledge: `${(metallurgyTotal / agentCount).toFixed(4)} (${metallurgyHolders})`,
|
||||
farmingKnowledge: `${(farmingTotal / knowledgeDenominator).toFixed(4)} (${farmingHolders})`,
|
||||
metallurgyKnowledge: `${(metallurgyTotal / knowledgeDenominator).toFixed(4)} (${metallurgyHolders})`,
|
||||
deaths: sim.deaths.toLocaleString()
|
||||
};
|
||||
for (const [id, value] of Object.entries(stats)) {
|
||||
|
|
@ -880,7 +940,7 @@ newEthnicity: {
|
|||
label: "new ethnicity",
|
||||
color: "rgba(105, 181, 120, 0.95)",
|
||||
draw: drawGraphDiamond,
|
||||
summary: data => `E${data.ethnicity ?? "-"}, ${Math.round(data.population || 0).toLocaleString()} frontier groups`
|
||||
summary: data => `E${data.ethnicity ?? "-"}, ${Math.round(data.population || 0).toLocaleString()} frontier population`
|
||||
},
|
||||
campaign: {
|
||||
label: "campaign",
|
||||
|
|
@ -924,6 +984,54 @@ color: "rgba(236, 126, 111, 0.95)",
|
|||
draw: drawGraphTriangleUp,
|
||||
summary: data => `${data.outcome || "war"} vs S${data.targetPolityId ?? "-"}`
|
||||
},
|
||||
nomadBand: {
|
||||
label: "nomad band",
|
||||
color: "rgba(210, 184, 104, 0.95)",
|
||||
draw: drawGraphDiamond,
|
||||
summary: data => `band #${data.bandId ?? "-"}, E${data.ethnicity ?? "-"}, pop ${Math.round(data.population || 0).toLocaleString()}`
|
||||
},
|
||||
nomadTrade: {
|
||||
label: "nomad trade",
|
||||
color: "rgba(216, 177, 86, 0.95)",
|
||||
draw: drawGraphCircle,
|
||||
summary: data => `band #${data.bandId ?? "-"} with city #${data.cityId ?? "-"}`
|
||||
},
|
||||
nomadRaid: {
|
||||
label: "nomad raid",
|
||||
color: "rgba(232, 101, 76, 0.95)",
|
||||
draw: drawGraphTriangleDown,
|
||||
summary: data => `band #${data.bandId ?? "-"} raided city #${data.cityId ?? "-"}`
|
||||
},
|
||||
nomadConquest: {
|
||||
label: "nomad conquest",
|
||||
color: "rgba(202, 87, 78, 0.95)",
|
||||
draw: drawGraphTriangleUp,
|
||||
summary: data => `band #${data.bandId ?? "-"}, city #${data.cityId ?? "-"}, ${data.outcome || "conquest"}`
|
||||
},
|
||||
nomadConfederation: {
|
||||
label: "nomad confederation",
|
||||
color: "rgba(191, 162, 91, 0.95)",
|
||||
draw: drawGraphDiamond,
|
||||
summary: data => `band #${data.bandId ?? "-"}, pop ${Math.round(data.population || 0).toLocaleString()}`
|
||||
},
|
||||
nomadSplit: {
|
||||
label: "nomad split",
|
||||
color: "rgba(151, 130, 91, 0.95)",
|
||||
draw: drawGraphTriangleDown,
|
||||
summary: data => `band #${data.bandId ?? "-"} split #${data.newBandId ?? "-"}`
|
||||
},
|
||||
nomadMerge: {
|
||||
label: "nomad merge",
|
||||
color: "rgba(170, 145, 91, 0.95)",
|
||||
draw: drawGraphDiamond,
|
||||
summary: data => `band #${data.mergedBandId ?? "-"} joined #${data.bandId ?? "-"}`
|
||||
},
|
||||
sedentarization: {
|
||||
label: "sedentarization",
|
||||
color: "rgba(202, 169, 102, 0.95)",
|
||||
draw: drawGraphCircle,
|
||||
summary: data => `band #${data.bandId ?? "-"} settled ${Math.round(data.population || 0).toLocaleString()} in city #${data.cityId ?? "-"}`
|
||||
},
|
||||
war: {
|
||||
label: "war",
|
||||
color: "rgba(202, 87, 78, 0.95)",
|
||||
|
|
|
|||
4
utils.js
4
utils.js
|
|
@ -43,7 +43,9 @@ function averageTraits(sum, count) {
|
|||
return Object.fromEntries(traitFields.map(([key, , , , , sumScale]) => [key, sum[key] / count / sumScale]));
|
||||
}
|
||||
function compositionTotal(composition) {
|
||||
return [...composition.values()].reduce((sum, value) => sum + value, 0);
|
||||
let sum = 0;
|
||||
for (const value of composition.values()) sum += value;
|
||||
return sum;
|
||||
}
|
||||
function addBirthsToComposition(composition, births, rng = null) {
|
||||
const entries = [...composition.entries()];
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue