Private
Public Access
1
0

feat: Adding the name of capturers of planets + displaying usernames for each team

This commit is contained in:
gauvainboiche
2026-04-01 17:46:48 +02:00
parent 362aa07f5a
commit e28a2d6e9c
8 changed files with 217 additions and 10 deletions

View File

@@ -187,6 +187,7 @@
<details class="panel panelCollapsible" id="planetStatsDetails" open>
<summary class="panelTitle panelTitleSummary">🪐 Statistiques Planétaires</summary>
<pre id="details" class="details details--hidden">Les stats sont vides sauf à cliquer sur une tuile exploitable.</pre>
<div id="captorInfo" class="captorInfo hidden"></div>
</details>
<!-- Resources overview (collapsible) -->
@@ -286,6 +287,11 @@
</aside>
<!-- Player list popup (shown on click of joueur count) -->
<div id="playerListPopup" class="playerListPopup hidden" role="tooltip">
<div id="playerListContent"></div>
</div>
<!-- ── Galaxy (square, 1000×1000, fixed ratio) ────────────────────────── -->
<main class="galaxyMain">
<!-- Mobile burger button -->

View File

@@ -108,6 +108,12 @@ export async function apiFetchActivePlayers() {
return res.json();
}
export async function apiFetchActivePlayerNames() {
const res = await fetch("/api/active-players/names");
if (!res.ok) throw new Error("active_player_names_fetch_failed");
return res.json();
}
export async function apiFetchMilitaryDeductions() {
const res = await fetch("/api/military-deductions");
if (!res.ok) throw new Error("military_deductions_fetch_failed");

View File

@@ -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, apiFetchMilitaryDeductions, apiMilitaryAttack, apiCaptureCell } from "./api.js";
import { apiFetchConfig, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints, apiFetchActivePlayers, apiFetchActivePlayerNames, apiFetchMilitaryDeductions, apiMilitaryAttack, apiCaptureCell } from "./api.js";
import { computeTeamIncome, computeTeamElementBonus, computeTeamElementBonusDetailed, renderResourceTable, renderElementBonusTable, setEconSort, getEconSort, setElemSort, getElemSort, computeTeamMilitaryDetailed, renderMilitaryTable, setMilSort, getMilSort } from "./economy.js";
// ── Constants ─────────────────────────────────────────────────────────────────
@@ -234,6 +234,9 @@ const captureModalBodyEl = document.getElementById("captureModalBody");
const captureModalYesEl = document.getElementById("captureModalYes");
const captureModalNoEl = document.getElementById("captureModalNo");
const teamQuotaEl = document.getElementById("teamActionsRemaining");
const captorInfoEl = document.getElementById("captorInfo");
const playerListPopupEl = document.getElementById("playerListPopup");
const playerListContentEl = document.getElementById("playerListContent");
// ── Cell helpers ──────────────────────────────────────────────────────────────
export function cellKey(x, y) { return `${x},${y}`; }
@@ -325,6 +328,64 @@ export async function fetchAndApplyActivePlayers() {
} catch { /* ignore */ }
}
// ── Player list popup (click on joueur count) ─────────────────────────────────
function closePlayerListPopup() {
if (playerListPopupEl) playerListPopupEl.classList.add("hidden");
}
async function openPlayerListPopup(anchorEl, team) {
if (!playerListPopupEl || !playerListContentEl) return;
try {
const names = await apiFetchActivePlayerNames();
const list = names[team] ?? [];
if (!list.length) {
playerListContentEl.innerHTML = `<span class="playerListEmpty">Aucun joueur actif</span>`;
} else {
const teamClass = `playerListName--${team}`;
playerListContentEl.innerHTML =
list.map(u => `<div class="${escHtml(teamClass)}">${escHtml(u)}</div>`).join("");
}
// Position popup below the anchor
const rect = anchorEl.getBoundingClientRect();
const parentRect = anchorEl.closest(".infoColumn, aside")?.getBoundingClientRect() ?? { left: 0, top: 0 };
playerListPopupEl.style.left = `${rect.left - parentRect.left}px`;
playerListPopupEl.style.top = `${rect.bottom - parentRect.top + 4}px`;
playerListPopupEl.classList.remove("hidden");
playerListPopupEl.dataset.team = team;
} catch { /* ignore */ }
}
if (activeCountBlueEl) {
activeCountBlueEl.style.cursor = "pointer";
activeCountBlueEl.addEventListener("click", (ev) => {
ev.stopPropagation();
if (playerListPopupEl && !playerListPopupEl.classList.contains("hidden") && playerListPopupEl.dataset.team === "blue") {
closePlayerListPopup();
} else {
openPlayerListPopup(activeCountBlueEl, "blue");
}
});
}
if (activeCountRedEl) {
activeCountRedEl.style.cursor = "pointer";
activeCountRedEl.addEventListener("click", (ev) => {
ev.stopPropagation();
if (playerListPopupEl && !playerListPopupEl.classList.contains("hidden") && playerListPopupEl.dataset.team === "red") {
closePlayerListPopup();
} else {
openPlayerListPopup(activeCountRedEl, "red");
}
});
}
document.addEventListener("click", (ev) => {
if (playerListPopupEl && !playerListPopupEl.contains(ev.target)) {
closePlayerListPopup();
}
});
// ── Element bonus ─────────────────────────────────────────────────────────────
let elemBonusBlue = 0;
@@ -534,6 +595,7 @@ export async function fetchGridForSeed(seed, depth = 0) {
controlledBy: row.discovered_by ?? row.discoveredBy ?? null,
hasPlanet: Boolean(row.has_planet),
planet: row.planet_json ?? null,
capturedBy: row.captured_by ?? row.capturedBy ?? null,
});
}
}
@@ -696,6 +758,20 @@ function refreshCursorFromLast() {
// ── Selection display ─────────────────────────────────────────────────────────
/** Escapes a string for safe HTML insertion. */
function escHtml(str) {
return String(str).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
}
/** Shows or hides the captor info banner below planet stats. */
function showCaptorInfo(capturedBy, team) {
if (!captorInfoEl) return;
if (!capturedBy) { captorInfoEl.className = "captorInfo hidden"; return; }
const teamClass = team === "blue" ? "captorInfo--blue" : team === "red" ? "captorInfo--red" : "";
captorInfoEl.className = `captorInfo ${teamClass}`;
captorInfoEl.innerHTML = `Capturée par <strong>${escHtml(capturedBy)}</strong>`;
}
function applyRevealPayload(cell) {
const _revealKey = cellKey(cell.x, cell.y);
markTileReveal(_revealKey);
@@ -703,16 +779,19 @@ function applyRevealPayload(cell) {
controlledBy: cell.discoveredBy ?? null,
hasPlanet: Boolean(cell.hasPlanet),
planet: cell.planet ?? null,
capturedBy: cell.capturedBy ?? null,
});
details.classList.remove("details--hidden");
if (!cell.exploitable) {
hint.textContent = `(${cell.x},${cell.y}) Inexploitable`;
details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : Inexploitable`;
showCaptorInfo(null, null);
return;
}
if (!cell.hasPlanet) {
hint.textContent = `(${cell.x},${cell.y}) Vide`;
details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : Vide`;
showCaptorInfo(null, null);
return;
}
const controlStatus = cell.discoveredBy === null
@@ -722,6 +801,7 @@ function applyRevealPayload(cell) {
: `Contrôlée par l'adversaire`;
hint.textContent = `(${cell.x},${cell.y}) Planète présente`;
details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : ${controlStatus}\n\n${formatPlanet(cell.planet)}`;
showCaptorInfo(cell.capturedBy ?? null, cell.discoveredBy);
}
function showLocalSelection(x, y) {
@@ -732,11 +812,13 @@ function showLocalSelection(x, y) {
if (!isExploitable(x, y)) {
hint.textContent = `(${x},${y}) Inexploitable`;
details.textContent = `Tuile (${x},${y})\n\nStatus : Inexploitable`;
showCaptorInfo(null, null);
return;
}
if (!meta.hasPlanet) {
hint.textContent = `(${x},${y}) Vide`;
details.textContent = `Tuile (${x},${y})\n\nStatus : Vide`;
showCaptorInfo(null, null);
return;
}
let planet = meta.planet;
@@ -751,6 +833,7 @@ function showLocalSelection(x, y) {
: `Contrôlée par l'adversaire`;
hint.textContent = `(${x},${y}) Planète présente`;
details.textContent = `Tuile (${x},${y})\n\nStatus : ${controlStatus}\n\n${formatPlanet(planet)}`;
showCaptorInfo(meta.capturedBy ?? null, meta.controlledBy);
}
// ── Military attack modal ────────────────────────────────────────────────────
@@ -896,7 +979,7 @@ async function onCanvasClick(ev) {
if (!res.ok) { hint.textContent = "Erreur lors de la capture."; return; }
const data = await res.json();
markTileReveal(key);
cells.set(key, { ...meta, controlledBy: currentTeam });
cells.set(key, { ...meta, controlledBy: currentTeam, capturedBy: data.cell?.capturedBy ?? null });
if (data.teamActionsRemaining !== undefined && data.teamActionsRemaining !== null) {
teamActionsRemaining = data.teamActionsRemaining;
updateTeamQuotaDisplay();

View File

@@ -1231,4 +1231,68 @@ canvas {
.scoreStatVal {
font-size: 16px;
}
}
/* ── Captor info banner ────────────────────────────────────────────────────── */
.captorInfo {
padding: 6px 14px 10px;
font-family: "Courier New", Courier, monospace;
font-size: 11px;
opacity: 0.85;
}
.captorInfo.hidden {
display: none;
}
.captorInfo--blue {
color: rgba(90, 200, 255, 0.9);
}
.captorInfo--red {
color: rgba(220, 75, 85, 0.9);
}
/* ── Player list popup ─────────────────────────────────────────────────────── */
.playerListPopup {
position: absolute;
z-index: 30;
min-width: 120px;
max-width: 200px;
border-radius: 10px;
border: 1px solid rgba(255, 255, 255, 0.13);
background: rgba(12, 18, 38, 0.97);
backdrop-filter: blur(8px);
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.6);
padding: 8px 10px;
font-size: 11px;
font-family: system-ui, -apple-system, Segoe UI, Roboto, Ubuntu, Cantarell, "Helvetica Neue", Arial, sans-serif;
pointer-events: auto;
}
.playerListPopup.hidden {
display: none;
}
.playerListName--blue {
color: rgba(90, 200, 255, 0.9);
padding: 2px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.playerListName--red {
color: rgba(220, 75, 85, 0.9);
padding: 2px 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.playerListEmpty {
color: rgba(233, 238, 246, 0.4);
font-style: italic;
}