Private
Public Access
1
0

refacto: Displaying number of players for each team + adding logo at registration

This commit is contained in:
gauvainboiche
2026-03-31 16:35:08 +02:00
parent fa4fec3a11
commit d1240adbb7
10 changed files with 154 additions and 15 deletions

View File

@@ -48,11 +48,19 @@
<div class="authTeamChoice"> <div class="authTeamChoice">
<label class="authTeamOption"> <label class="authTeamOption">
<input type="radio" name="regTeam" value="blue" required /> <input type="radio" name="regTeam" value="blue" required />
<span class="authTeamBadge authTeamBadge--blue">Résistance</span> <span class="authTeamBadge authTeamBadge--blue">
<img src="./graphism/logo_resistance.svg" class="authTeamLogo" alt="" />
Résistance
</span>
<span class="authTeamCount authTeamCount--blue" id="regCountBlue">… joueurs</span>
</label> </label>
<label class="authTeamOption"> <label class="authTeamOption">
<input type="radio" name="regTeam" value="red" /> <input type="radio" name="regTeam" value="red" />
<span class="authTeamBadge authTeamBadge--red">Premier ordre</span> <span class="authTeamBadge authTeamBadge--red">
Premier ordre
<img src="./graphism/logo_first_order.svg" class="authTeamLogo" alt="" />
</span>
<span class="authTeamCount authTeamCount--red" id="regCountRed">… joueurs</span>
</label> </label>
</div> </div>
</div> </div>
@@ -78,7 +86,10 @@
<!-- Team score display --> <!-- Team score display -->
<div class="scoreBoard" id="scoreBoard"> <div class="scoreBoard" id="scoreBoard">
<img src="./graphism/logo_resistance.svg" alt="Resistance" class="team-logo" /> <div class="teamLogoWrap">
<img src="./graphism/logo_resistance.svg" alt="Resistance" class="team-logo" />
<span class="teamPlayerCount teamPlayerCount--blue" id="activeCountBlue">0 joueur</span>
</div>
<div class="scoreBoardContent"> <div class="scoreBoardContent">
<div class="scoreBoardRow"> <div class="scoreBoardRow">
<div class="scoreTeam scoreTeam--blue"> <div class="scoreTeam scoreTeam--blue">
@@ -133,7 +144,10 @@
</div> </div>
</div> </div>
</div> </div>
<img src="./graphism/logo_first_order.svg" alt="First Order" class="team-logo" /> <div class="teamLogoWrap">
<img src="./graphism/logo_first_order.svg" alt="First Order" class="team-logo" />
<span class="teamPlayerCount teamPlayerCount--red" id="activeCountRed">0 joueur</span>
</div>
</div> </div>
<!-- Info rows --> <!-- Info rows -->

View File

@@ -99,3 +99,15 @@ export async function apiFetchVictoryPoints() {
if (!res.ok) throw new Error("vp_fetch_failed"); if (!res.ok) throw new Error("vp_fetch_failed");
return res.json(); 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();
}

View File

@@ -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"; import { setCurrentTeam, refreshFromServer } from "./game.js";
// ── DOM refs ────────────────────────────────────────────────────────────────── // ── DOM refs ──────────────────────────────────────────────────────────────────
@@ -15,6 +15,8 @@ const regUsernameEl = document.getElementById("regUsername");
const regEmailEl = document.getElementById("regEmail"); const regEmailEl = document.getElementById("regEmail");
const regPasswordEl = document.getElementById("regPassword"); const regPasswordEl = document.getElementById("regPassword");
const registerErrorEl = document.getElementById("registerError"); const registerErrorEl = document.getElementById("registerError");
const regCountBlueEl = document.getElementById("regCountBlue");
const regCountRedEl = document.getElementById("regCountRed");
const userDisplayEl = document.getElementById("userDisplay"); const userDisplayEl = document.getElementById("userDisplay");
const logoutBtn = document.getElementById("logoutBtn"); const logoutBtn = document.getElementById("logoutBtn");
@@ -68,6 +70,15 @@ export async function tryRestoreSession() {
// ── Tab switching ───────────────────────────────────────────────────────────── // ── 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.addEventListener("click", () => {
tabLogin.classList.add("authTab--active"); tabLogin.classList.add("authTab--active");
tabRegister.classList.remove("authTab--active"); tabRegister.classList.remove("authTab--active");
@@ -82,6 +93,7 @@ tabRegister.addEventListener("click", () => {
registerForm.classList.remove("hidden"); registerForm.classList.remove("hidden");
loginForm.classList.add("hidden"); loginForm.classList.add("hidden");
clearError(registerErrorEl); clearError(registerErrorEl);
loadRegisterCounts();
}); });
// ── Login form ──────────────────────────────────────────────────────────────── // ── Login form ────────────────────────────────────────────────────────────────

View File

@@ -1,6 +1,6 @@
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, 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"; import { computeTeamIncome, computeTeamElementBonus, computeTeamElementBonusDetailed, renderResourceTable, renderElementBonusTable, setEconSort, getEconSort, setElemSort, getElemSort } from "./economy.js";
// ── Constants ───────────────────────────────────────────────────────────────── // ── Constants ─────────────────────────────────────────────────────────────────
@@ -164,6 +164,8 @@ const econScoreRedEl = document.getElementById("econScoreRed");
const econDeltaBlueEl = document.getElementById("econDeltaBlue"); const econDeltaBlueEl = document.getElementById("econDeltaBlue");
const econDeltaRedEl = document.getElementById("econDeltaRed"); const econDeltaRedEl = document.getElementById("econDeltaRed");
const elemBonusTableEl = document.getElementById("elementBonusTableBody"); const elemBonusTableEl = document.getElementById("elementBonusTableBody");
const activeCountBlueEl = document.getElementById("activeCountBlue");
const activeCountRedEl = document.getElementById("activeCountRed");
// ── Cell helpers ────────────────────────────────────────────────────────────── // ── Cell helpers ──────────────────────────────────────────────────────────────
export function cellKey(x, y) { return `${x},${y}`; } export function cellKey(x, y) { return `${x},${y}`; }
@@ -243,6 +245,15 @@ export async function loadVictoryPoints() {
} catch { /* ignore */ } } 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 ───────────────────────────────────────────────────────────── // ── Element bonus ─────────────────────────────────────────────────────────────
let elemBonusBlue = 0; let elemBonusBlue = 0;
@@ -576,16 +587,16 @@ function applyRevealPayload(cell) {
details.classList.remove("details--hidden"); details.classList.remove("details--hidden");
if (!cell.exploitable) { if (!cell.exploitable) {
hint.textContent = `(${cell.x},${cell.y}) Inexploitable`; 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; return;
} }
if (!cell.hasPlanet) { if (!cell.hasPlanet) {
hint.textContent = `(${cell.x},${cell.y}) Vide`; 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; return;
} }
hint.textContent = `(${cell.x},${cell.y}) Planète présente`; 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) { function showLocalSelection(x, y) {
@@ -594,13 +605,13 @@ function showLocalSelection(x, y) {
if (!isOwnTile(k)) return; if (!isOwnTile(k)) return;
if (!isExploitable(x, y)) { if (!isExploitable(x, y)) {
hint.textContent = `(${x},${y}) Inexploitable`; hint.textContent = `(${x},${y}) Inexploitable`;
details.textContent = `Cell (${x},${y})\n\nStatus : Inexploitable`; details.textContent = `Tuile (${x},${y})\n\nStatus : Inexploitable`;
return; return;
} }
const meta = cellMeta(k); const meta = cellMeta(k);
if (!meta?.hasPlanet) { if (!meta?.hasPlanet) {
hint.textContent = `(${x},${y}) Vide`; hint.textContent = `(${x},${y}) Vide`;
details.textContent = `Cell (${x},${y})\n\nStatus : Vide`; details.textContent = `Tuile (${x},${y})\n\nStatus : Vide`;
return; return;
} }
let planet = meta.planet; let planet = meta.planet;
@@ -609,7 +620,7 @@ function showLocalSelection(x, y) {
planet = generatePlanet(mulberry32(h)); planet = generatePlanet(mulberry32(h));
} }
hint.textContent = `(${x},${y}) Planète présente`; 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 ────────────────────────────────────────────────────── // ── Canvas click handler ──────────────────────────────────────────────────────
@@ -684,6 +695,7 @@ export async function refreshFromServer() {
} }
await fetchGridForSeed(seedStr); await fetchGridForSeed(seedStr);
await fetchAndApplyScores(); await fetchAndApplyScores();
await fetchAndApplyActivePlayers();
updateEconomyDisplay(); updateEconomyDisplay();
draw(); draw();
refreshCursorFromLast(); refreshCursorFromLast();

View File

@@ -5,6 +5,7 @@ import {
fetchConfig, fetchConfig,
fetchGridForSeed, fetchGridForSeed,
fetchAndApplyScores, fetchAndApplyScores,
fetchAndApplyActivePlayers,
updateEconomyDisplay, updateEconomyDisplay,
loadEconScores, loadEconScores,
loadVictoryPoints, loadVictoryPoints,
@@ -54,6 +55,7 @@ function scheduleScorePoll() {
clearTimeout(scorePollTimer); clearTimeout(scorePollTimer);
scorePollTimer = window.setTimeout(async () => { scorePollTimer = window.setTimeout(async () => {
await fetchAndApplyScores(); await fetchAndApplyScores();
await fetchAndApplyActivePlayers();
await loadEconScores(); await loadEconScores();
await loadElementBonus(); await loadElementBonus();
scheduleScorePoll(); scheduleScorePoll();
@@ -101,6 +103,7 @@ async function boot() {
await fetchConfig(); await fetchConfig();
await fetchGridForSeed(seedStr); await fetchGridForSeed(seedStr);
await fetchAndApplyScores(); await fetchAndApplyScores();
await fetchAndApplyActivePlayers();
await loadEconScores(); await loadEconScores();
await loadVictoryPoints(); await loadVictoryPoints();
await loadDbInfo(); await loadDbInfo();

View File

@@ -134,16 +134,23 @@ body {
} }
.authTeamBadge { .authTeamBadge {
display: block; display: flex;
align-items: center;
justify-content: space-between;
padding: 10px; padding: 10px;
border-radius: 10px; border-radius: 10px;
border: 2px solid rgba(255, 255, 255, 0.1); border: 2px solid rgba(255, 255, 255, 0.1);
text-align: center;
font-weight: 700; font-weight: 700;
font-size: 13px; font-size: 13px;
transition: border-color 0.15s, background 0.15s; transition: border-color 0.15s, background 0.15s;
} }
.authTeamLogo {
width: 32px;
height: 32px;
flex-shrink: 0;
}
.authTeamBadge--blue { .authTeamBadge--blue {
color: rgba(90, 200, 255, 0.9); color: rgba(90, 200, 255, 0.9);
} }
@@ -192,6 +199,18 @@ body {
background: rgba(113, 199, 255, 0.28); 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 ──────────────────────────────────────────────────────────── */ /* ── Score board ──────────────────────────────────────────────────────────── */
@@ -239,6 +258,26 @@ body {
flex-shrink: 0; 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 { .scoreTeam {
display: flex; display: flex;
flex-direction: column; flex-direction: column;

View File

@@ -273,3 +273,17 @@ export async function getScores(worldSeed) {
); );
return rows; 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;
}

View File

@@ -42,4 +42,13 @@ export async function getUserById(id) {
[id] [id]
); );
return rows[0] ?? null; 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;
} }

View File

@@ -2,7 +2,7 @@ import express from "express";
import bcrypt from "bcryptjs"; import bcrypt from "bcryptjs";
import jwt from "jsonwebtoken"; import jwt from "jsonwebtoken";
import { JWT_SECRET, authMiddleware } from "../middleware/auth.js"; 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(); 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 // GET /api/auth/me
router.get("/me", authMiddleware, async (req, res) => { router.get("/me", authMiddleware, async (req, res) => {
try { try {

View File

@@ -18,6 +18,7 @@ import {
setElementBonus, setElementBonus,
getDbCreatedAt, getDbCreatedAt,
getVictoryPoints, getVictoryPoints,
getActivePlayerCounts,
} from "../db/gameDb.js"; } from "../db/gameDb.js";
import { computeCell, rowToCellPayload } from "../helpers/cell.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 // GET /api/scores
router.get("/scores", async (_req, res) => { router.get("/scores", async (_req, res) => {
try { try {