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
+
-

+
+

+
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 {