feat: Adding the name of capturers of planets + displaying usernames for each team
This commit is contained in:
@@ -187,6 +187,7 @@
|
|||||||
<details class="panel panelCollapsible" id="planetStatsDetails" open>
|
<details class="panel panelCollapsible" id="planetStatsDetails" open>
|
||||||
<summary class="panelTitle panelTitleSummary">🪐 Statistiques Planétaires</summary>
|
<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>
|
<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>
|
</details>
|
||||||
|
|
||||||
<!-- Resources overview (collapsible) -->
|
<!-- Resources overview (collapsible) -->
|
||||||
@@ -286,6 +287,11 @@
|
|||||||
|
|
||||||
</aside>
|
</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) ────────────────────────── -->
|
<!-- ── Galaxy (square, 1000×1000, fixed ratio) ────────────────────────── -->
|
||||||
<main class="galaxyMain">
|
<main class="galaxyMain">
|
||||||
<!-- Mobile burger button -->
|
<!-- Mobile burger button -->
|
||||||
|
|||||||
@@ -108,6 +108,12 @@ export async function apiFetchActivePlayers() {
|
|||||||
return res.json();
|
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() {
|
export async function apiFetchMilitaryDeductions() {
|
||||||
const res = await fetch("/api/military-deductions");
|
const res = await fetch("/api/military-deductions");
|
||||||
if (!res.ok) throw new Error("military_deductions_fetch_failed");
|
if (!res.ok) throw new Error("military_deductions_fetch_failed");
|
||||||
|
|||||||
@@ -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, 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";
|
import { computeTeamIncome, computeTeamElementBonus, computeTeamElementBonusDetailed, renderResourceTable, renderElementBonusTable, setEconSort, getEconSort, setElemSort, getElemSort, computeTeamMilitaryDetailed, renderMilitaryTable, setMilSort, getMilSort } from "./economy.js";
|
||||||
|
|
||||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||||
@@ -234,6 +234,9 @@ const captureModalBodyEl = document.getElementById("captureModalBody");
|
|||||||
const captureModalYesEl = document.getElementById("captureModalYes");
|
const captureModalYesEl = document.getElementById("captureModalYes");
|
||||||
const captureModalNoEl = document.getElementById("captureModalNo");
|
const captureModalNoEl = document.getElementById("captureModalNo");
|
||||||
const teamQuotaEl = document.getElementById("teamActionsRemaining");
|
const teamQuotaEl = document.getElementById("teamActionsRemaining");
|
||||||
|
const captorInfoEl = document.getElementById("captorInfo");
|
||||||
|
const playerListPopupEl = document.getElementById("playerListPopup");
|
||||||
|
const playerListContentEl = document.getElementById("playerListContent");
|
||||||
// ── Cell helpers ──────────────────────────────────────────────────────────────
|
// ── Cell helpers ──────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export function cellKey(x, y) { return `${x},${y}`; }
|
export function cellKey(x, y) { return `${x},${y}`; }
|
||||||
@@ -325,6 +328,64 @@ export async function fetchAndApplyActivePlayers() {
|
|||||||
} catch { /* ignore */ }
|
} 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 ─────────────────────────────────────────────────────────────
|
// ── Element bonus ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
let elemBonusBlue = 0;
|
let elemBonusBlue = 0;
|
||||||
@@ -534,6 +595,7 @@ export async function fetchGridForSeed(seed, depth = 0) {
|
|||||||
controlledBy: row.discovered_by ?? row.discoveredBy ?? null,
|
controlledBy: row.discovered_by ?? row.discoveredBy ?? null,
|
||||||
hasPlanet: Boolean(row.has_planet),
|
hasPlanet: Boolean(row.has_planet),
|
||||||
planet: row.planet_json ?? null,
|
planet: row.planet_json ?? null,
|
||||||
|
capturedBy: row.captured_by ?? row.capturedBy ?? null,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -696,6 +758,20 @@ function refreshCursorFromLast() {
|
|||||||
|
|
||||||
// ── Selection display ─────────────────────────────────────────────────────────
|
// ── Selection display ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Escapes a string for safe HTML insertion. */
|
||||||
|
function escHtml(str) {
|
||||||
|
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** 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) {
|
function applyRevealPayload(cell) {
|
||||||
const _revealKey = cellKey(cell.x, cell.y);
|
const _revealKey = cellKey(cell.x, cell.y);
|
||||||
markTileReveal(_revealKey);
|
markTileReveal(_revealKey);
|
||||||
@@ -703,16 +779,19 @@ function applyRevealPayload(cell) {
|
|||||||
controlledBy: cell.discoveredBy ?? null,
|
controlledBy: cell.discoveredBy ?? null,
|
||||||
hasPlanet: Boolean(cell.hasPlanet),
|
hasPlanet: Boolean(cell.hasPlanet),
|
||||||
planet: cell.planet ?? null,
|
planet: cell.planet ?? null,
|
||||||
|
capturedBy: cell.capturedBy ?? null,
|
||||||
});
|
});
|
||||||
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 = `Tuile (${cell.x},${cell.y})\n\nStatus : Inexploitable`;
|
details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : Inexploitable`;
|
||||||
|
showCaptorInfo(null, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!cell.hasPlanet) {
|
if (!cell.hasPlanet) {
|
||||||
hint.textContent = `(${cell.x},${cell.y}) Vide`;
|
hint.textContent = `(${cell.x},${cell.y}) Vide`;
|
||||||
details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : Vide`;
|
details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : Vide`;
|
||||||
|
showCaptorInfo(null, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const controlStatus = cell.discoveredBy === null
|
const controlStatus = cell.discoveredBy === null
|
||||||
@@ -722,6 +801,7 @@ function applyRevealPayload(cell) {
|
|||||||
: `Contrôlée par l'adversaire`;
|
: `Contrôlée par l'adversaire`;
|
||||||
hint.textContent = `(${cell.x},${cell.y}) Planète présente`;
|
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)}`;
|
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) {
|
function showLocalSelection(x, y) {
|
||||||
@@ -732,11 +812,13 @@ function showLocalSelection(x, y) {
|
|||||||
if (!isExploitable(x, y)) {
|
if (!isExploitable(x, y)) {
|
||||||
hint.textContent = `(${x},${y}) Inexploitable`;
|
hint.textContent = `(${x},${y}) Inexploitable`;
|
||||||
details.textContent = `Tuile (${x},${y})\n\nStatus : Inexploitable`;
|
details.textContent = `Tuile (${x},${y})\n\nStatus : Inexploitable`;
|
||||||
|
showCaptorInfo(null, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (!meta.hasPlanet) {
|
if (!meta.hasPlanet) {
|
||||||
hint.textContent = `(${x},${y}) Vide`;
|
hint.textContent = `(${x},${y}) Vide`;
|
||||||
details.textContent = `Tuile (${x},${y})\n\nStatus : Vide`;
|
details.textContent = `Tuile (${x},${y})\n\nStatus : Vide`;
|
||||||
|
showCaptorInfo(null, null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let planet = meta.planet;
|
let planet = meta.planet;
|
||||||
@@ -751,6 +833,7 @@ function showLocalSelection(x, y) {
|
|||||||
: `Contrôlée par l'adversaire`;
|
: `Contrôlée par l'adversaire`;
|
||||||
hint.textContent = `(${x},${y}) Planète présente`;
|
hint.textContent = `(${x},${y}) Planète présente`;
|
||||||
details.textContent = `Tuile (${x},${y})\n\nStatus : ${controlStatus}\n\n${formatPlanet(planet)}`;
|
details.textContent = `Tuile (${x},${y})\n\nStatus : ${controlStatus}\n\n${formatPlanet(planet)}`;
|
||||||
|
showCaptorInfo(meta.capturedBy ?? null, meta.controlledBy);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Military attack modal ────────────────────────────────────────────────────
|
// ── Military attack modal ────────────────────────────────────────────────────
|
||||||
@@ -896,7 +979,7 @@ async function onCanvasClick(ev) {
|
|||||||
if (!res.ok) { hint.textContent = "Erreur lors de la capture."; return; }
|
if (!res.ok) { hint.textContent = "Erreur lors de la capture."; return; }
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
markTileReveal(key);
|
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) {
|
if (data.teamActionsRemaining !== undefined && data.teamActionsRemaining !== null) {
|
||||||
teamActionsRemaining = data.teamActionsRemaining;
|
teamActionsRemaining = data.teamActionsRemaining;
|
||||||
updateTeamQuotaDisplay();
|
updateTeamQuotaDisplay();
|
||||||
|
|||||||
@@ -1231,4 +1231,68 @@ canvas {
|
|||||||
.scoreStatVal {
|
.scoreStatVal {
|
||||||
font-size: 16px;
|
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;
|
||||||
}
|
}
|
||||||
@@ -140,6 +140,10 @@ export async function initGameSchema() {
|
|||||||
ALTER TABLE grid_cells ADD CONSTRAINT grid_cells_discovered_by_check
|
ALTER TABLE grid_cells ADD CONSTRAINT grid_cells_discovered_by_check
|
||||||
CHECK (discovered_by IS NULL OR discovered_by IN ('blue', 'red'));
|
CHECK (discovered_by IS NULL OR discovered_by IN ('blue', 'red'));
|
||||||
`);
|
`);
|
||||||
|
// ── Store the username of whoever last captured a tile ────────────────────
|
||||||
|
await pool.query(`
|
||||||
|
ALTER TABLE grid_cells ADD COLUMN IF NOT EXISTS captured_by TEXT;
|
||||||
|
`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── World-seed epoch ──────────────────────────────────────────────────────────
|
// ── World-seed epoch ──────────────────────────────────────────────────────────
|
||||||
@@ -203,7 +207,7 @@ export async function ensureSeedEpoch() {
|
|||||||
|
|
||||||
export async function getGridCells(worldSeed) {
|
export async function getGridCells(worldSeed) {
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`SELECT x, y, exploitable, has_planet, planet_json, discovered_by
|
`SELECT x, y, exploitable, has_planet, planet_json, discovered_by, captured_by
|
||||||
FROM grid_cells WHERE world_seed = $1`,
|
FROM grid_cells WHERE world_seed = $1`,
|
||||||
[worldSeed]
|
[worldSeed]
|
||||||
);
|
);
|
||||||
@@ -246,7 +250,7 @@ export async function insertTeamVisibility(worldSeed, team, x, y) {
|
|||||||
/** Returns all grid cells that are visible to a given team. */
|
/** Returns all grid cells that are visible to a given team. */
|
||||||
export async function getTeamVisibleCells(worldSeed, team) {
|
export async function getTeamVisibleCells(worldSeed, team) {
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`SELECT gc.x, gc.y, gc.exploitable, gc.has_planet, gc.planet_json, gc.discovered_by
|
`SELECT gc.x, gc.y, gc.exploitable, gc.has_planet, gc.planet_json, gc.discovered_by, gc.captured_by
|
||||||
FROM grid_cells gc
|
FROM grid_cells gc
|
||||||
JOIN team_cell_visibility tcv
|
JOIN team_cell_visibility tcv
|
||||||
ON tcv.world_seed = gc.world_seed AND tcv.x = gc.x AND tcv.y = gc.y
|
ON tcv.world_seed = gc.world_seed AND tcv.x = gc.x AND tcv.y = gc.y
|
||||||
@@ -258,7 +262,7 @@ export async function getTeamVisibleCells(worldSeed, team) {
|
|||||||
|
|
||||||
export async function getExistingCell(seed, x, y) {
|
export async function getExistingCell(seed, x, y) {
|
||||||
const { rows } = await pool.query(
|
const { rows } = await pool.query(
|
||||||
`SELECT x, y, exploitable, has_planet, planet_json, discovered_by
|
`SELECT x, y, exploitable, has_planet, planet_json, discovered_by, captured_by
|
||||||
FROM grid_cells WHERE world_seed = $1 AND x = $2 AND y = $3`,
|
FROM grid_cells WHERE world_seed = $1 AND x = $2 AND y = $3`,
|
||||||
[seed, x, y]
|
[seed, x, y]
|
||||||
);
|
);
|
||||||
@@ -431,13 +435,26 @@ export async function getCellAttackCount(worldSeed, x, y) {
|
|||||||
return rows[0]?.cnt ?? 0;
|
return rows[0]?.cnt ?? 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function setTileOwner(worldSeed, x, y, team) {
|
export async function setTileOwner(worldSeed, x, y, team, capturedBy = null) {
|
||||||
await pool.query(
|
await pool.query(
|
||||||
`UPDATE grid_cells SET discovered_by = $1 WHERE world_seed = $2 AND x = $3 AND y = $4`,
|
`UPDATE grid_cells SET discovered_by = $1, captured_by = $2 WHERE world_seed = $3 AND x = $4 AND y = $5`,
|
||||||
[team, worldSeed, x, y]
|
[team, capturedBy, worldSeed, x, y]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns per-team lists of user IDs who have been active this seed epoch. */
|
||||||
|
export async function getActivePlayerIds(worldSeed) {
|
||||||
|
const { rows } = await pool.query(
|
||||||
|
`SELECT team, user_id FROM user_cooldowns WHERE world_seed = $1`,
|
||||||
|
[worldSeed]
|
||||||
|
);
|
||||||
|
const result = { blue: [], red: [] };
|
||||||
|
for (const row of rows) {
|
||||||
|
if (result[row.team]) result[row.team].push(row.user_id);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
// ── Team action quota (daily, independent of world seed) ─────────────────────
|
// ── Team action quota (daily, independent of world seed) ─────────────────────
|
||||||
|
|
||||||
export async function getTeamActionsRow(team) {
|
export async function getTeamActionsRow(team) {
|
||||||
|
|||||||
@@ -75,6 +75,16 @@ export async function getTeamPlayerCounts() {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Returns username and team for each of the given user IDs. */
|
||||||
|
export async function getUsersByIds(ids) {
|
||||||
|
if (!ids.length) return [];
|
||||||
|
const { rows } = await usersPool.query(
|
||||||
|
`SELECT id, username, team FROM users WHERE id = ANY($1::int[])`,
|
||||||
|
[ids]
|
||||||
|
);
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
// ── User action quota ─────────────────────────────────────────────────────────
|
// ── User action quota ─────────────────────────────────────────────────────────
|
||||||
|
|
||||||
/** Returns the current quota row for a user, or null if it doesn't exist. */
|
/** Returns the current quota row for a user, or null if it doesn't exist. */
|
||||||
|
|||||||
@@ -51,5 +51,6 @@ export function rowToCellPayload(row) {
|
|||||||
hasPlanet: row.has_planet,
|
hasPlanet: row.has_planet,
|
||||||
planet: row.planet_json ?? null,
|
planet: row.planet_json ?? null,
|
||||||
discoveredBy: row.discovered_by,
|
discoveredBy: row.discovered_by,
|
||||||
|
capturedBy: row.captured_by ?? null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
getDbCreatedAt,
|
getDbCreatedAt,
|
||||||
getVictoryPoints,
|
getVictoryPoints,
|
||||||
getActivePlayerCounts,
|
getActivePlayerCounts,
|
||||||
|
getActivePlayerIds,
|
||||||
getMilitaryDeductions,
|
getMilitaryDeductions,
|
||||||
addMilitaryDeduction,
|
addMilitaryDeduction,
|
||||||
recordCellAttack,
|
recordCellAttack,
|
||||||
@@ -37,6 +38,7 @@ import {
|
|||||||
getUserActionsRow,
|
getUserActionsRow,
|
||||||
resetUserActions,
|
resetUserActions,
|
||||||
decrementUserActions,
|
decrementUserActions,
|
||||||
|
getUsersByIds,
|
||||||
} from "../db/usersDb.js";
|
} from "../db/usersDb.js";
|
||||||
import { computeCell, rowToCellPayload } from "../helpers/cell.js";
|
import { computeCell, rowToCellPayload } from "../helpers/cell.js";
|
||||||
import { computeTeamMilitaryPower } from "../helpers/economy.js";
|
import { computeTeamMilitaryPower } from "../helpers/economy.js";
|
||||||
@@ -252,7 +254,7 @@ router.post("/cell/capture", authMiddleware, async (req, res) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Transfer ownership to capturing team
|
// Transfer ownership to capturing team
|
||||||
await setTileOwner(worldSeed, x, y, team);
|
await setTileOwner(worldSeed, x, y, team, req.user.username);
|
||||||
|
|
||||||
const updatedCell = await getExistingCell(worldSeed, x, y);
|
const updatedCell = await getExistingCell(worldSeed, x, y);
|
||||||
const updatedTeamRow = await getTeamActionsRow(team);
|
const updatedTeamRow = await getTeamActionsRow(team);
|
||||||
@@ -391,6 +393,24 @@ router.get("/active-players", async (_req, res) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// GET /api/active-players/names
|
||||||
|
router.get("/active-players/names", async (_req, res) => {
|
||||||
|
try {
|
||||||
|
const worldSeed = await ensureSeedEpoch();
|
||||||
|
const playerIds = await getActivePlayerIds(worldSeed);
|
||||||
|
const allIds = [...playerIds.blue, ...playerIds.red];
|
||||||
|
const users = await getUsersByIds(allIds);
|
||||||
|
const result = { blue: [], red: [] };
|
||||||
|
for (const user of users) {
|
||||||
|
if (result[user.team]) result[user.team].push(user.username);
|
||||||
|
}
|
||||||
|
res.json(result);
|
||||||
|
} 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 {
|
||||||
@@ -449,7 +469,7 @@ router.post("/military/attack", authMiddleware, async (req, res) => {
|
|||||||
await addMilitaryDeduction(worldSeed, attackingTeam, COST_BILLIONS);
|
await addMilitaryDeduction(worldSeed, attackingTeam, COST_BILLIONS);
|
||||||
|
|
||||||
// Transfer tile ownership to the attacking team
|
// Transfer tile ownership to the attacking team
|
||||||
await setTileOwner(worldSeed, x, y, attackingTeam);
|
await setTileOwner(worldSeed, x, y, attackingTeam, req.user.username);
|
||||||
|
|
||||||
// Record the attack event
|
// Record the attack event
|
||||||
await recordCellAttack(worldSeed, x, y, attackingTeam);
|
await recordCellAttack(worldSeed, x, y, attackingTeam);
|
||||||
|
|||||||
Reference in New Issue
Block a user