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, getUserCooldown, upsertUserCooldown, getScores, getEconScores, addEconScore, getElementBonus, setElementBonus, getDbCreatedAt, getVictoryPoints, getActivePlayerCounts, } from "../db/gameDb.js"; import { computeCell, rowToCellPayload } from "../helpers/cell.js"; const router = express.Router(); // GET /api/config router.get("/config", async (req, res) => { try { const worldSeed = await ensureSeedEpoch(); const cfg = getConfig(); const rot = cfg.databaseWipeoutIntervalSeconds; const ws = computeWorldSeedState(rot); let teamCooldownRemaining = 0; const team = typeof req.query.team === "string" ? req.query.team : undefined; if (team === "blue" || team === "red") { const bonus = await getElementBonus(worldSeed); const teamBonus = bonus[team] ?? 0; 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); if (row) { const secondsSince = (Date.now() - new Date(row.last_reveal).getTime()) / 1000; if (secondsSince < effectiveCooldown) { teamCooldownRemaining = Math.ceil(effectiveCooldown - secondsSince); } } } } res.json({ clickCooldownSeconds: cfg.clickCooldownSeconds, databaseWipeoutIntervalSeconds: rot, debugModeForTeams: cfg.debugModeForTeams, configReloadIntervalSeconds: cfg.configReloadIntervalSeconds, worldSeed: ws.worldSeed, seedPeriodEndsAtUtc: ws.seedPeriodEndsAtUtc, seedPeriodStartsAtUtc: ws.seedPeriodStartsAtUtc, teamCooldownRemaining, resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} }, elementWorth: cfg.elementWorth ?? {}, }); } catch (e) { console.error(e); res.status(500).json({ error: "config_error" }); } }); // GET /api/grid/:seed router.get("/grid/:seed", async (req, res) => { const seed = decodeURIComponent(req.params.seed || ""); try { const worldSeed = await ensureSeedEpoch(); if (seed !== worldSeed) { return res.status(410).json({ error: "seed_expired", worldSeed, seedPeriodEndsAtUtc: computeWorldSeedState( getConfig().databaseWipeoutIntervalSeconds ).seedPeriodEndsAtUtc, }); } const rows = await getGridCells(seed); res.json({ seed, cells: rows }); } catch (e) { console.error(e); res.status(500).json({ error: "database_error" }); } }); // POST /api/cell/reveal router.post("/cell/reveal", authMiddleware, async (req, res) => { const seed = String(req.body?.seed ?? ""); 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" }); } try { const worldSeed = await ensureSeedEpoch(); if (seed !== worldSeed) { return res.status(410).json({ error: "seed_expired", worldSeed }); } const cfg = getConfig(); if (cfg.clickCooldownSeconds > 0) { const bonus = await getElementBonus(worldSeed); const teamBonus = bonus[team] ?? 0; const effectiveCooldown = cfg.clickCooldownSeconds / (1 + teamBonus / 100); const cooldownRow = await getUserCooldown(worldSeed, userId); if (cooldownRow) { const secondsSince = (Date.now() - new Date(cooldownRow.last_reveal).getTime()) / 1000; if (secondsSince < effectiveCooldown) { return res.status(429).json({ error: "cooldown_active", team, remainingSeconds: Math.ceil(effectiveCooldown - secondsSince), cooldownSeconds: effectiveCooldown, }); } } } const cell = computeCell(seed, x, y); const planetJson = cell.planet ? JSON.stringify(cell.planet) : null; const inserted = await insertCell(seed, x, y, cell.exploitable, cell.hasPlanet, planetJson, team); if (inserted) { await upsertUserCooldown(worldSeed, userId, team); return res.json(rowToCellPayload(inserted)); } const existing = await getExistingCell(seed, x, y); if (!existing) return res.status(500).json({ error: "insert_race" }); if (existing.discovered_by !== team) { return res.status(409).json({ error: "taken_by_other_team", discoveredBy: existing.discovered_by, cell: rowToCellPayload(existing), }); } return res.json(rowToCellPayload(existing)); } catch (e) { console.error(e); res.status(500).json({ error: "database_error" }); } }); // GET /api/econ-scores router.get("/econ-scores", async (_req, res) => { try { const worldSeed = await ensureSeedEpoch(); const scores = await getEconScores(worldSeed); res.json(scores); } catch (e) { console.error(e); res.status(500).json({ error: "database_error" }); } }); // POST /api/econ-scores/tick body: { seed, blue, red } router.post("/econ-scores/tick", async (req, res) => { const seed = String(req.body?.seed ?? ""); const blue = Number(req.body?.blue ?? 0); const red = Number(req.body?.red ?? 0); try { const worldSeed = await ensureSeedEpoch(); if (seed !== worldSeed) return res.status(410).json({ error: "seed_expired", worldSeed }); await addEconScore(worldSeed, "blue", blue); await addEconScore(worldSeed, "red", red); const scores = await getEconScores(worldSeed); res.json(scores); } catch (e) { console.error(e); res.status(500).json({ error: "database_error" }); } }); // GET /api/element-bonus router.get("/element-bonus", async (_req, res) => { try { const worldSeed = await ensureSeedEpoch(); const bonus = await getElementBonus(worldSeed); res.json(bonus); } catch (e) { console.error(e); res.status(500).json({ error: "database_error" }); } }); // POST /api/element-bonus/tick body: { seed, blue, red } router.post("/element-bonus/tick", async (req, res) => { const seed = String(req.body?.seed ?? ""); const blue = Number(req.body?.blue ?? 0); const red = Number(req.body?.red ?? 0); try { const worldSeed = await ensureSeedEpoch(); if (seed !== worldSeed) return res.status(410).json({ error: "seed_expired", worldSeed }); await setElementBonus(worldSeed, "blue", blue); await setElementBonus(worldSeed, "red", red); const bonus = await getElementBonus(worldSeed); res.json(bonus); } catch (e) { console.error(e); res.status(500).json({ error: "database_error" }); } }); // GET /api/db-info router.get("/db-info", async (_req, res) => { try { const createdAt = await getDbCreatedAt(); res.json({ createdAt }); } catch (e) { console.error(e); res.status(500).json({ error: "database_error" }); } }); // GET /api/victory-points router.get("/victory-points", async (_req, res) => { try { const vp = await getVictoryPoints(); res.json(vp); } catch (e) { console.error(e); res.status(500).json({ error: "database_error" }); } }); // GET /api/active-players router.get("/active-players", async (_req, res) => { try { const worldSeed = await ensureSeedEpoch(); const counts = await getActivePlayerCounts(worldSeed); res.json(counts); } catch (e) { console.error(e); res.status(500).json({ error: "database_error" }); } }); // GET /api/scores router.get("/scores", async (_req, res) => { try { const worldSeed = await ensureSeedEpoch(); const rows = await getScores(worldSeed); const scores = { blue: 0, red: 0 }; for (const row of rows) { if (row.discovered_by === "blue") scores.blue = Number(row.cnt); if (row.discovered_by === "red") scores.red = Number(row.cnt); } res.json(scores); } catch (e) { console.error(e); res.status(500).json({ error: "database_error" }); } }); export default router;