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>
|
||||
|
||||
<!-- Resources overview (collapsible) -->
|
||||
<details class="panel panelCollapsible">
|
||||
<details class="panel panelCollapsible" id="resourcesDetails">
|
||||
<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>
|
||||
</div>
|
||||
</details>
|
||||
@@ -267,6 +274,20 @@
|
||||
</div>
|
||||
</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 -->
|
||||
<details class="panel panelCollapsible">
|
||||
<summary class="panelTitle panelTitleSummary">🏷️ Crédits</summary>
|
||||
@@ -287,11 +308,6 @@
|
||||
|
||||
</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) ────────────────────────── -->
|
||||
<main class="galaxyMain">
|
||||
<!-- Mobile burger button -->
|
||||
|
||||
+91
-1
@@ -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<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 ─────────────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -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 `<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 ───────────────────────────────────────────
|
||||
|
||||
/**
|
||||
@@ -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";
|
||||
|
||||
|
||||
+125
-53
@@ -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,64 +346,70 @@ 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";
|
||||
|
||||
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 (playerListSortDir === "desc") {
|
||||
blue.reverse();
|
||||
red.reverse();
|
||||
}
|
||||
|
||||
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>`;
|
||||
}
|
||||
|
||||
async function openPlayerListPopup(anchorEl, team) {
|
||||
if (!playerListPopupEl || !playerListContentEl) return;
|
||||
try {
|
||||
const names = await apiFetchActivePlayerNames();
|
||||
const list = names[team] ?? [];
|
||||
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 */ }
|
||||
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 (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 (playerListSearchEl) {
|
||||
playerListSearchEl.addEventListener("input", renderPlayerListTable);
|
||||
}
|
||||
|
||||
if (playerListSortBtnEl) {
|
||||
playerListSortBtnEl.addEventListener("click", () => {
|
||||
playerListSortDir = playerListSortDir === "asc" ? "desc" : "asc";
|
||||
playerListSortBtnEl.textContent = playerListSortDir === "asc" ? "A–Z ↑" : "Z–A ↓";
|
||||
renderPlayerListTable();
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
});
|
||||
|
||||
// ── Element bonus ─────────────────────────────────────────────────────────────
|
||||
|
||||
let elemBonusBlue = 0;
|
||||
@@ -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;
|
||||
|
||||
@@ -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();
|
||||
|
||||
+127
-31
@@ -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 ─────────────────────────────────────────────────── */
|
||||
|
||||
+22
-1
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user