diff --git a/public/index.html b/public/index.html index c6b4185..1219d98 100644 --- a/public/index.html +++ b/public/index.html @@ -274,6 +274,14 @@ + +
+ πŸ† Tableau des scores +
+

Chargement…

+
+
+
πŸ‘₯ Joueurs actifs diff --git a/public/src/api.js b/public/src/api.js index 273999e..019c2d8 100644 --- a/public/src/api.js +++ b/public/src/api.js @@ -157,3 +157,9 @@ export async function apiCaptureCell(seed, x, y) { body: JSON.stringify({ seed, x, y }), }); } + +export async function apiFetchRoundHistory() { + const res = await fetch("/api/round-history"); + if (!res.ok) throw new Error("round_history_fetch_failed"); + return res.json(); +} diff --git a/public/src/game.js b/public/src/game.js index 2ead9b6..568afc3 100644 --- a/public/src/game.js +++ b/public/src/game.js @@ -1,6 +1,6 @@ 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 { apiFetchConfig, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints, apiFetchActivePlayers, apiFetchActivePlayerNames, apiFetchMilitaryDeductions, apiMilitaryAttack, apiCaptureCell, apiFetchRoundHistory } from "./api.js"; import { computeTeamIncome, computeTeamElementBonus, computeTeamElementBonusDetailed, renderResourceTable, renderElementBonusTable, setEconSort, getEconSort, setElemSort, getElemSort, computeTeamMilitaryDetailed, renderMilitaryTable, setMilSort, getMilSort, computeTeamIncomeByPlanet, renderResourceByPlanetTable, setPlanetSort, getPlanetSort } from "./economy.js"; // ── Constants ───────────────────────────────────────────────────────────────── @@ -265,6 +265,7 @@ const playerListTableWrapEl = document.getElementById("playerListTableWrap"); const playerListSearchEl = document.getElementById("playerListSearch"); const playerListSortBtnEl = document.getElementById("playerListSortBtn"); const mapAnimEl = document.getElementById("mapAnim"); +const roundHistoryTableBodyEl = document.getElementById("roundHistoryTableBody"); // ── Cell helpers ────────────────────────────────────────────────────────────── export function cellKey(x, y) { return `${x},${y}`; } @@ -369,6 +370,55 @@ export async function loadVictoryPoints() { } catch { /* ignore */ } } +export async function loadRoundHistory() { + if (!roundHistoryTableBodyEl) return; + try { + const history = await apiFetchRoundHistory(); + renderRoundHistoryTable(history); + } catch { /* ignore */ } +} + +function renderRoundHistoryTable(history) { + if (!roundHistoryTableBodyEl) return; + if (!history || history.length === 0) { + roundHistoryTableBodyEl.innerHTML = `

Aucune manche terminΓ©e.

`; + return; + } + const rows = history.map(entry => { + const blueScore = entry.blueScore; + const redScore = entry.redScore; + const blueWins = blueScore >= redScore; + const date = new Date(entry.endedAt); + const label = date.toLocaleString("fr-FR", { + day: "2-digit", month: "2-digit", year: "2-digit", + hour: "2-digit", minute: "2-digit", + }); + const fmt = (v) => v >= 1e6 + ? (v / 1e6).toFixed(1) + " M" + : v >= 1e3 + ? (v / 1e3).toFixed(1) + " k" + : v.toFixed(0); + const blueClass = blueWins ? "roundHistoryCell--winner-blue" : ""; + const redClass = !blueWins ? "roundHistoryCell--winner-red" : ""; + return ` + ${label} + ${fmt(blueScore)} + ${fmt(redScore)} + `; + }).join(""); + roundHistoryTableBodyEl.innerHTML = ` + + + + + + + + + ${rows} +
RΓ©sistancePremier Ordre
`; +} + export async function fetchAndApplyActivePlayers() { try { const { blue, red } = await apiFetchActivePlayers(); diff --git a/public/src/main.js b/public/src/main.js index 64849b8..324cbe2 100644 --- a/public/src/main.js +++ b/public/src/main.js @@ -10,6 +10,7 @@ import { updateEconomyDisplay, loadEconScores, loadVictoryPoints, + loadRoundHistory, loadDbInfo, loadElementBonus, loadMilitaryDeductions, @@ -98,7 +99,10 @@ function startFallbackPolling() { fallbackPollingEnabled = true; scheduleConfigPoll(); scheduleScorePoll(); - victoryPollTimer = setInterval(loadVictoryPoints, 30_000); + victoryPollTimer = setInterval(async () => { + await loadVictoryPoints(); + await loadRoundHistory(); + }, 30_000); gridPollTimer = setInterval(refreshGridDisplay, 1_000); } @@ -113,6 +117,7 @@ async function syncFromServer() { try { await refreshFromServer(); await loadVictoryPoints(); + await loadRoundHistory(); } finally { syncInFlight = false; } @@ -210,6 +215,7 @@ async function boot() { await fetchAndApplyActivePlayers(); await loadEconScores(); await loadVictoryPoints(); + await loadRoundHistory(); await loadDbInfo(); await loadElementBonus(); await loadMilitaryDeductions(); diff --git a/public/style.css b/public/style.css index 8bb8376..c010e2e 100644 --- a/public/style.css +++ b/public/style.css @@ -1397,4 +1397,75 @@ canvas { z-index: 20; user-select: none; white-space: nowrap; +} + +/* ── Round history table ──────────────────────────────────────────────────── */ + +.roundHistoryWrap { + padding: 6px 0 4px; +} + +.roundHistoryTable { + width: 100%; + border-collapse: collapse; + font-size: 12px; + table-layout: fixed; +} + +.roundHistoryTable thead th { + padding: 4px 8px; + font-size: 11px; + font-weight: 700; + letter-spacing: 0.04em; + text-transform: uppercase; + border-bottom: 1px solid rgba(255,255,255,0.1); + text-align: center; +} + +.roundHistoryTable thead th:first-child { + text-align: left; + width: 36%; +} + +.roundHistoryTh--blue { color: rgba(90, 200, 255, 0.9); } +.roundHistoryTh--red { color: rgba(220, 75, 85, 0.9); } + +.roundHistoryTable tbody tr { + border-bottom: 1px solid rgba(255,255,255,0.04); +} + +.roundHistoryTable td { + padding: 5px 8px; + text-align: center; + color: rgba(255,255,255,0.7); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.roundHistoryTable td:first-child { + text-align: left; + font-size: 10px; + color: rgba(255,255,255,0.4); +} + +.roundHistoryCell--winner-blue { + background: rgba(90, 200, 255, 0.18); + color: rgba(90, 200, 255, 1); + font-weight: 700; + border-radius: 3px; +} + +.roundHistoryCell--winner-red { + background: rgba(220, 75, 85, 0.18); + color: rgba(220, 75, 85, 1); + font-weight: 700; + border-radius: 3px; +} + +.roundHistoryEmpty { + padding: 10px 8px; + font-size: 12px; + color: rgba(255,255,255,0.35); + font-style: italic; } \ No newline at end of file diff --git a/server/db/gameDb.js b/server/db/gameDb.js index 66ea245..dbe0aea 100644 --- a/server/db/gameDb.js +++ b/server/db/gameDb.js @@ -134,6 +134,17 @@ export async function initGameSchema() { await pool.query(` ALTER TABLE grid_cells ADD COLUMN IF NOT EXISTS captured_by TEXT; `); + // ── Round history (one row per completed seed epoch) ───────────────────── + await pool.query(` + CREATE TABLE IF NOT EXISTS round_history ( + id SERIAL PRIMARY KEY, + world_seed TEXT NOT NULL, + ended_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + blue_score DOUBLE PRECISION NOT NULL DEFAULT 0, + red_score DOUBLE PRECISION NOT NULL DEFAULT 0 + ); + CREATE INDEX IF NOT EXISTS idx_round_history_ended_at ON round_history (ended_at DESC); + `); } // ── World-seed epoch ────────────────────────────────────────────────────────── @@ -164,6 +175,11 @@ export async function ensureSeedEpoch() { ); console.log(`[world] VP awarded to ${winner} for seed ${expiredSeed} (blue=${scores.blue.toFixed(3)}, red=${scores.red.toFixed(3)})`); } + // Record round result in history regardless of whether scores are non-zero + await pool.query( + `INSERT INTO round_history (world_seed, blue_score, red_score) VALUES ($1, $2, $3)`, + [expiredSeed, scores.blue, scores.red] + ); } catch (e) { console.error("[world] VP award error:", e); } @@ -347,10 +363,27 @@ export async function getDbCreatedAt() { return rows[0]?.created_at ?? null; } +// ── Round history ───────────────────────────────────────────────────────────── + +export async function getRoundHistory(limit = 10) { + const { rows } = await pool.query( + `SELECT world_seed, ended_at, blue_score, red_score + FROM round_history + ORDER BY ended_at DESC + LIMIT $1`, + [limit] + ); + return rows.map(r => ({ + worldSeed: r.world_seed, + endedAt: r.ended_at, + blueScore: Number(r.blue_score), + redScore: Number(r.red_score), + })); +} + // ── Victory points ──────────────────────────────────────────────────────────── -export async function getVictoryPoints() { - const { rows } = await pool.query( +export async function getVictoryPoints() { const { rows } = await pool.query( `SELECT team, COUNT(*) AS cnt FROM victory_points GROUP BY team` ); const result = { blue: 0, red: 0 }; diff --git a/server/routes/game.js b/server/routes/game.js index 2ae6108..b15dcea 100644 --- a/server/routes/game.js +++ b/server/routes/game.js @@ -32,6 +32,7 @@ import { checkTeamVisibility, insertTeamVisibility, getTeamVisibleCells, + getRoundHistory, } from "../db/gameDb.js"; import { nextResetUtc, @@ -553,4 +554,15 @@ router.get("/cell/attacks", async (req, res) => { } }); +// GET /api/round-history +router.get("/round-history", async (_req, res) => { + try { + const history = await getRoundHistory(10); + res.json(history); + } catch (e) { + console.error(e); + res.status(500).json({ error: "database_error" }); + } +}); + export default router; \ No newline at end of file