diff --git a/public/src/economy.js b/public/src/economy.js
index 20b652a..b75f0bb 100644
--- a/public/src/economy.js
+++ b/public/src/economy.js
@@ -1,6 +1,6 @@
import { resources, elements } from "./planetEconomy.js";
-// ── Sort state ────────────────────────────────────────────────────────────────
+// ── Sort state (resources) ────────────────────────────────────────────────────
/** 0=Ressource, 1=Rareté, 2=Valeur, 3=Revenu/s */
let _sortCol = 3;
@@ -15,6 +15,21 @@ export function getEconSort() {
return { col: _sortCol, dir: _sortDir };
}
+// ── Sort state (elements) ─────────────────────────────────────────────────────
+
+/** 0=Élément, 1=Valeur/élément, 2=Bonus % */
+let _elemSortCol = 2;
+let _elemSortDir = "desc";
+
+export function setElemSort(col, dir) {
+ _elemSortCol = col;
+ _elemSortDir = dir;
+}
+
+export function getElemSort() {
+ return { col: _elemSortCol, dir: _elemSortDir };
+}
+
// ── Label → resource key lookup ───────────────────────────────────────────────
/** Map from French label string → { cat: "common"|"rare", key: string } */
@@ -102,6 +117,35 @@ export function computeTeamElementBonus(team, cells, elementWorth) {
return bonus;
}
+/**
+ * Compute per-element bonus breakdown for a team.
+ *
+ * @param {string} team
+ * @param {Map
} cells
+ * @param {object} elementWorth - { common: 1, petrol: 3, ... }
+ * @returns {{ total: number, byElement: Map }}
+ * byElement keys are French element label strings, values are cumulative bonus %
+ */
+export function computeTeamElementBonusDetailed(team, cells, elementWorth) {
+ const byElement = new Map();
+ let total = 0;
+ for (const [, meta] of cells) {
+ if (meta.discoveredBy !== team) continue;
+ if (!meta.hasPlanet || !meta.planet) continue;
+ const { production } = meta.planet;
+ if (!production) continue;
+ for (const [elementLabel, pct] of Object.entries(production)) {
+ const elementKey = ELEMENT_LABEL_TO_KEY[elementLabel] ?? elementLabel;
+ const worth = elementWorth?.[elementKey] ?? 0;
+ if (worth === 0) continue;
+ const bonus = (pct / 100) * worth;
+ byElement.set(elementLabel, (byElement.get(elementLabel) ?? 0) + bonus);
+ total += bonus;
+ }
+ }
+ return { total, byElement };
+}
+
export { elements };
// ── Resource table for the sidebar ───────────────────────────────────────────
@@ -158,6 +202,64 @@ export function renderResourceTable(resourceWorth, teamByResource) {
})
.join("");
+ return `
+ ${headers}
+ ${tableRows}
+
`;
+}
+
+// ── Element bonus table for the sidebar ──────────────────────────────────────
+
+/**
+ * Renders the element bonus breakdown table.
+ *
+ * @param {object} elementWorth - { common: 1, petrol: 3, ... }
+ * @param {Map} teamByElement - bonus per element label for current team
+ * @returns {string} HTML string
+ */
+export function renderElementBonusTable(elementWorth, teamByElement) {
+ const rows = [];
+
+ for (const [key, label] of Object.entries(elements)) {
+ const worth = elementWorth?.[key] ?? 0;
+ const bonus = teamByElement?.get(label) ?? 0;
+ const bonusStr = bonus > 0 ? `+${bonus.toFixed(3)}%` : "—";
+ rows.push({ label, worth, bonus, bonusStr });
+ }
+
+ const mult = _elemSortDir === "asc" ? 1 : -1;
+ rows.sort((a, b) => {
+ if (_elemSortCol === 0) return mult * a.label.localeCompare(b.label, "fr");
+ if (_elemSortCol === 1) return mult * (a.worth - b.worth);
+ if (_elemSortCol === 2) return mult * (a.bonus - b.bonus);
+ return b.bonus - a.bonus || b.worth - a.worth;
+ });
+
+ if (rows.every(r => r.bonus === 0)) {
+ return `Aucune tuile conquise avec des éléments de production.
`;
+ }
+
+ const tableRows = rows
+ .map(({ label, worth, bonus, bonusStr }) => {
+ const bonusClass = bonus > 0 ? " econ-income--positive" : "";
+ return `
+ | ${label} |
+ ${worth} |
+ ${bonusStr} |
+
`;
+ })
+ .join("");
+
+ const thLabels = ["Élément", "Val./élément", "Bonus %"];
+ const headers = thLabels
+ .map((lbl, i) => {
+ const isActive = i === _elemSortCol;
+ const indicator = isActive ? (_elemSortDir === "asc" ? " ▲" : " ▼") : " ⇅";
+ const activeClass = isActive ? " econTh--active" : "";
+ return `${lbl}${indicator} | `;
+ })
+ .join("");
+
return `
${headers}
${tableRows}
diff --git a/public/src/game.js b/public/src/game.js
index c1c07e7..ffc022e 100644
--- a/public/src/game.js
+++ b/public/src/game.js
@@ -1,7 +1,7 @@
import { fnv1a32, hash2u32, mulberry32 } from "./rng.js";
import { formatPlanet, generatePlanet } from "./planetGeneration.js";
import { apiFetchConfig, apiFetchScores, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints } from "./api.js";
-import { computeTeamIncome, computeTeamElementBonus, renderResourceTable, setEconSort, getEconSort } from "./economy.js";
+import { computeTeamIncome, computeTeamElementBonus, computeTeamElementBonusDetailed, renderResourceTable, renderElementBonusTable, setEconSort, getEconSort, setElemSort, getElemSort } from "./economy.js";
// ── Constants ─────────────────────────────────────────────────────────────────
@@ -163,6 +163,7 @@ const econScoreBlueEl = document.getElementById("econScoreBlue");
const econScoreRedEl = document.getElementById("econScoreRed");
const econDeltaBlueEl = document.getElementById("econDeltaBlue");
const econDeltaRedEl = document.getElementById("econDeltaRed");
+const elemBonusTableEl = document.getElementById("elementBonusTableBody");
const teamCorner = document.getElementById("teamCorner");
const teamTrack = document.getElementById("teamSegmentedTrack");
const teamBlueBtn = document.getElementById("teamBlue");
@@ -333,6 +334,14 @@ export function updateEconomyDisplay() {
if (resourceTableEl) {
resourceTableEl.innerHTML = renderResourceTable(worth, teamIncome.byResource);
}
+
+ const elemWorth = GAME_CONFIG.elementWorth;
+ const teamElemBonus = currentTeam === "blue"
+ ? computeTeamElementBonusDetailed("blue", cells, elemWorth)
+ : computeTeamElementBonusDetailed("red", cells, elemWorth);
+ if (elemBonusTableEl) {
+ elemBonusTableEl.innerHTML = renderElementBonusTable(elemWorth, teamElemBonus.byElement);
+ }
}
// ── Economic score ────────────────────────────────────────────────────────────
@@ -718,6 +727,18 @@ resourceTableEl?.addEventListener("click", (ev) => {
updateEconomyDisplay();
});
+elemBonusTableEl?.addEventListener("click", (ev) => {
+ const th = ev.target.closest("th[data-elem-sort-col]");
+ if (!th) return;
+ const col = Number(th.dataset.elemSortCol);
+ const { col: curCol, dir: curDir } = getElemSort();
+ const newDir = col === curCol
+ ? (curDir === "asc" ? "desc" : "asc")
+ : (col === 0 ? "asc" : "desc");
+ setElemSort(col, newDir);
+ updateEconomyDisplay();
+});
+
canvas.addEventListener("mousemove", (ev) => { lastPointerEvent = ev; refreshCursor(ev); });
canvas.addEventListener("mouseleave", () => { lastPointerEvent = null; canvas.style.cursor = "default"; });
canvas.addEventListener("click", onCanvasClick);
diff --git a/public/style.css b/public/style.css
index 9599175..cc1d5d4 100644
--- a/public/style.css
+++ b/public/style.css
@@ -276,8 +276,8 @@ body {
.scoreBoard {
display: flex;
+ flex-direction: row;
align-items: center;
- justify-content: space-between;
gap: 8px;
padding: 10px 12px;
border-radius: 14px;
@@ -287,6 +287,29 @@ body {
font-variant-numeric: tabular-nums;
}
+.scoreBoardContent {
+ flex: 1;
+ display: flex;
+ flex-direction: column;
+ min-width: 0;
+}
+
+.scoreBoardRow {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+}
+
+.scoreBoardEcon {
+ display: flex;
+ flex-direction: column;
+ gap: 6px;
+ padding: 8px 0 0;
+ border-top: 1px solid rgba(255, 255, 255, 0.08);
+ margin-top: 8px;
+}
+
.team-logo {
width: 54px;
height: 54px;
@@ -676,15 +699,6 @@ button:hover {
/* ── Team income summary ──────────────────────────────────────────────────── */
-.econSummary {
- display: flex;
- flex-direction: column;
- gap: 6px;
- padding: 8px 12px;
- font-family: "Courier New", Courier, monospace;
- font-size: 12px;
-}
-
.econSummaryTeam {
display: flex;
align-items: center;
@@ -700,10 +714,12 @@ button:hover {
justify-content: flex-start;
}
-.econSummaryTeam--blue .econSummaryLabel { color: rgba(90, 200, 255, 0.85); }
-.econSummaryTeam--blue .econSummaryVal { color: rgba(90, 200, 255, 1); font-weight: 700; }
-.econSummaryTeam--red .econSummaryLabel { color: rgba(220, 75, 85, 0.85); }
-.econSummaryTeam--red .econSummaryVal { color: rgba(220, 75, 85, 1); font-weight: 700; }
+.econSummaryTeam--blue .econSummaryLabel { color: rgba(90, 200, 255, 0.85); font-size: 12px; }
+.econSummaryTeam--blue .econSummaryVal { color: rgba(90, 200, 255, 1); font-weight: 700; font-size: 12px; }
+.econSummaryTeam--red .econSummaryLabel { color: rgba(220, 75, 85, 0.85); font-size: 12px; }
+.econSummaryTeam--red .econSummaryVal { color: rgba(220, 75, 85, 1); font-weight: 700; font-size: 12px; }
+
+.econSummaryTeam--center { justify-content: center; }
.econSummarySep {
opacity: 0.3;
@@ -872,20 +888,29 @@ button:hover {
/* ── Element bonus section ────────────────────────────────────────────────── */
-.elemBonusSection {
- display: flex;
- flex-direction: column;
- gap: 6px;
- padding: 8px 12px;
- font-family: "Courier New", Courier, monospace;
- font-size: 12px;
-}
-
-.elemBonusRow {
+.elemBonusTotals {
display: flex;
align-items: center;
justify-content: center;
gap: 0;
+ padding: 8px 12px 4px;
+ font-family: "Courier New", Courier, monospace;
+ font-size: 12px;
+ flex-wrap: wrap;
+ row-gap: 4px;
+}
+
+.elemBonusEffective {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 5px;
+ border-top: 1px solid rgba(255, 255, 255, 0.06);
+ padding-top: 5px;
+ margin-top: 2px;
+ font-size: 11px;
+ font-family: "Courier New", Courier, monospace;
}
.elemBonusTeam {
@@ -919,16 +944,6 @@ button:hover {
margin: 0 6px;
}
-.elemBonusDetail {
- display: flex;
- align-items: center;
- gap: 6px;
- border-top: 1px solid rgba(255, 255, 255, 0.06);
- padding-top: 6px;
- margin-top: 2px;
- font-size: 11px;
-}
-
.elemBonusDetailLabel {
opacity: 0.55;
}