refacto: Cooldown is now user-set
This commit is contained in:
@@ -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 }),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user