From 8fabeb13b193f3bdbb0baaff550350827eea95ed Mon Sep 17 00:00:00 2001 From: gauvainboiche Date: Tue, 31 Mar 2026 10:36:25 +0200 Subject: [PATCH] refacto: Cooldown is now user-set --- public/src/api.js | 10 ++++++++-- server/db/gameDb.js | 29 +++++++++++++++++++++++++++++ server/routes/game.js | 42 +++++++++++++++++++++++++++++------------- 3 files changed, 66 insertions(+), 15 deletions(-) diff --git a/public/src/api.js b/public/src/api.js index 8b17cb2..1eff14e 100644 --- a/public/src/api.js +++ b/public/src/api.js @@ -3,7 +3,9 @@ // parsed JSON. No state mutations, no DOM access. export async function apiFetchConfig(team) { - const res = await fetch(`/api/config?team=${encodeURIComponent(team)}`); + const token = localStorage.getItem("authToken"); + const headers = token ? { Authorization: `Bearer ${token}` } : {}; + const res = await fetch(`/api/config?team=${encodeURIComponent(team)}`, { headers }); if (!res.ok) throw new Error("config_fetch_failed"); return res.json(); } @@ -21,9 +23,13 @@ export async function apiFetchGrid(seed) { /** Returns the raw Response so the caller can inspect status codes (409, 410, etc.). */ export async function apiRevealCell(seed, x, y, team) { + const token = localStorage.getItem("authToken"); return fetch("/api/cell/reveal", { method: "POST", - headers: { "Content-Type": "application/json" }, + headers: { + "Content-Type": "application/json", + ...(token ? { Authorization: `Bearer ${token}` } : {}), + }, body: JSON.stringify({ seed, x, y, team }), }); } diff --git a/server/db/gameDb.js b/server/db/gameDb.js index 602348a..b49590c 100644 --- a/server/db/gameDb.js +++ b/server/db/gameDb.js @@ -29,6 +29,15 @@ export async function initGameSchema() { PRIMARY KEY (world_seed, team) ); `); + await pool.query(` + CREATE TABLE IF NOT EXISTS user_cooldowns ( + world_seed TEXT NOT NULL, + user_id INTEGER NOT NULL, + team TEXT NOT NULL CHECK (team IN ('blue', 'red')), + last_reveal TIMESTAMPTZ, + PRIMARY KEY (world_seed, user_id) + ); + `); await pool.query(` CREATE TABLE IF NOT EXISTS team_econ_scores ( world_seed TEXT NOT NULL, @@ -112,6 +121,7 @@ export async function ensureSeedEpoch() { } await pool.query("TRUNCATE grid_cells RESTART IDENTITY"); await pool.query("DELETE FROM team_cooldowns WHERE world_seed != $1", [worldSeed]); + await pool.query("DELETE FROM user_cooldowns WHERE world_seed != $1", [worldSeed]); await pool.query("DELETE FROM team_econ_scores WHERE world_seed != $1", [worldSeed]); await pool.query("DELETE FROM team_element_bonus WHERE world_seed != $1", [worldSeed]); console.log(`[world] Slot ${lastSeedSlot} → ${seedSlot}; grid wiped, old cooldowns cleared.`); @@ -170,6 +180,25 @@ export async function upsertTeamCooldown(worldSeed, team) { ); } +// ── User cooldowns (per-user, replaces team-wide cooldown for reveal) ───────── + +export async function getUserCooldown(worldSeed, userId) { + const { rows } = await pool.query( + `SELECT last_reveal FROM user_cooldowns WHERE world_seed = $1 AND user_id = $2`, + [worldSeed, userId] + ); + return rows[0] ?? null; +} + +export async function upsertUserCooldown(worldSeed, userId, team) { + await pool.query( + `INSERT INTO user_cooldowns (world_seed, user_id, team, last_reveal) + VALUES ($1, $2, $3, NOW()) + ON CONFLICT (world_seed, user_id) DO UPDATE SET last_reveal = NOW()`, + [worldSeed, userId, team] + ); +} + // ── Economic scores ─────────────────────────────────────────────────────────── export async function getEconScores(worldSeed) { diff --git a/server/routes/game.js b/server/routes/game.js index 3db681a..e856d2a 100644 --- a/server/routes/game.js +++ b/server/routes/game.js @@ -1,13 +1,16 @@ import express from "express"; +import jwt from "jsonwebtoken"; import { getConfig } from "../configLoader.js"; import { computeWorldSeedState } from "../worldSeed.js"; +import { authMiddleware, JWT_SECRET } from "../middleware/auth.js"; import { ensureSeedEpoch, getGridCells, insertCell, getExistingCell, getTeamCooldown, - upsertTeamCooldown, + getUserCooldown, + upsertUserCooldown, getScores, getEconScores, addEconScore, @@ -34,11 +37,26 @@ router.get("/config", async (req, res) => { const bonus = await getElementBonus(worldSeed); const teamBonus = bonus[team] ?? 0; const effectiveCooldown = cfg.clickCooldownSeconds / (1 + teamBonus / 100); - const row = await getTeamCooldown(worldSeed, team); - if (row) { - const secondsSince = (Date.now() - new Date(row.last_reveal).getTime()) / 1000; - if (secondsSince < effectiveCooldown) { - teamCooldownRemaining = Math.ceil(effectiveCooldown - secondsSince); + // Use per-user cooldown if auth token provided, else fall back to team-wide + const authHeader = req.headers["authorization"]; + if (authHeader && authHeader.startsWith("Bearer ")) { + try { + const payload = jwt.verify(authHeader.slice(7), JWT_SECRET); + const row = await getUserCooldown(worldSeed, payload.userId); + if (row) { + const secondsSince = (Date.now() - new Date(row.last_reveal).getTime()) / 1000; + if (secondsSince < effectiveCooldown) { + teamCooldownRemaining = Math.ceil(effectiveCooldown - secondsSince); + } + } + } catch { /* invalid token — return 0 */ } + } else { + const row = await getTeamCooldown(worldSeed, team); + if (row) { + const secondsSince = (Date.now() - new Date(row.last_reveal).getTime()) / 1000; + if (secondsSince < effectiveCooldown) { + teamCooldownRemaining = Math.ceil(effectiveCooldown - secondsSince); + } } } } @@ -84,18 +102,16 @@ router.get("/grid/:seed", async (req, res) => { }); // POST /api/cell/reveal -router.post("/cell/reveal", async (req, res) => { +router.post("/cell/reveal", authMiddleware, async (req, res) => { const seed = String(req.body?.seed ?? ""); - const team = String(req.body?.team ?? ""); + const team = req.user.team; // taken from verified JWT, not request body + const userId = req.user.userId; const x = Number(req.body?.x); const y = Number(req.body?.y); if (!seed || !Number.isInteger(x) || !Number.isInteger(y) || x < 0 || x >= 100 || y < 0 || y >= 100) { return res.status(400).json({ error: "invalid_body" }); } - if (team !== "blue" && team !== "red") { - return res.status(400).json({ error: "invalid_team" }); - } try { const worldSeed = await ensureSeedEpoch(); @@ -108,7 +124,7 @@ router.post("/cell/reveal", async (req, res) => { const bonus = await getElementBonus(worldSeed); const teamBonus = bonus[team] ?? 0; const effectiveCooldown = cfg.clickCooldownSeconds / (1 + teamBonus / 100); - const cooldownRow = await getTeamCooldown(worldSeed, team); + const cooldownRow = await getUserCooldown(worldSeed, userId); if (cooldownRow) { const secondsSince = (Date.now() - new Date(cooldownRow.last_reveal).getTime()) / 1000; if (secondsSince < effectiveCooldown) { @@ -127,7 +143,7 @@ router.post("/cell/reveal", async (req, res) => { const inserted = await insertCell(seed, x, y, cell.exploitable, cell.hasPlanet, planetJson, team); if (inserted) { - await upsertTeamCooldown(worldSeed, team); + await upsertUserCooldown(worldSeed, userId, team); return res.json(rowToCellPayload(inserted)); }