From d1240adbb77cf40187235322056f66dae1165b52 Mon Sep 17 00:00:00 2001 From: gauvainboiche Date: Tue, 31 Mar 2026 16:35:08 +0200 Subject: [PATCH] refacto: Displaying number of players for each team + adding logo at registration --- public/index.html | 22 ++++++++++++++++++---- public/src/api.js | 12 ++++++++++++ public/src/auth.js | 14 +++++++++++++- public/src/game.js | 26 +++++++++++++++++++------- public/src/main.js | 3 +++ public/style.css | 43 +++++++++++++++++++++++++++++++++++++++++-- server/db/gameDb.js | 14 ++++++++++++++ server/db/usersDb.js | 9 +++++++++ server/routes/auth.js | 13 ++++++++++++- server/routes/game.js | 13 +++++++++++++ 10 files changed, 154 insertions(+), 15 deletions(-) diff --git a/public/index.html b/public/index.html index 013a55d..4de37ea 100644 --- a/public/index.html +++ b/public/index.html @@ -48,11 +48,19 @@
@@ -78,7 +86,10 @@
- +
+ + 0 joueur +
@@ -133,7 +144,10 @@
- +
+ + 0 joueur +
diff --git a/public/src/api.js b/public/src/api.js index 1eff14e..d39d39f 100644 --- a/public/src/api.js +++ b/public/src/api.js @@ -99,3 +99,15 @@ export async function apiFetchVictoryPoints() { if (!res.ok) throw new Error("vp_fetch_failed"); return res.json(); } + +export async function apiFetchPlayerCounts() { + const res = await fetch("/api/auth/player-counts"); + if (!res.ok) throw new Error("player_counts_fetch_failed"); + return res.json(); +} + +export async function apiFetchActivePlayers() { + const res = await fetch("/api/active-players"); + if (!res.ok) throw new Error("active_players_fetch_failed"); + return res.json(); +} diff --git a/public/src/auth.js b/public/src/auth.js index 5d5aad5..abafd30 100644 --- a/public/src/auth.js +++ b/public/src/auth.js @@ -1,4 +1,4 @@ -import { apiLogin, apiRegister, apiGetMe } from "./api.js"; +import { apiLogin, apiRegister, apiGetMe, apiFetchPlayerCounts } from "./api.js"; import { setCurrentTeam, refreshFromServer } from "./game.js"; // ── DOM refs ────────────────────────────────────────────────────────────────── @@ -15,6 +15,8 @@ const regUsernameEl = document.getElementById("regUsername"); const regEmailEl = document.getElementById("regEmail"); const regPasswordEl = document.getElementById("regPassword"); const registerErrorEl = document.getElementById("registerError"); +const regCountBlueEl = document.getElementById("regCountBlue"); +const regCountRedEl = document.getElementById("regCountRed"); const userDisplayEl = document.getElementById("userDisplay"); const logoutBtn = document.getElementById("logoutBtn"); @@ -68,6 +70,15 @@ export async function tryRestoreSession() { // ── Tab switching ───────────────────────────────────────────────────────────── +async function loadRegisterCounts() { + try { + const counts = await apiFetchPlayerCounts(); + const fmt = (n) => `${n} joueur${n > 1 ? "s" : ""}`; + regCountBlueEl.textContent = fmt(counts.blue ?? 0); + regCountRedEl.textContent = fmt(counts.red ?? 0); + } catch { /* silently ignore */ } +} + tabLogin.addEventListener("click", () => { tabLogin.classList.add("authTab--active"); tabRegister.classList.remove("authTab--active"); @@ -82,6 +93,7 @@ tabRegister.addEventListener("click", () => { registerForm.classList.remove("hidden"); loginForm.classList.add("hidden"); clearError(registerErrorEl); + loadRegisterCounts(); }); // ── Login form ──────────────────────────────────────────────────────────────── diff --git a/public/src/game.js b/public/src/game.js index c007b66..e3e1b04 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, apiFetchScores, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints } from "./api.js"; +import { apiFetchConfig, apiFetchScores, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints, apiFetchActivePlayers } from "./api.js"; import { computeTeamIncome, computeTeamElementBonus, computeTeamElementBonusDetailed, renderResourceTable, renderElementBonusTable, setEconSort, getEconSort, setElemSort, getElemSort } from "./economy.js"; // ── Constants ───────────────────────────────────────────────────────────────── @@ -164,6 +164,8 @@ const econScoreRedEl = document.getElementById("econScoreRed"); const econDeltaBlueEl = document.getElementById("econDeltaBlue"); const econDeltaRedEl = document.getElementById("econDeltaRed"); const elemBonusTableEl = document.getElementById("elementBonusTableBody"); +const activeCountBlueEl = document.getElementById("activeCountBlue"); +const activeCountRedEl = document.getElementById("activeCountRed"); // ── Cell helpers ────────────────────────────────────────────────────────────── export function cellKey(x, y) { return `${x},${y}`; } @@ -243,6 +245,15 @@ export async function loadVictoryPoints() { } catch { /* ignore */ } } +export async function fetchAndApplyActivePlayers() { + try { + const { blue, red } = await apiFetchActivePlayers(); + const fmt = (n) => `${n} joueur${n > 1 ? "s" : ""}`; + if (activeCountBlueEl) activeCountBlueEl.textContent = fmt(blue ?? 0); + if (activeCountRedEl) activeCountRedEl.textContent = fmt(red ?? 0); + } catch { /* ignore */ } +} + // ── Element bonus ───────────────────────────────────────────────────────────── let elemBonusBlue = 0; @@ -576,16 +587,16 @@ function applyRevealPayload(cell) { details.classList.remove("details--hidden"); if (!cell.exploitable) { hint.textContent = `(${cell.x},${cell.y}) Inexploitable`; - details.textContent = `Cell (${cell.x},${cell.y})\n\nStatus : Inexploitable`; + details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : Inexploitable`; return; } if (!cell.hasPlanet) { hint.textContent = `(${cell.x},${cell.y}) Vide`; - details.textContent = `Cell (${cell.x},${cell.y})\n\nStatus : Vide`; + details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : Vide`; return; } hint.textContent = `(${cell.x},${cell.y}) Planète présente`; - details.textContent = `Cell (${cell.x},${cell.y})\n\nStatus : Planète\n\n${formatPlanet(cell.planet)}`; + details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : Planète\n\n${formatPlanet(cell.planet)}`; } function showLocalSelection(x, y) { @@ -594,13 +605,13 @@ function showLocalSelection(x, y) { if (!isOwnTile(k)) return; if (!isExploitable(x, y)) { hint.textContent = `(${x},${y}) Inexploitable`; - details.textContent = `Cell (${x},${y})\n\nStatus : Inexploitable`; + details.textContent = `Tuile (${x},${y})\n\nStatus : Inexploitable`; return; } const meta = cellMeta(k); if (!meta?.hasPlanet) { hint.textContent = `(${x},${y}) Vide`; - details.textContent = `Cell (${x},${y})\n\nStatus : Vide`; + details.textContent = `Tuile (${x},${y})\n\nStatus : Vide`; return; } let planet = meta.planet; @@ -609,7 +620,7 @@ function showLocalSelection(x, y) { planet = generatePlanet(mulberry32(h)); } hint.textContent = `(${x},${y}) Planète présente`; - details.textContent = `Cell (${x},${y})\n\nStatus : Planète\n\n${formatPlanet(planet)}`; + details.textContent = `Tuile (${x},${y})\n\nStatus : Planète\n\n${formatPlanet(planet)}`; } // ── Canvas click handler ────────────────────────────────────────────────────── @@ -684,6 +695,7 @@ export async function refreshFromServer() { } await fetchGridForSeed(seedStr); await fetchAndApplyScores(); + await fetchAndApplyActivePlayers(); updateEconomyDisplay(); draw(); refreshCursorFromLast(); diff --git a/public/src/main.js b/public/src/main.js index d77c9b6..cc6daac 100644 --- a/public/src/main.js +++ b/public/src/main.js @@ -5,6 +5,7 @@ import { fetchConfig, fetchGridForSeed, fetchAndApplyScores, + fetchAndApplyActivePlayers, updateEconomyDisplay, loadEconScores, loadVictoryPoints, @@ -54,6 +55,7 @@ function scheduleScorePoll() { clearTimeout(scorePollTimer); scorePollTimer = window.setTimeout(async () => { await fetchAndApplyScores(); + await fetchAndApplyActivePlayers(); await loadEconScores(); await loadElementBonus(); scheduleScorePoll(); @@ -101,6 +103,7 @@ async function boot() { await fetchConfig(); await fetchGridForSeed(seedStr); await fetchAndApplyScores(); + await fetchAndApplyActivePlayers(); await loadEconScores(); await loadVictoryPoints(); await loadDbInfo(); diff --git a/public/style.css b/public/style.css index ace5cf4..a88132a 100644 --- a/public/style.css +++ b/public/style.css @@ -134,16 +134,23 @@ body { } .authTeamBadge { - display: block; + display: flex; + align-items: center; + justify-content: space-between; padding: 10px; border-radius: 10px; border: 2px solid rgba(255, 255, 255, 0.1); - text-align: center; font-weight: 700; font-size: 13px; transition: border-color 0.15s, background 0.15s; } +.authTeamLogo { + width: 32px; + height: 32px; + flex-shrink: 0; +} + .authTeamBadge--blue { color: rgba(90, 200, 255, 0.9); } @@ -192,6 +199,18 @@ body { background: rgba(113, 199, 255, 0.28); } +.authTeamCount { + display: block; + text-align: center; + font-size: 10px; + font-weight: 600; + margin-top: 4px; + opacity: 0.7; +} + +.authTeamCount--blue { color: rgba(90, 200, 255, 0.9); } +.authTeamCount--red { color: rgba(220, 75, 85, 0.9); } + /* ── Score board ──────────────────────────────────────────────────────────── */ @@ -239,6 +258,26 @@ body { flex-shrink: 0; } +.teamLogoWrap { + display: flex; + flex-direction: column; + align-items: center; + gap: 4px; + flex-shrink: 0; +} + +.teamPlayerCount { + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.04em; + opacity: 0.75; + white-space: nowrap; +} + +.teamPlayerCount--blue { color: rgba(90, 200, 255, 0.9); } +.teamPlayerCount--red { color: rgba(220, 75, 85, 0.9); } + .scoreTeam { display: flex; flex-direction: column; diff --git a/server/db/gameDb.js b/server/db/gameDb.js index b49590c..229e40e 100644 --- a/server/db/gameDb.js +++ b/server/db/gameDb.js @@ -273,3 +273,17 @@ export async function getScores(worldSeed) { ); return rows; } + +// ── Active player counts (players who have played in the current epoch) ─────── + +export async function getActivePlayerCounts(worldSeed) { + const { rows } = await pool.query( + `SELECT team, COUNT(DISTINCT user_id)::int AS count + FROM user_cooldowns WHERE world_seed = $1 + GROUP BY team`, + [worldSeed] + ); + const result = { blue: 0, red: 0 }; + for (const row of rows) result[row.team] = row.count; + return result; +} diff --git a/server/db/usersDb.js b/server/db/usersDb.js index af22d3b..4e05b09 100644 --- a/server/db/usersDb.js +++ b/server/db/usersDb.js @@ -42,4 +42,13 @@ export async function getUserById(id) { [id] ); return rows[0] ?? null; +} + +export async function getTeamPlayerCounts() { + const { rows } = await usersPool.query( + `SELECT team, COUNT(*)::int AS count FROM users GROUP BY team` + ); + const result = { blue: 0, red: 0 }; + for (const row of rows) result[row.team] = row.count; + return result; } \ No newline at end of file diff --git a/server/routes/auth.js b/server/routes/auth.js index d34025e..8eeda35 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -2,7 +2,7 @@ import express from "express"; import bcrypt from "bcryptjs"; import jwt from "jsonwebtoken"; import { JWT_SECRET, authMiddleware } from "../middleware/auth.js"; -import { createUser, getUserByUsername, getUserById } from "../db/usersDb.js"; +import { createUser, getUserByUsername, getUserById, getTeamPlayerCounts } from "../db/usersDb.js"; const router = express.Router(); @@ -67,6 +67,17 @@ router.post("/login", async (req, res) => { } }); +// GET /api/auth/player-counts +router.get("/player-counts", async (_req, res) => { + try { + const counts = await getTeamPlayerCounts(); + return res.json(counts); + } catch (e) { + console.error(e); + return res.status(500).json({ error: "database_error" }); + } +}); + // GET /api/auth/me router.get("/me", authMiddleware, async (req, res) => { try { diff --git a/server/routes/game.js b/server/routes/game.js index 3d00337..d7b8d56 100644 --- a/server/routes/game.js +++ b/server/routes/game.js @@ -18,6 +18,7 @@ import { setElementBonus, getDbCreatedAt, getVictoryPoints, + getActivePlayerCounts, } from "../db/gameDb.js"; import { computeCell, rowToCellPayload } from "../helpers/cell.js"; @@ -246,6 +247,18 @@ router.get("/victory-points", async (_req, res) => { } }); +// GET /api/active-players +router.get("/active-players", async (_req, res) => { + try { + const worldSeed = await ensureSeedEpoch(); + const counts = await getActivePlayerCounts(worldSeed); + res.json(counts); + } catch (e) { + console.error(e); + res.status(500).json({ error: "database_error" }); + } +}); + // GET /api/scores router.get("/scores", async (_req, res) => { try {