booa
This commit is contained in:
parent
79a0c29f84
commit
e2d2dfcb66
7 changed files with 2610 additions and 1271 deletions
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)",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue