Private
Public Access
1
0

refacto: Cooldown is now user-set

This commit is contained in:
gauvainboiche
2026-03-31 10:36:25 +02:00
parent a23230690d
commit 8fabeb13b1
3 changed files with 66 additions and 15 deletions

View File

@@ -3,7 +3,9 @@
// parsed JSON. No state mutations, no DOM access. // parsed JSON. No state mutations, no DOM access.
export async function apiFetchConfig(team) { 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"); if (!res.ok) throw new Error("config_fetch_failed");
return res.json(); 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.). */ /** Returns the raw Response so the caller can inspect status codes (409, 410, etc.). */
export async function apiRevealCell(seed, x, y, team) { export async function apiRevealCell(seed, x, y, team) {
const token = localStorage.getItem("authToken");
return fetch("/api/cell/reveal", { return fetch("/api/cell/reveal", {
method: "POST", method: "POST",
headers: { "Content-Type": "application/json" }, headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ seed, x, y, team }), body: JSON.stringify({ seed, x, y, team }),
}); });
} }

View File

@@ -29,6 +29,15 @@ export async function initGameSchema() {
PRIMARY KEY (world_seed, team) 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(` await pool.query(`
CREATE TABLE IF NOT EXISTS team_econ_scores ( CREATE TABLE IF NOT EXISTS team_econ_scores (
world_seed TEXT NOT NULL, world_seed TEXT NOT NULL,
@@ -112,6 +121,7 @@ export async function ensureSeedEpoch() {
} }
await pool.query("TRUNCATE grid_cells RESTART IDENTITY"); 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 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_econ_scores WHERE world_seed != $1", [worldSeed]);
await pool.query("DELETE FROM team_element_bonus 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.`); 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 ─────────────────────────────────────────────────────────── // ── Economic scores ───────────────────────────────────────────────────────────
export async function getEconScores(worldSeed) { export async function getEconScores(worldSeed) {

View File

@@ -1,13 +1,16 @@
import express from "express"; import express from "express";
import jwt from "jsonwebtoken";
import { getConfig } from "../configLoader.js"; import { getConfig } from "../configLoader.js";
import { computeWorldSeedState } from "../worldSeed.js"; import { computeWorldSeedState } from "../worldSeed.js";
import { authMiddleware, JWT_SECRET } from "../middleware/auth.js";
import { import {
ensureSeedEpoch, ensureSeedEpoch,
getGridCells, getGridCells,
insertCell, insertCell,
getExistingCell, getExistingCell,
getTeamCooldown, getTeamCooldown,
upsertTeamCooldown, getUserCooldown,
upsertUserCooldown,
getScores, getScores,
getEconScores, getEconScores,
addEconScore, addEconScore,
@@ -34,6 +37,20 @@ router.get("/config", async (req, res) => {
const bonus = await getElementBonus(worldSeed); const bonus = await getElementBonus(worldSeed);
const teamBonus = bonus[team] ?? 0; const teamBonus = bonus[team] ?? 0;
const effectiveCooldown = cfg.clickCooldownSeconds / (1 + teamBonus / 100); const effectiveCooldown = cfg.clickCooldownSeconds / (1 + teamBonus / 100);
// 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); const row = await getTeamCooldown(worldSeed, team);
if (row) { if (row) {
const secondsSince = (Date.now() - new Date(row.last_reveal).getTime()) / 1000; const secondsSince = (Date.now() - new Date(row.last_reveal).getTime()) / 1000;
@@ -42,6 +59,7 @@ router.get("/config", async (req, res) => {
} }
} }
} }
}
res.json({ res.json({
clickCooldownSeconds: cfg.clickCooldownSeconds, clickCooldownSeconds: cfg.clickCooldownSeconds,
@@ -84,18 +102,16 @@ router.get("/grid/:seed", async (req, res) => {
}); });
// POST /api/cell/reveal // 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 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 x = Number(req.body?.x);
const y = Number(req.body?.y); const y = Number(req.body?.y);
if (!seed || !Number.isInteger(x) || !Number.isInteger(y) || x < 0 || x >= 100 || y < 0 || y >= 100) { if (!seed || !Number.isInteger(x) || !Number.isInteger(y) || x < 0 || x >= 100 || y < 0 || y >= 100) {
return res.status(400).json({ error: "invalid_body" }); return res.status(400).json({ error: "invalid_body" });
} }
if (team !== "blue" && team !== "red") {
return res.status(400).json({ error: "invalid_team" });
}
try { try {
const worldSeed = await ensureSeedEpoch(); const worldSeed = await ensureSeedEpoch();
@@ -108,7 +124,7 @@ router.post("/cell/reveal", async (req, res) => {
const bonus = await getElementBonus(worldSeed); const bonus = await getElementBonus(worldSeed);
const teamBonus = bonus[team] ?? 0; const teamBonus = bonus[team] ?? 0;
const effectiveCooldown = cfg.clickCooldownSeconds / (1 + teamBonus / 100); const effectiveCooldown = cfg.clickCooldownSeconds / (1 + teamBonus / 100);
const cooldownRow = await getTeamCooldown(worldSeed, team); const cooldownRow = await getUserCooldown(worldSeed, userId);
if (cooldownRow) { if (cooldownRow) {
const secondsSince = (Date.now() - new Date(cooldownRow.last_reveal).getTime()) / 1000; const secondsSince = (Date.now() - new Date(cooldownRow.last_reveal).getTime()) / 1000;
if (secondsSince < effectiveCooldown) { 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); const inserted = await insertCell(seed, x, y, cell.exploitable, cell.hasPlanet, planetJson, team);
if (inserted) { if (inserted) {
await upsertTeamCooldown(worldSeed, team); await upsertUserCooldown(worldSeed, userId, team);
return res.json(rowToCellPayload(inserted)); return res.json(rowToCellPayload(inserted));
} }