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