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">
<label class="authTeamOption">
<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 class="authTeamOption">
<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>
</div>
</div>
@@ -78,7 +86,10 @@
<!-- Team score display -->
<div class="scoreBoard" id="scoreBoard">
<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="scoreBoardRow">
<div class="scoreTeam scoreTeam--blue">
@@ -133,7 +144,10 @@
</div>
</div>
</div>
<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>
<!-- Info rows -->

View File

@@ -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();
}

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";
// ── 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 ────────────────────────────────────────────────────────────────

View File

@@ -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();

View File

@@ -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();

View File

@@ -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;

View File

@@ -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;
}

View File

@@ -43,3 +43,12 @@ export async function getUserById(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;
}

View File

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

View File

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