removing agent for a new system

This commit is contained in:
33333-33333 2026-05-17 21:08:03 +09:00
commit 872a70ad8a
6 changed files with 1422 additions and 149 deletions

View file

@ -41,9 +41,9 @@ scale: null
};
const legendByMode = {
terrain: () => terrainInfo.map(t => `<span><i style="background: rgb(${t.color.join(",")})"></i>${t.name}</span>`).join(""),
ethnicity: () => "<span>Agent and city colors show lineage. Land remains terrain-colored.</span>",
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 = city-centered state. Uncolored cities are independent.</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>"

View file

@ -36,9 +36,9 @@ maxCities: 300,
agingStartYears: 180,
agingDecayPerYear: 0.002,
agingOverCapAttrition: 0.035,
agingRestoreBudgetShare: 0.18,
agingRestoreMinWear: 0.08,
agingRestoreCostPerWear: 70,
agingRestoreMinWear: 0.08
agingRestoreBudgetShare: 0.18
}),
render: Object.freeze({
graphThrottleMs: 1400,
@ -55,12 +55,34 @@ polity: Object.freeze({
minimumPerCapitaFood: 0.06,
logisticsDistance: 34,
charismaMin: 0.5,
charismaMax: 3.0,
charismaAverage: 1.0
charismaMax: 1.5,
charismaAverage: 1.0,
leaderTenureMinYears: 22,
leaderTenureMaxYears: 72
}),
campaign: Object.freeze({
checkIntervalYears: 3,
baseChance: 0.18,
minCooldownYears: 12,
maxCooldownYears: 45,
maxActiveCampaigns: 12,
frontierTargetChance: 0.35,
nonStateTargetChance: 0.30,
independentCityTargetChance: 0.20,
polityTargetChance: 0.15,
baseDurationYears: 8,
maxDurationYears: 25,
claimRadius: 8,
maxTargetDistance: 48
}),
polityAccess: Object.freeze({
enabled: true,
noAccessPenalty: 0.17
maxSearchDepth: 8,
indirectPenalty: 0.012,
perHopPenalty: 0.006,
noAccessPenalty: 0.045,
lowLoyaltyTransitPenalty: 0.012,
foreignTransitPenalty: 0.008
}),
culture: Object.freeze({
spreadRadius: 3,

1349
engine.js

File diff suppressed because it is too large Load diff

176
render.js
View file

@ -10,7 +10,14 @@ const image = renderImage;
const data = image.data;
const mode = els.viewMode.value;
const cityPolityColor = new Map();
const polityColors = new Map();
if (mode === "polities") {
for (const polity of sim.polities) {
polityColors.set(polity.id, {
color: polity.color,
isGraphHover: graphState.hoverPolityId === polity.id
});
}
for (const city of sim.cities) {
if (city.polityId === null) continue;
const polity = cityPolity(city);
@ -29,15 +36,34 @@ const v = clamp(w.resource[i] / 30, 0, 1);
color = mix([28, 36, 40], [107, 188, 85], v);
} else if (mode === "ethnicity") {
color = terrainInfo[w.terrain[i]].color;
const dominant = w.dominantEthnicity?.[i] ?? -1;
const ethnicityColor = dominant >= 0 ? sim.ethnicities.get(dominant)?.color : null;
if (ethnicityColor) {
const populationStrength = clamp(Math.sqrt(Math.max(0, w.population?.[i] || w.pressure[i] || 0)) / 16, 0.18, 0.68);
const diversityDimming = clamp(w.cultureDiversity?.[i] || 0, 0, 1) * 0.18;
color = mix(color, ethnicityColor, clamp(populationStrength - diversityDimming, 0.12, 0.64));
}
if (w.city[i] >= 0) {
const city = sim.getCityById(w.city[i]);
if (city) color = mix(color, cityMajorityColor(city), 0.72);
}
} else if (mode === "pressure") {
const v = clamp(w.pressure[i] / 12, 0, 1);
const pressure = w.populationPressure?.[i] ?? (w.pressure[i] / 12);
const v = clamp(pressure, 0, 1.6) / 1.6;
color = mix(terrainInfo[w.terrain[i]].color, [210, 73, 67], v);
} else if (mode === "polities") {
color = terrainInfo[w.terrain[i]].color;
const territory = w.polity?.[i] ?? -1;
if (territory >= 0) {
const polityColor = polityColors.get(territory);
if (polityColor) {
const strength = clamp(0.16 + (w.control?.[i] || 0) * 0.38, 0.18, 0.55);
color = mix(color, polityColor.color, polityColor.isGraphHover ? Math.min(0.72, strength + 0.18) : strength);
if (w.contested?.[i]) color = mix(color, [128, 74, 50], 0.30);
if (sim.isBorderTile?.(i)) color = mix(color, [232, 225, 185], 0.22);
if (graphState.hoverPolityId !== null && !polityColor.isGraphHover) color = mix(color, [16, 18, 19], 0.30);
}
}
if (w.city[i] >= 0) {
const polityColor = cityPolityColor.get(w.city[i]);
if (polityColor) {
@ -62,6 +88,7 @@ data[p + 3] = 255;
ctx.putImageData(image, 0, 0);
renderDisasters();
drawTradeLinks();
drawCampaigns();
drawAgentsAndCities(mode);
drawGraphPolityHighlight();
maybeRenderStateGraph();
@ -137,6 +164,27 @@ ctx.fillRect(tile % w.size, Math.floor(tile / w.size), 1, 1);
}
ctx.restore();
}
function drawCampaigns() {
if (!sim?.campaigns?.length) return;
const w = sim.world;
ctx.save();
for (const campaign of sim.campaigns) {
if (campaign.status !== "active" || campaign.targetTile == null) continue;
const x = campaign.targetTile % w.size;
const y = Math.floor(campaign.targetTile / w.size);
const age = Math.max(0, sim.year - campaign.started) / Math.max(1, campaign.expires - campaign.started);
const pulse = 0.5 + Math.sin(age * Math.PI * 8) * 0.5;
const radius = 2.5 + clamp(campaign.progress, 0, 1) * 4 + pulse * 1.5;
ctx.globalAlpha = 0.55;
ctx.strokeStyle = campaign.type === "border_war" ? "rgba(236, 126, 111, 0.95)" : "rgba(239, 211, 119, 0.95)";
ctx.lineWidth = 1;
strokeCircle(ctx, x + 0.5, y + 0.5, radius);
ctx.globalAlpha = 0.22;
ctx.fillStyle = campaign.type === "nonstate_subjugation" ? "rgba(180, 111, 73, 0.9)" : "rgba(239, 211, 119, 0.9)";
fillSquare(ctx, x, y, 1);
}
ctx.restore();
}
function drawAgentsAndCities(mode) {
ctx.save();
if (mode !== "ethnicity") {
@ -161,12 +209,8 @@ ctx.fillStyle = rgba(color, clamp(0.32 + tech * 0.68, 0.32, 1));
ctx.fillRect(a.x, a.y, 1, 1);
}
} else if (mode === "ethnicity") {
for (const a of sim.agents) {
const e = sim.ethnicities.get(a.ethnicity);
if (!e) continue;
ctx.fillStyle = rgb(e.color);
ctx.fillRect(a.x, a.y, 1, 1);
}
// Phase 2: ethnicity view is driven by tile-level population/culture fields.
// Individual agents remain in the simulation but no longer define this layer.
} else {
ctx.fillStyle = "#eeeccf";
for (const tile of sim.tileAgents.keys()) {
@ -273,7 +317,11 @@ 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 regionalEthnicity = w.dominantEthnicity[i] >= 0 ? `E${w.dominantEthnicity[i]}` : "-";
const tilePopulation = w.population?.[i] || 0;
const nearbyCampaigns = activeCampaignsNear(i);
els.tooltip.innerHTML = `
<strong>${city ? `City #${city.id}` : agent ? "Agent group" : terrain.name}</strong>
${tooltipSection("Tile", [
@ -284,22 +332,30 @@ els.tooltip.innerHTML = `
tooltipRow("Minerals", w.mineral[i].toFixed(2)),
tooltipRow("Temp / humid", `${w.temperature[i].toFixed(2)} / ${w.humidity[i].toFixed(2)}`),
tooltipRow("Water", waterInfluence),
tooltipRow("Pressure", w.pressure[i].toFixed(0)),
tooltipRow("Pressure", (w.populationPressure?.[i] ?? 0).toFixed(2)),
tooltipRow("Capacity", (w.populationCapacity?.[i] ?? 0).toFixed(1)),
tooltipRow("Pheromone", w.pheromone[i].toFixed(1)),
tooltipRow("Route", w.tradeRoute[i], w.tradeRoute[i])
])}
${tooltipSection("Territory", [
tooltipRow("Territory", territoryPolity ? `#${territoryPolity.id}` : "Unclaimed"),
tooltipRow("Control", `${Math.round((w.control?.[i] || 0) * 100)}%`),
tooltipRow("Contested", w.contested?.[i] ? "yes" : "no"),
tooltipRow("Campaign", nearbyCampaigns, nearbyCampaigns),
tooltipRow("Dominant ethnicity", regionalEthnicity),
tooltipRow("Tile population", tilePopulation >= 10 ? Math.round(tilePopulation).toLocaleString() : tilePopulation.toFixed(1))
])}
${tooltipSection("City & State", [
tooltipRow("Population", city?.population.toLocaleString(), city),
tooltipRow("Age cap", city ? `${Math.floor(sim.cityAgePopulationLimit(city)).toLocaleString()} / ${Math.round((city.ageWear || 0) * 100)}% wear` : "", city),
tooltipRow("Food stock", city?.storedResources.toFixed(1), city),
tooltipRow("Supply stress", (city?.supplyStress || 0).toFixed(2), city),
tooltipRow("Trade", city ? `${city.tradeLinks.size} links, ${(city.tradeValue || 0).toFixed(2)} value` : "", city),
tooltipRow("Knowledge", city ? `${knowledgeLevel(city, "farming").toFixed(2)} farm / ${knowledgeLevel(city, "metallurgy").toFixed(2)} metal` : "", city),
tooltipRow("City majority", cityEthnicity != null ? `E${cityEthnicity}` : "", cityEthnicity != null),
tooltipRow("City majority", `E${cityEthnicity}`, cityEthnicity),
city ? cityEthnicityPie(city) : "",
tooltipRow("State", polity ? `#${polity.id}` : "Independent", city),
tooltipRow("Loyalty", city?.loyalty.toFixed(2), city),
tooltipRow("Charisma", clamp(polity?.charisma ?? 1, SimConfig.polity?.charismaMin ?? 0.5, SimConfig.polity?.charismaMax ?? 3.0).toFixed(2), polity),
tooltipRow("Charisma", clamp(polity?.charisma ?? 1, 0.5, 1.5).toFixed(2), polity),
tooltipRow("Leader tenure", polity ? `${Math.floor((sim.year - (polity.leaderStarted ?? polity.founded ?? sim.year)) / WEEKS_PER_YEAR)}y` : "", polity),
tooltipRow("Treasury", polity?.treasury.toFixed(1), polity),
tooltipRow("Capital", polity && city ? polity.centerCityId === city.id ? "yes" : "no" : "", polity)
@ -329,44 +385,33 @@ els.tooltip.style.left = `${clamp(left, margin, Math.max(margin, hoverState.widt
els.tooltip.style.top = `${clamp(top, margin, Math.max(margin, hoverState.height - height - margin))}px`;
}
function cityEthnicityPie(city) {
if (!city) return "";
let entries = [...(city.ethnicityComposition || new Map())]
.map(([id, count]) => [id, Number(count)])
.filter(([, count]) => Number.isFinite(count) && count > 0)
if (!city?.ethnicityComposition?.size) return "";
const entries = [...city.ethnicityComposition]
.filter(([, count]) => count > 0)
.sort((a, b) => b[1] - a[1]);
if (!entries.length && city.population > 0) {
const tile = sim.world.idx(city.x, city.y);
const fallbackId = sim.world.dominantEthnicity[tile] > 0 ? sim.world.dominantEthnicity[tile] : null;
if (fallbackId != null) entries = [[fallbackId, city.population]];
}
const total = entries.reduce((sum, [, count]) => sum + count, 0);
if (total <= 0) return "";
const top = entries.slice(0, 5);
const other = entries.slice(5).reduce((sum, [, count]) => sum + count, 0);
const slices = other > 0 ? [...top, [0, other]] : top;
const sliceColor = id => {
const ethnicity = id !== 0 ? sim.ethnicities.get(id) : null;
return ethnicity?.color || [122, 130, 126];
};
let cursor = 0;
const gradient = slices.map(([id, count]) => {
const start = cursor / total * 100;
cursor += count;
const end = cursor / total * 100;
const color = sliceColor(id);
const ethnicity = id ? sim.ethnicities.get(id) : null;
const color = ethnicity?.color || [122, 130, 126];
return `rgb(${color.join(",")}) ${start.toFixed(2)}% ${end.toFixed(2)}%`;
}).join(", ");
const labels = slices.map(([id, count]) => {
const color = sliceColor(id);
const label = id !== 0 ? `E${id}` : "Other";
const ethnicity = id ? sim.ethnicities.get(id) : null;
const color = ethnicity?.color || [122, 130, 126];
const label = id ? `E${id}` : "Other";
return `<span class="ethnicity-pie-key"><i style="background: rgb(${color.join(",")})"></i>${label} ${Math.round(count / total * 100)}%</span>`;
}).join("");
const background = slices.length === 1
? `rgb(${sliceColor(slices[0][0]).join(",")})`
: `conic-gradient(${gradient})`;
return `
<div class="ethnicity-pie-wrap">
<div class="ethnicity-pie" style="background: ${background}"></div>
<div class="ethnicity-pie" style="background: conic-gradient(${gradient})"></div>
<div class="ethnicity-pie-labels">${labels}</div>
</div>
`;
@ -375,6 +420,21 @@ function hideTooltip() {
hoverState = null;
els.tooltip.hidden = true;
}
function activeCampaignsNear(tile) {
if (!sim?.campaigns?.length) return "";
const w = sim.world;
const x = tile % w.size;
const y = Math.floor(tile / w.size);
const labels = [];
for (const campaign of sim.campaigns) {
if (campaign.status !== "active" || campaign.targetTile == null) continue;
const cx = campaign.targetTile % w.size;
const cy = Math.floor(campaign.targetTile / w.size);
if (Math.abs(cx - x) + Math.abs(cy - y) > 3) continue;
labels.push(`${campaign.type.replaceAll("_", " ")} ${Math.round(clamp(campaign.progress, 0, 1) * 100)}%`);
}
return labels.slice(0, 2).join("; ");
}
function findAgentAt(x, y) {
const directIndex = sim.world.idx(x, y);
return sim.tileAgents.get(directIndex)?.[0] || null;
@ -797,6 +857,60 @@ 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`
},
campaign: {
label: "campaign",
color: "rgba(232, 205, 116, 0.95)",
draw: drawGraphTriangleUp,
summary: data => `${data.outcome || "event"} ${data.campaignType || data.targetType || "target"}`
},
expansion: {
label: "expansion",
color: "rgba(210, 194, 104, 0.95)",
draw: drawGraphDiamond,
summary: data => `${data.outcome || "event"} ${data.campaignType || "frontier"}, tile ${data.targetTile ?? "-"}`
},
colonization: {
label: "colonization",
color: "rgba(232, 205, 116, 0.95)",
draw: drawGraphDiamond,
summary: data => `${data.campaignType || "frontier"}, progress ${Math.round((data.progress || 0) * 100)}%`
},
subjugation: {
label: "subjugation",
color: "rgba(196, 122, 77, 0.95)",
draw: drawGraphTriangleUp,
summary: data => `${data.campaignType || "non-state"}, tile ${data.targetTile ?? "-"}`
},
annexation: {
label: "annexation",
color: "rgba(227, 179, 65, 0.95)",
draw: drawGraphDiamond,
summary: data => `city #${data.targetCityId ?? "-"}`
},
failedCampaign: {
label: "failed campaign",
color: "rgba(151, 110, 91, 0.95)",
draw: drawGraphTriangleDown,
summary: data => `${data.outcome || "failed"} ${data.campaignType || "campaign"}`
},
borderWar: {
label: "border war",
color: "rgba(236, 126, 111, 0.95)",
draw: drawGraphTriangleUp,
summary: data => `${data.outcome || "war"} vs S${data.targetPolityId ?? "-"}`
},
war: {
label: "war",
color: "rgba(202, 87, 78, 0.95)",
draw: drawGraphTriangleUp,
summary: data => `${data.outcome || "war"} ${data.campaignType || ""}`
},
collapse: {
label: "collapse",
color: "rgba(130, 136, 139, 0.95)",
draw: drawGraphTriangleDown,
summary: data => data.reason || "state ended"
}
};
function graphEventLabel(event) {

View file

@ -412,6 +412,15 @@ canvas#world {
text-align: right;
}
.tooltip .status,
.tooltip .warning {
font-weight: 700;
}
.tooltip .warning em {
color: var(--danger);
}
.ethnicity-pie-wrap {
display: grid;
grid-template-columns: 44px 1fr;

View file

@ -77,7 +77,6 @@ function dominantComposition(composition) {
let bestId = null;
let bestCount = 0;
for (const [id, count] of composition) {
if (!Number.isFinite(count) || count <= 0) continue;
if (count > bestCount) {
bestId = id;
bestCount = count;