refacto: Changing display of revenues per planet by default + fixing WS for revenues
This commit is contained in:
+23
-7
@@ -191,9 +191,16 @@
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
<!-- Resources overview (collapsible) -->
|
<!-- Resources overview (collapsible) -->
|
||||||
<details class="panel panelCollapsible">
|
<details class="panel panelCollapsible" id="resourcesDetails">
|
||||||
<summary class="panelTitle panelTitleSummary">💰 Ressources</summary>
|
<summary class="panelTitle panelTitleSummary">💰 Ressources</summary>
|
||||||
<div id="resourceTableBody" class="econTableWrap">
|
<div class="resTabs">
|
||||||
|
<button type="button" class="resTabBtn resTabBtn--active" data-res-tab="planet">Par planète</button>
|
||||||
|
<button type="button" class="resTabBtn" data-res-tab="type">Par type</button>
|
||||||
|
</div>
|
||||||
|
<div id="resourcePlanetTableBody" class="econTableWrap">
|
||||||
|
<p class="econEmpty">Chargement…</p>
|
||||||
|
</div>
|
||||||
|
<div id="resourceTableBody" class="econTableWrap hidden">
|
||||||
<p class="econEmpty">Chargement…</p>
|
<p class="econEmpty">Chargement…</p>
|
||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
@@ -267,6 +274,20 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<!-- Players list -->
|
||||||
|
<details class="panel panelCollapsible">
|
||||||
|
<summary class="panelTitle panelTitleSummary">👥 Joueurs actifs</summary>
|
||||||
|
<div class="playerListPanel">
|
||||||
|
<div class="playerListFilter">
|
||||||
|
<input type="text" id="playerListSearch" class="playerListSearchInput" placeholder="Filtrer par nom…" autocomplete="off" />
|
||||||
|
<button type="button" id="playerListSortBtn" class="playerListSortBtn" title="Trier">A–Z ↑</button>
|
||||||
|
</div>
|
||||||
|
<div id="playerListTableWrap" class="playerListTableWrap">
|
||||||
|
<p class="econEmpty">Chargement…</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<!-- Credits -->
|
<!-- Credits -->
|
||||||
<details class="panel panelCollapsible">
|
<details class="panel panelCollapsible">
|
||||||
<summary class="panelTitle panelTitleSummary">🏷️ Crédits</summary>
|
<summary class="panelTitle panelTitleSummary">🏷️ Crédits</summary>
|
||||||
@@ -287,11 +308,6 @@
|
|||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<!-- Player list popup (shown on click of joueur count) -->
|
|
||||||
<div id="playerListPopup" class="playerListPopup hidden" role="tooltip">
|
|
||||||
<div id="playerListContent"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- ── Galaxy (square, 1000×1000, fixed ratio) ────────────────────────── -->
|
<!-- ── Galaxy (square, 1000×1000, fixed ratio) ────────────────────────── -->
|
||||||
<main class="galaxyMain">
|
<main class="galaxyMain">
|
||||||
<!-- Mobile burger button -->
|
<!-- Mobile burger button -->
|
||||||
|
|||||||
+91
-1
@@ -30,6 +30,21 @@ export function getElemSort() {
|
|||||||
return { col: _elemSortCol, dir: _elemSortDir };
|
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 ───────────────────────────────────────────────
|
// ── Label → resource key lookup ───────────────────────────────────────────────
|
||||||
|
|
||||||
/** Map from French label string → { cat: "common"|"rare", key: string } */
|
/** Map from French label string → { cat: "common"|"rare", key: string } */
|
||||||
@@ -79,6 +94,34 @@ export function computeTeamIncome(team, cells, resourceWorth) {
|
|||||||
return { total, byResource };
|
return { total, byResource };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute income per second per planet for a team.
|
||||||
|
*
|
||||||
|
* @param {string} team - "blue" or "red"
|
||||||
|
* @param {Map<string, { controlledBy: string|null, hasPlanet: boolean, planet: object|null }>} 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 ─────────────────────────────────────────────────
|
// ── Element bonus calculation ─────────────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,6 +191,51 @@ export function computeTeamElementBonusDetailed(team, cells, elementWorth) {
|
|||||||
|
|
||||||
export { elements };
|
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 `<p class="econEmpty">Aucune planète sous contrôle de votre équipe.</p>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 }) =>
|
||||||
|
`<tr>
|
||||||
|
<td class="econ-label">${name}</td>
|
||||||
|
<td class="econ-income econ-income--positive">+${income.toFixed(3)}/s</td>
|
||||||
|
</tr>`
|
||||||
|
)
|
||||||
|
.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 `<th class="econTh${activeClass}" data-planet-sort-col="${i}">${lbl}<span class="econSortIcon">${indicator}</span></th>`;
|
||||||
|
})
|
||||||
|
.join("");
|
||||||
|
|
||||||
|
return `<table class="econTable">
|
||||||
|
<thead><tr>${headers}</tr></thead>
|
||||||
|
<tbody>${tableRows}</tbody>
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Resource table for the sidebar ───────────────────────────────────────────
|
// ── 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 _milSortCol = 2;
|
||||||
let _milSortDir = "desc";
|
let _milSortDir = "desc";
|
||||||
|
|
||||||
|
|||||||
+128
-56
@@ -1,7 +1,7 @@
|
|||||||
import { fnv1a32, hash2u32, mulberry32 } from "./rng.js";
|
import { fnv1a32, hash2u32, mulberry32 } from "./rng.js";
|
||||||
import { formatPlanet, generatePlanet } from "./planetGeneration.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 { 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 ─────────────────────────────────────────────────────────────────
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -214,6 +214,9 @@ const effectiveCooldownEl = document.getElementById("effectiveCooldown");
|
|||||||
const incomeBlueEl = document.getElementById("incomeBlue");
|
const incomeBlueEl = document.getElementById("incomeBlue");
|
||||||
const incomeRedEl = document.getElementById("incomeRed");
|
const incomeRedEl = document.getElementById("incomeRed");
|
||||||
const resourceTableEl = document.getElementById("resourceTableBody");
|
const resourceTableEl = document.getElementById("resourceTableBody");
|
||||||
|
const resourcePlanetTableEl = document.getElementById("resourcePlanetTableBody");
|
||||||
|
/** @type {'planet'|'type'} */
|
||||||
|
let _resActiveTab = "planet";
|
||||||
const econScoreBlueEl = document.getElementById("econScoreBlue");
|
const econScoreBlueEl = document.getElementById("econScoreBlue");
|
||||||
const econScoreRedEl = document.getElementById("econScoreRed");
|
const econScoreRedEl = document.getElementById("econScoreRed");
|
||||||
const econDeltaBlueEl = document.getElementById("econDeltaBlue");
|
const econDeltaBlueEl = document.getElementById("econDeltaBlue");
|
||||||
@@ -234,8 +237,9 @@ const captureModalYesEl = document.getElementById("captureModalYes");
|
|||||||
const captureModalNoEl = document.getElementById("captureModalNo");
|
const captureModalNoEl = document.getElementById("captureModalNo");
|
||||||
const teamQuotaEl = document.getElementById("teamActionsRemaining");
|
const teamQuotaEl = document.getElementById("teamActionsRemaining");
|
||||||
const captorInfoEl = document.getElementById("captorInfo");
|
const captorInfoEl = document.getElementById("captorInfo");
|
||||||
const playerListPopupEl = document.getElementById("playerListPopup");
|
const playerListTableWrapEl = document.getElementById("playerListTableWrap");
|
||||||
const playerListContentEl = document.getElementById("playerListContent");
|
const playerListSearchEl = document.getElementById("playerListSearch");
|
||||||
|
const playerListSortBtnEl = document.getElementById("playerListSortBtn");
|
||||||
const mapAnimEl = document.getElementById("mapAnim");
|
const mapAnimEl = document.getElementById("mapAnim");
|
||||||
// ── Cell helpers ──────────────────────────────────────────────────────────────
|
// ── Cell helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -324,6 +328,13 @@ export async function fetchAndApplyActivePlayers() {
|
|||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function loadPlayerNames() {
|
||||||
|
try {
|
||||||
|
const names = await apiFetchActivePlayerNames();
|
||||||
|
applyPlayerNamesUpdate(names);
|
||||||
|
} catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
|
||||||
function setVictoryPointsDisplay(blue, red) {
|
function setVictoryPointsDisplay(blue, red) {
|
||||||
if (vpBlueEl) vpBlueEl.textContent = String(blue ?? 0);
|
if (vpBlueEl) vpBlueEl.textContent = String(blue ?? 0);
|
||||||
if (vpRedEl) vpRedEl.textContent = String(red ?? 0);
|
if (vpRedEl) vpRedEl.textContent = String(red ?? 0);
|
||||||
@@ -335,63 +346,69 @@ function setActivePlayersDisplay(blue, red) {
|
|||||||
if (activeCountRedEl) activeCountRedEl.textContent = fmt(red ?? 0);
|
if (activeCountRedEl) activeCountRedEl.textContent = fmt(red ?? 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Player list popup (click on joueur count) ─────────────────────────────────
|
// ── Player list panel ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function closePlayerListPopup() {
|
/** Last known player names from the server, sorted ascending. */
|
||||||
if (playerListPopupEl) playerListPopupEl.classList.add("hidden");
|
let playerNamesCache = { blue: [], red: [] };
|
||||||
}
|
/** Current sort direction: "asc" or "desc". */
|
||||||
|
let playerListSortDir = "asc";
|
||||||
|
|
||||||
async function openPlayerListPopup(anchorEl, team) {
|
function renderPlayerListTable() {
|
||||||
if (!playerListPopupEl || !playerListContentEl) return;
|
if (!playerListTableWrapEl) return;
|
||||||
try {
|
const filter = playerListSearchEl?.value.trim().toLowerCase() ?? "";
|
||||||
const names = await apiFetchActivePlayerNames();
|
const blue = playerNamesCache.blue.filter(n => n.toLowerCase().includes(filter));
|
||||||
const list = names[team] ?? [];
|
const red = playerNamesCache.red.filter(n => n.toLowerCase().includes(filter));
|
||||||
if (!list.length) {
|
|
||||||
playerListContentEl.innerHTML = `<span class="playerListEmpty">Aucun joueur actif</span>`;
|
|
||||||
} else {
|
|
||||||
const teamClass = `playerListName--${team}`;
|
|
||||||
playerListContentEl.innerHTML =
|
|
||||||
list.map(u => `<div class="${escHtml(teamClass)}">${escHtml(u)}</div>`).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 */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
if (activeCountBlueEl) {
|
if (playerListSortDir === "desc") {
|
||||||
activeCountBlueEl.style.cursor = "pointer";
|
blue.reverse();
|
||||||
activeCountBlueEl.addEventListener("click", (ev) => {
|
red.reverse();
|
||||||
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();
|
|
||||||
}
|
}
|
||||||
});
|
|
||||||
|
const maxRows = Math.max(blue.length, red.length);
|
||||||
|
if (maxRows === 0) {
|
||||||
|
playerListTableWrapEl.innerHTML = `<p class="econEmpty">Aucun joueur actif</p>`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const rows = Array.from({ length: maxRows }, (_, i) => {
|
||||||
|
const b = blue[i] ? `<td class="playerListCell--blue">${escHtml(blue[i])}</td>` : `<td></td>`;
|
||||||
|
const r = red[i] ? `<td class="playerListCell--red">${escHtml(red[i])}</td>` : `<td></td>`;
|
||||||
|
return `<tr>${b}${r}</tr>`;
|
||||||
|
}).join("");
|
||||||
|
|
||||||
|
playerListTableWrapEl.innerHTML = `
|
||||||
|
<table class="playerListTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="playerListTh--blue">Résistance (${blue.length})</th>
|
||||||
|
<th class="playerListTh--red">Premier Ordre (${red.length})</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${rows}</tbody>
|
||||||
|
</table>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 ─────────────────────────────────────────────────────────────
|
// ── Element bonus ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -482,6 +499,10 @@ export function updateEconomyDisplay() {
|
|||||||
if (resourceTableEl) {
|
if (resourceTableEl) {
|
||||||
resourceTableEl.innerHTML = renderResourceTable(worth, teamIncome.byResource);
|
resourceTableEl.innerHTML = renderResourceTable(worth, teamIncome.byResource);
|
||||||
}
|
}
|
||||||
|
if (resourcePlanetTableEl) {
|
||||||
|
const planetRows = computeTeamIncomeByPlanet(currentTeam, cells, worth);
|
||||||
|
resourcePlanetTableEl.innerHTML = renderResourceByPlanetTable(planetRows);
|
||||||
|
}
|
||||||
|
|
||||||
const elemWorth = GAME_CONFIG.elementWorth;
|
const elemWorth = GAME_CONFIG.elementWorth;
|
||||||
const teamElemBonus = currentTeam === "blue"
|
const teamElemBonus = currentTeam === "blue"
|
||||||
@@ -580,6 +601,10 @@ export function applyRealtimeSnapshot(snapshot) {
|
|||||||
setActivePlayersDisplay(snapshot.activePlayers.blue ?? 0, snapshot.activePlayers.red ?? 0);
|
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") {
|
if (snapshot.victoryPoints && typeof snapshot.victoryPoints === "object") {
|
||||||
setVictoryPointsDisplay(snapshot.victoryPoints.blue ?? 0, snapshot.victoryPoints.red ?? 0);
|
setVictoryPointsDisplay(snapshot.victoryPoints.blue ?? 0, snapshot.victoryPoints.red ?? 0);
|
||||||
}
|
}
|
||||||
@@ -587,6 +612,20 @@ export function applyRealtimeSnapshot(snapshot) {
|
|||||||
if (shouldUpdateEconomy) {
|
if (shouldUpdateEconomy) {
|
||||||
updateEconomyDisplay();
|
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. */
|
/** Trigger the delta fade animation on an element. */
|
||||||
@@ -1214,6 +1253,39 @@ resourceTableEl?.addEventListener("click", (ev) => {
|
|||||||
updateEconomyDisplay();
|
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) => {
|
elemBonusTableEl?.addEventListener("click", (ev) => {
|
||||||
const th = ev.target.closest("th[data-elem-sort-col]");
|
const th = ev.target.closest("th[data-elem-sort-col]");
|
||||||
if (!th) return;
|
if (!th) return;
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
fetchConfig,
|
fetchConfig,
|
||||||
fetchGridForSeed,
|
fetchGridForSeed,
|
||||||
fetchAndApplyActivePlayers,
|
fetchAndApplyActivePlayers,
|
||||||
|
loadPlayerNames,
|
||||||
updateEconomyDisplay,
|
updateEconomyDisplay,
|
||||||
loadEconScores,
|
loadEconScores,
|
||||||
loadVictoryPoints,
|
loadVictoryPoints,
|
||||||
@@ -83,6 +84,7 @@ function scheduleScorePoll() {
|
|||||||
clearTimeout(scorePollTimer);
|
clearTimeout(scorePollTimer);
|
||||||
scorePollTimer = window.setTimeout(async () => {
|
scorePollTimer = window.setTimeout(async () => {
|
||||||
await fetchAndApplyActivePlayers();
|
await fetchAndApplyActivePlayers();
|
||||||
|
await loadPlayerNames();
|
||||||
await loadEconScores();
|
await loadEconScores();
|
||||||
await loadElementBonus();
|
await loadElementBonus();
|
||||||
await loadMilitaryDeductions();
|
await loadMilitaryDeductions();
|
||||||
|
|||||||
+127
-31
@@ -1,5 +1,7 @@
|
|||||||
/* ── Reset & base ─────────────────────────────────────────────────────────── */
|
/* ── Reset & base ─────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.hidden { display: none !important; }
|
||||||
|
|
||||||
*,
|
*,
|
||||||
*::before,
|
*::before,
|
||||||
*::after {
|
*::after {
|
||||||
@@ -772,6 +774,41 @@ button:hover {
|
|||||||
animation: econDeltaFade 3s ease forwards;
|
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 ───────────────────────────────────────────────── */
|
/* ── Economy resource table ───────────────────────────────────────────────── */
|
||||||
|
|
||||||
.econTableWrap {
|
.econTableWrap {
|
||||||
@@ -1254,47 +1291,106 @@ canvas {
|
|||||||
color: rgba(220, 75, 85, 0.9);
|
color: rgba(220, 75, 85, 0.9);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Player list popup ─────────────────────────────────────────────────────── */
|
/* ── Players list panel ───────────────────────────────────────────────────── */
|
||||||
|
|
||||||
.playerListPopup {
|
.playerListPanel {
|
||||||
position: absolute;
|
padding: 10px 10px 12px;
|
||||||
z-index: 30;
|
display: flex;
|
||||||
min-width: 120px;
|
flex-direction: column;
|
||||||
max-width: 200px;
|
gap: 8px;
|
||||||
border-radius: 10px;
|
}
|
||||||
border: 1px solid rgba(255, 255, 255, 0.13);
|
|
||||||
background: rgba(12, 18, 38, 0.97);
|
.playerListFilter {
|
||||||
backdrop-filter: blur(8px);
|
display: flex;
|
||||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
|
gap: 6px;
|
||||||
padding: 8px 10px;
|
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-size: 11px;
|
||||||
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif;
|
font-weight: 700;
|
||||||
pointer-events: auto;
|
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 {
|
.playerListSortBtn:hover {
|
||||||
display: none;
|
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);
|
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);
|
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 ─────────────────────────────────────────────────── */
|
/* ── Map action animation ─────────────────────────────────────────────────── */
|
||||||
|
|||||||
+22
-1
@@ -1,7 +1,7 @@
|
|||||||
import { pool } from "./pools.js";
|
import { pool } from "./pools.js";
|
||||||
import { loadConfigFile, getConfig } from "../configLoader.js";
|
import { loadConfigFile, getConfig } from "../configLoader.js";
|
||||||
import { computeWorldSeedState } from "../worldSeed.js";
|
import { computeWorldSeedState } from "../worldSeed.js";
|
||||||
import { nextNoonUtc, resetAllUserActions } from "./usersDb.js";
|
import { nextNoonUtc, resetAllUserActions, getUsersByIds } from "./usersDb.js";
|
||||||
|
|
||||||
let lastSeedSlot = null;
|
let lastSeedSlot = null;
|
||||||
|
|
||||||
@@ -455,6 +455,27 @@ export async function getActivePlayerIds(worldSeed) {
|
|||||||
return result;
|
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) ─────────────────────
|
// ── Team action quota (daily, independent of world seed) ─────────────────────
|
||||||
|
|
||||||
export async function getTeamActionsRow(team) {
|
export async function getTeamActionsRow(team) {
|
||||||
|
|||||||
@@ -3,24 +3,47 @@ import {
|
|||||||
getElementBonus,
|
getElementBonus,
|
||||||
getMilitaryDeductions,
|
getMilitaryDeductions,
|
||||||
getActivePlayerCounts,
|
getActivePlayerCounts,
|
||||||
|
getActivePlayerNames,
|
||||||
getVictoryPoints,
|
getVictoryPoints,
|
||||||
|
getGridCells,
|
||||||
} from "./db/gameDb.js";
|
} from "./db/gameDb.js";
|
||||||
|
import { getConfig } from "./configLoader.js";
|
||||||
|
import { computeTeamIncome, computeTeamMilitaryPower } from "./helpers/economy.js";
|
||||||
|
|
||||||
export async function buildRealtimeSnapshot(worldSeed) {
|
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),
|
getEconScores(worldSeed),
|
||||||
getElementBonus(worldSeed),
|
getElementBonus(worldSeed),
|
||||||
getMilitaryDeductions(worldSeed),
|
getMilitaryDeductions(worldSeed),
|
||||||
getActivePlayerCounts(worldSeed),
|
getActivePlayerCounts(worldSeed),
|
||||||
|
getActivePlayerNames(worldSeed),
|
||||||
getVictoryPoints(),
|
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 {
|
return {
|
||||||
worldSeed,
|
worldSeed,
|
||||||
scores,
|
scores,
|
||||||
elementBonus,
|
elementBonus,
|
||||||
militaryDeductions,
|
militaryDeductions,
|
||||||
activePlayers,
|
activePlayers,
|
||||||
|
playerNames,
|
||||||
victoryPoints,
|
victoryPoints,
|
||||||
|
incomePerSecond,
|
||||||
|
militaryPowerGross,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user