From 5ce2ae6c987bd96b39d68bde8776f61a6cd2965c Mon Sep 17 00:00:00 2001 From: gauvainboiche Date: Thu, 2 Apr 2026 15:10:51 +0200 Subject: [PATCH] refacto: Changing display of revenues per planet by default + fixing WS for revenues --- public/index.html | 30 ++++-- public/src/economy.js | 92 ++++++++++++++++++- public/src/game.js | 184 ++++++++++++++++++++++++++----------- public/src/main.js | 2 + public/style.css | 158 ++++++++++++++++++++++++------- server/db/gameDb.js | 23 ++++- server/realtimeSnapshot.js | 25 ++++- 7 files changed, 417 insertions(+), 97 deletions(-) diff --git a/public/index.html b/public/index.html index d731b46..6e0c674 100644 --- a/public/index.html +++ b/public/index.html @@ -191,9 +191,16 @@ -
+
πŸ’° Ressources -
+
+ + +
+
+

Chargement…

+
+
@@ -267,6 +274,20 @@
+ +
+ πŸ‘₯ Joueurs actifs +
+
+ + +
+
+

Chargement…

+
+
+
+
🏷️ Crédits @@ -287,11 +308,6 @@ - - -
diff --git a/public/src/economy.js b/public/src/economy.js index c044ce9..6a92661 100644 --- a/public/src/economy.js +++ b/public/src/economy.js @@ -30,6 +30,21 @@ export function getElemSort() { return { col: _elemSortCol, dir: _elemSortDir }; } +// ── Sort state (per-planet resources) ──────────────────────────────────────── + +/** 0=PlanΓ¨te, 1=Revenu/s */ +let _planetSortCol = 0; +let _planetSortDir = "asc"; + +export function setPlanetSort(col, dir) { + _planetSortCol = col; + _planetSortDir = dir; +} + +export function getPlanetSort() { + return { col: _planetSortCol, dir: _planetSortDir }; +} + // ── Label β†’ resource key lookup ─────────────────────────────────────────────── /** Map from French label string β†’ { cat: "common"|"rare", key: string } */ @@ -79,6 +94,34 @@ export function computeTeamIncome(team, cells, resourceWorth) { return { total, byResource }; } +/** + * Compute income per second per planet for a team. + * + * @param {string} team - "blue" or "red" + * @param {Map} cells + * @param {object} resourceWorth - { common: {…}, rare: {…} } + * @returns {Array<{ name: string, income: number }>} + */ +export function computeTeamIncomeByPlanet(team, cells, resourceWorth) { + const rows = []; + for (const [, meta] of cells) { + if (meta.controlledBy !== team) continue; + if (!meta.hasPlanet || !meta.planet) continue; + const { name, naturalResources } = meta.planet; + if (!naturalResources) continue; + let income = 0; + for (const [label, pct] of Object.entries(naturalResources)) { + const info = LABEL_TO_RESOURCE.get(label); + if (!info) continue; + const worth = resourceWorth?.[info.cat]?.[info.key] ?? 0; + if (worth === 0) continue; + income += (pct / 100) * worth; + } + if (income > 0) rows.push({ name: name ?? "?", income }); + } + return rows; +} + // ── Element bonus calculation ───────────────────────────────────────────────── /** @@ -148,6 +191,51 @@ export function computeTeamElementBonusDetailed(team, cells, elementWorth) { export { elements }; +// ── Per-planet resource table for the sidebar ───────────────────────────────── + +/** + * Renders the per-planet income table. + * + * @param {Array<{ name: string, income: number }>} rows - output of computeTeamIncomeByPlanet + * @returns {string} HTML string + */ +export function renderResourceByPlanetTable(rows) { + if (!rows || rows.length === 0) { + return `

Aucune planète sous contrôle de votre équipe.

`; + } + + const sorted = [...rows]; + const mult = _planetSortDir === "asc" ? 1 : -1; + sorted.sort((a, b) => { + if (_planetSortCol === 0) return mult * a.name.localeCompare(b.name, "fr"); + return mult * (a.income - b.income); + }); + + const tableRows = sorted + .map(({ name, income }) => + ` + ${name} + +${income.toFixed(3)}/s + ` + ) + .join(""); + + const thLabels = ["Planète", "Revenu/s"]; + const headers = thLabels + .map((lbl, i) => { + const isActive = i === _planetSortCol; + const indicator = isActive ? (_planetSortDir === "asc" ? " ▲" : " ▼") : " ⇅"; + const activeClass = isActive ? " econTh--active" : ""; + return `${lbl}${indicator}`; + }) + .join(""); + + return ` + ${headers} + ${tableRows} +
`; +} + // ── Resource table for the sidebar ─────────────────────────────────────────── /** @@ -276,7 +364,9 @@ for (const [, subgroup] of Object.entries(population)) { } } -/** Sort state for military table: 0=Type, 1=% Mil., 2=Soldats */ +// ── Sort state (military) ───────────────────────────────────────────────────── + +/** 0=Type, 1=% Mil., 2=Soldats */ let _milSortCol = 2; let _milSortDir = "desc"; diff --git a/public/src/game.js b/public/src/game.js index 38fa387..c7c3732 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, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints, apiFetchActivePlayers, apiFetchActivePlayerNames, apiFetchMilitaryDeductions, apiMilitaryAttack, apiCaptureCell } from "./api.js"; -import { computeTeamIncome, computeTeamElementBonus, computeTeamElementBonusDetailed, renderResourceTable, renderElementBonusTable, setEconSort, getEconSort, setElemSort, getElemSort, computeTeamMilitaryDetailed, renderMilitaryTable, setMilSort, getMilSort } from "./economy.js"; +import { computeTeamIncome, computeTeamElementBonus, computeTeamElementBonusDetailed, renderResourceTable, renderElementBonusTable, setEconSort, getEconSort, setElemSort, getElemSort, computeTeamMilitaryDetailed, renderMilitaryTable, setMilSort, getMilSort, computeTeamIncomeByPlanet, renderResourceByPlanetTable, setPlanetSort, getPlanetSort } from "./economy.js"; // ── Constants ───────────────────────────────────────────────────────────────── @@ -214,6 +214,9 @@ const effectiveCooldownEl = document.getElementById("effectiveCooldown"); const incomeBlueEl = document.getElementById("incomeBlue"); const incomeRedEl = document.getElementById("incomeRed"); const resourceTableEl = document.getElementById("resourceTableBody"); +const resourcePlanetTableEl = document.getElementById("resourcePlanetTableBody"); +/** @type {'planet'|'type'} */ +let _resActiveTab = "planet"; const econScoreBlueEl = document.getElementById("econScoreBlue"); const econScoreRedEl = document.getElementById("econScoreRed"); const econDeltaBlueEl = document.getElementById("econDeltaBlue"); @@ -234,8 +237,9 @@ const captureModalYesEl = document.getElementById("captureModalYes"); const captureModalNoEl = document.getElementById("captureModalNo"); const teamQuotaEl = document.getElementById("teamActionsRemaining"); const captorInfoEl = document.getElementById("captorInfo"); -const playerListPopupEl = document.getElementById("playerListPopup"); -const playerListContentEl = document.getElementById("playerListContent"); +const playerListTableWrapEl = document.getElementById("playerListTableWrap"); +const playerListSearchEl = document.getElementById("playerListSearch"); +const playerListSortBtnEl = document.getElementById("playerListSortBtn"); const mapAnimEl = document.getElementById("mapAnim"); // ── Cell helpers ────────────────────────────────────────────────────────────── @@ -324,6 +328,13 @@ export async function fetchAndApplyActivePlayers() { } catch { /* ignore */ } } +export async function loadPlayerNames() { + try { + const names = await apiFetchActivePlayerNames(); + applyPlayerNamesUpdate(names); + } catch { /* ignore */ } +} + function setVictoryPointsDisplay(blue, red) { if (vpBlueEl) vpBlueEl.textContent = String(blue ?? 0); if (vpRedEl) vpRedEl.textContent = String(red ?? 0); @@ -335,63 +346,69 @@ function setActivePlayersDisplay(blue, red) { if (activeCountRedEl) activeCountRedEl.textContent = fmt(red ?? 0); } -// ── Player list popup (click on joueur count) ───────────────────────────────── +// ── Player list panel ───────────────────────────────────────────────────────── -function closePlayerListPopup() { - if (playerListPopupEl) playerListPopupEl.classList.add("hidden"); -} +/** Last known player names from the server, sorted ascending. */ +let playerNamesCache = { blue: [], red: [] }; +/** Current sort direction: "asc" or "desc". */ +let playerListSortDir = "asc"; -async function openPlayerListPopup(anchorEl, team) { - if (!playerListPopupEl || !playerListContentEl) return; - try { - const names = await apiFetchActivePlayerNames(); - const list = names[team] ?? []; - if (!list.length) { - playerListContentEl.innerHTML = `Aucun joueur actif`; - } else { - const teamClass = `playerListName--${team}`; - playerListContentEl.innerHTML = - list.map(u => `
${escHtml(u)}
`).join(""); - } - // Position popup below the anchor - const rect = anchorEl.getBoundingClientRect(); - const parentRect = anchorEl.closest(".infoColumn, aside")?.getBoundingClientRect() ?? { left: 0, top: 0 }; - playerListPopupEl.style.left = `${rect.left - parentRect.left}px`; - playerListPopupEl.style.top = `${rect.bottom - parentRect.top + 4}px`; - playerListPopupEl.classList.remove("hidden"); - playerListPopupEl.dataset.team = team; - } catch { /* ignore */ } -} +function renderPlayerListTable() { + if (!playerListTableWrapEl) return; + const filter = playerListSearchEl?.value.trim().toLowerCase() ?? ""; + const blue = playerNamesCache.blue.filter(n => n.toLowerCase().includes(filter)); + const red = playerNamesCache.red.filter(n => n.toLowerCase().includes(filter)); -if (activeCountBlueEl) { - activeCountBlueEl.style.cursor = "pointer"; - activeCountBlueEl.addEventListener("click", (ev) => { - ev.stopPropagation(); - if (playerListPopupEl && !playerListPopupEl.classList.contains("hidden") && playerListPopupEl.dataset.team === "blue") { - closePlayerListPopup(); - } else { - openPlayerListPopup(activeCountBlueEl, "blue"); - } - }); -} - -if (activeCountRedEl) { - activeCountRedEl.style.cursor = "pointer"; - activeCountRedEl.addEventListener("click", (ev) => { - ev.stopPropagation(); - if (playerListPopupEl && !playerListPopupEl.classList.contains("hidden") && playerListPopupEl.dataset.team === "red") { - closePlayerListPopup(); - } else { - openPlayerListPopup(activeCountRedEl, "red"); - } - }); -} - -document.addEventListener("click", (ev) => { - if (playerListPopupEl && !playerListPopupEl.contains(ev.target)) { - closePlayerListPopup(); + if (playerListSortDir === "desc") { + blue.reverse(); + red.reverse(); } -}); + + const maxRows = Math.max(blue.length, red.length); + if (maxRows === 0) { + playerListTableWrapEl.innerHTML = `

Aucun joueur actif

`; + return; + } + + const rows = Array.from({ length: maxRows }, (_, i) => { + const b = blue[i] ? `${escHtml(blue[i])}` : ``; + const r = red[i] ? `${escHtml(red[i])}` : ``; + return `${b}${r}`; + }).join(""); + + playerListTableWrapEl.innerHTML = ` + + + + + + + + ${rows} +
RΓ©sistance (${blue.length})Premier Ordre (${red.length})
`; +} + +export function applyPlayerNamesUpdate(names) { + if (!names || typeof names !== "object") return; + // Server sends pre-sorted asc; store as-is and sort desc lazily when rendering + playerNamesCache = { + blue: Array.isArray(names.blue) ? [...names.blue] : [], + red: Array.isArray(names.red) ? [...names.red] : [], + }; + renderPlayerListTable(); +} + +if (playerListSearchEl) { + playerListSearchEl.addEventListener("input", renderPlayerListTable); +} + +if (playerListSortBtnEl) { + playerListSortBtnEl.addEventListener("click", () => { + playerListSortDir = playerListSortDir === "asc" ? "desc" : "asc"; + playerListSortBtnEl.textContent = playerListSortDir === "asc" ? "A–Z ↑" : "Z–A ↓"; + renderPlayerListTable(); + }); +} // ── Element bonus ───────────────────────────────────────────────────────────── @@ -482,6 +499,10 @@ export function updateEconomyDisplay() { if (resourceTableEl) { resourceTableEl.innerHTML = renderResourceTable(worth, teamIncome.byResource); } + if (resourcePlanetTableEl) { + const planetRows = computeTeamIncomeByPlanet(currentTeam, cells, worth); + resourcePlanetTableEl.innerHTML = renderResourceByPlanetTable(planetRows); + } const elemWorth = GAME_CONFIG.elementWorth; const teamElemBonus = currentTeam === "blue" @@ -580,6 +601,10 @@ export function applyRealtimeSnapshot(snapshot) { setActivePlayersDisplay(snapshot.activePlayers.blue ?? 0, snapshot.activePlayers.red ?? 0); } + if (snapshot.playerNames && typeof snapshot.playerNames === "object") { + applyPlayerNamesUpdate(snapshot.playerNames); + } + if (snapshot.victoryPoints && typeof snapshot.victoryPoints === "object") { setVictoryPointsDisplay(snapshot.victoryPoints.blue ?? 0, snapshot.victoryPoints.red ?? 0); } @@ -587,6 +612,20 @@ export function applyRealtimeSnapshot(snapshot) { if (shouldUpdateEconomy) { updateEconomyDisplay(); } + + // Override income and military displays with authoritative server values, + // which reflect ALL team cells (not just locally-visible ones). + if (snapshot.incomePerSecond && typeof snapshot.incomePerSecond === "object") { + if (incomeBlueEl) incomeBlueEl.textContent = `+${Number(snapshot.incomePerSecond.blue ?? 0).toFixed(3)}/s`; + if (incomeRedEl) incomeRedEl.textContent = `+${Number(snapshot.incomePerSecond.red ?? 0).toFixed(3)}/s`; + } + + if (snapshot.militaryPowerGross && typeof snapshot.militaryPowerGross === "object") { + const blueMilNet = Number(snapshot.militaryPowerGross.blue ?? 0) - milDeductBlue; + const redMilNet = Number(snapshot.militaryPowerGross.red ?? 0) - milDeductRed; + if (milTotalBlueEl) milTotalBlueEl.textContent = (blueMilNet * 1000).toFixed(1); + if (milTotalRedEl) milTotalRedEl.textContent = (redMilNet * 1000).toFixed(1); + } } /** Trigger the delta fade animation on an element. */ @@ -1214,6 +1253,39 @@ resourceTableEl?.addEventListener("click", (ev) => { updateEconomyDisplay(); }); +// ── Per-planet resource table sort (delegated, set up once) ────────────────── + +resourcePlanetTableEl?.addEventListener("click", (ev) => { + const th = ev.target.closest("th[data-planet-sort-col]"); + if (!th) return; + const col = Number(th.dataset.planetSortCol); + const { col: curCol, dir: curDir } = getPlanetSort(); + const newDir = col === curCol + ? (curDir === "asc" ? "desc" : "asc") + : (col === 0 ? "asc" : "desc"); + setPlanetSort(col, newDir); + updateEconomyDisplay(); +}); + +// ── Resource section tab switching ──────────────────────────────────────────── + +document.getElementById("resourcesDetails")?.addEventListener("click", (ev) => { + const btn = ev.target.closest(".resTabBtn[data-res-tab]"); + if (!btn) return; + const tab = btn.dataset.resTab; + if (tab === _resActiveTab) return; + _resActiveTab = tab; + // Update button active state + document.querySelectorAll(".resTabBtn[data-res-tab]").forEach(b => { + b.classList.toggle("resTabBtn--active", b.dataset.resTab === tab); + }); + // Show/hide panes + const planetPane = document.getElementById("resourcePlanetTableBody"); + const typePane = document.getElementById("resourceTableBody"); + if (planetPane) planetPane.classList.toggle("hidden", tab !== "planet"); + if (typePane) typePane.classList.toggle("hidden", tab !== "type"); +}); + elemBonusTableEl?.addEventListener("click", (ev) => { const th = ev.target.closest("th[data-elem-sort-col]"); if (!th) return; diff --git a/public/src/main.js b/public/src/main.js index 564ed12..db4809c 100644 --- a/public/src/main.js +++ b/public/src/main.js @@ -5,6 +5,7 @@ import { fetchConfig, fetchGridForSeed, fetchAndApplyActivePlayers, + loadPlayerNames, updateEconomyDisplay, loadEconScores, loadVictoryPoints, @@ -83,6 +84,7 @@ function scheduleScorePoll() { clearTimeout(scorePollTimer); scorePollTimer = window.setTimeout(async () => { await fetchAndApplyActivePlayers(); + await loadPlayerNames(); await loadEconScores(); await loadElementBonus(); await loadMilitaryDeductions(); diff --git a/public/style.css b/public/style.css index 8e58239..e484e9a 100644 --- a/public/style.css +++ b/public/style.css @@ -1,5 +1,7 @@ /* ── Reset & base ─────────────────────────────────────────────────────────── */ +.hidden { display: none !important; } + *, *::before, *::after { @@ -772,6 +774,41 @@ button:hover { animation: econDeltaFade 3s ease forwards; } +/* ── Resource section tabs ────────────────────────────────────────────────── */ + +.resTabs { + display: flex; + gap: 2px; + padding: 6px 8px 0; +} + +.resTabBtn { + flex: 1; + padding: 4px 8px; + background: rgba(255, 255, 255, 0.05); + border: 1px solid rgba(255, 255, 255, 0.1); + border-radius: 4px 4px 0 0; + color: rgba(233, 238, 246, 0.55); + font-family: "Courier New", Courier, monospace; + font-size: 11px; + font-weight: 600; + cursor: pointer; + letter-spacing: 0.03em; + transition: color 0.15s, background 0.15s, border-color 0.15s; + text-transform: uppercase; +} + +.resTabBtn:hover { + color: rgba(233, 238, 246, 0.85); + background: rgba(255, 255, 255, 0.09); +} + +.resTabBtn--active { + background: rgba(113, 199, 255, 0.12); + border-color: rgba(113, 199, 255, 0.35); + color: rgba(113, 199, 255, 0.95); +} + /* ── Economy resource table ───────────────────────────────────────────────── */ .econTableWrap { @@ -1254,47 +1291,106 @@ canvas { color: rgba(220, 75, 85, 0.9); } -/* ── Player list popup ─────────────────────────────────────────────────────── */ +/* ── Players list panel ───────────────────────────────────────────────────── */ -.playerListPopup { - position: absolute; - z-index: 30; - min-width: 120px; - max-width: 200px; - border-radius: 10px; - border: 1px solid rgba(255, 255, 255, 0.13); - background: rgba(12, 18, 38, 0.97); - backdrop-filter: blur(8px); - box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6); - padding: 8px 10px; +.playerListPanel { + padding: 10px 10px 12px; + display: flex; + flex-direction: column; + gap: 8px; +} + +.playerListFilter { + display: flex; + gap: 6px; + align-items: center; +} + +.playerListSearchInput { + flex: 1; + padding: 6px 10px; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.12); + background: rgba(255, 255, 255, 0.06); + color: #e9eef6; + font-size: 12px; + outline: none; + font-family: "Courier New", Courier, monospace; + transition: border-color 0.15s; +} + +.playerListSearchInput:focus { + border-color: rgba(113, 199, 255, 0.5); +} + +.playerListSearchInput::placeholder { + color: rgba(233, 238, 246, 0.35); +} + +.playerListSortBtn { + padding: 5px 10px; font-size: 11px; - font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif; - pointer-events: auto; + font-weight: 700; + font-family: "Courier New", Courier, monospace; + border-radius: 8px; + border: 1px solid rgba(255, 255, 255, 0.14); + background: rgba(255, 255, 255, 0.07); + color: rgba(233, 238, 246, 0.75); + cursor: pointer; + white-space: nowrap; + transition: background 0.15s; } -.playerListPopup.hidden { - display: none; +.playerListSortBtn:hover { + background: rgba(255, 255, 255, 0.13); + color: #e9eef6; } -.playerListName--blue { +.playerListTableWrap { + overflow-x: auto; +} + +.playerListTable { + width: 100%; + border-collapse: collapse; + font-size: 11px; + font-family: "Courier New", Courier, monospace; +} + +.playerListTable thead th { + padding: 5px 8px; + text-align: left; + font-size: 10px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.05em; + border-bottom: 1px solid rgba(255, 255, 255, 0.08); +} + +.playerListTable thead th.playerListTh--blue { + color: rgba(90, 200, 255, 0.85); +} + +.playerListTable thead th.playerListTh--red { + color: rgba(220, 75, 85, 0.85); +} + +.playerListTable tbody tr:hover { + background: rgba(255, 255, 255, 0.04); +} + +.playerListTable td { + padding: 3px 8px; + border-bottom: 1px solid rgba(255, 255, 255, 0.04); + vertical-align: top; +} + +.playerListCell--blue { color: rgba(90, 200, 255, 0.9); - padding: 2px 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; } -.playerListName--red { +.playerListCell--red { color: rgba(220, 75, 85, 0.9); - padding: 2px 0; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; -} - -.playerListEmpty { - color: rgba(233, 238, 246, 0.4); - font-style: italic; } /* ── Map action animation ─────────────────────────────────────────────────── */ diff --git a/server/db/gameDb.js b/server/db/gameDb.js index 6b5220f..69721b1 100644 --- a/server/db/gameDb.js +++ b/server/db/gameDb.js @@ -1,7 +1,7 @@ import { pool } from "./pools.js"; import { loadConfigFile, getConfig } from "../configLoader.js"; import { computeWorldSeedState } from "../worldSeed.js"; -import { nextNoonUtc, resetAllUserActions } from "./usersDb.js"; +import { nextNoonUtc, resetAllUserActions, getUsersByIds } from "./usersDb.js"; let lastSeedSlot = null; @@ -455,6 +455,27 @@ export async function getActivePlayerIds(worldSeed) { return result; } +/** + * Returns per-team sorted lists of usernames who have been active this seed epoch. + * Uses a two-step lookup because users live in a separate database. + */ +export async function getActivePlayerNames(worldSeed) { + const playerIds = await getActivePlayerIds(worldSeed); + const allIds = [...playerIds.blue, ...playerIds.red]; + const users = await getUsersByIds(allIds); + const teamOf = {}; + for (const id of playerIds.blue) teamOf[id] = "blue"; + for (const id of playerIds.red) teamOf[id] = "red"; + const result = { blue: [], red: [] }; + for (const user of users) { + const team = teamOf[user.id]; + if (team) result[team].push(user.username); + } + result.blue.sort((a, b) => a.localeCompare(b)); + result.red.sort((a, b) => a.localeCompare(b)); + return result; +} + // ── Team action quota (daily, independent of world seed) ───────────────────── export async function getTeamActionsRow(team) { diff --git a/server/realtimeSnapshot.js b/server/realtimeSnapshot.js index fe8178d..3245499 100644 --- a/server/realtimeSnapshot.js +++ b/server/realtimeSnapshot.js @@ -3,24 +3,47 @@ import { getElementBonus, getMilitaryDeductions, getActivePlayerCounts, + getActivePlayerNames, getVictoryPoints, + getGridCells, } from "./db/gameDb.js"; +import { getConfig } from "./configLoader.js"; +import { computeTeamIncome, computeTeamMilitaryPower } from "./helpers/economy.js"; export async function buildRealtimeSnapshot(worldSeed) { - const [scores, elementBonus, militaryDeductions, activePlayers, victoryPoints] = await Promise.all([ + const [scores, elementBonus, militaryDeductions, activePlayers, playerNames, victoryPoints, rows] = await Promise.all([ getEconScores(worldSeed), getElementBonus(worldSeed), getMilitaryDeductions(worldSeed), getActivePlayerCounts(worldSeed), + getActivePlayerNames(worldSeed), getVictoryPoints(), + getGridCells(worldSeed), ]); + const cfg = getConfig(); + const resourceWorth = cfg.resourceWorth ?? { common: {}, rare: {} }; + const militaryPowerCfg = cfg.militaryPower ?? {}; + + const incomePerSecond = { + blue: computeTeamIncome("blue", rows, resourceWorth), + red: computeTeamIncome("red", rows, resourceWorth), + }; + + const militaryPowerGross = { + blue: computeTeamMilitaryPower("blue", rows, militaryPowerCfg), + red: computeTeamMilitaryPower("red", rows, militaryPowerCfg), + }; + return { worldSeed, scores, elementBonus, militaryDeductions, activePlayers, + playerNames, victoryPoints, + incomePerSecond, + militaryPowerGross, }; }