removing agent for a new system
This commit is contained in:
parent
ff89ec62fd
commit
872a70ad8a
6 changed files with 1422 additions and 149 deletions
|
|
@ -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>"
|
||||
|
|
|
|||
32
config.js
32
config.js
|
|
@ -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,
|
||||
|
|
|
|||
176
render.js
176
render.js
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
1
utils.js
1
utils.js
|
|
@ -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;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue