diff --git a/config/game.settings.json b/config/game.settings.json index f5a27d1..6c15b1f 100644 --- a/config/game.settings.json +++ b/config/game.settings.json @@ -1,44 +1,54 @@ { - "clickCooldownSeconds": 2, + "clickCooldownSeconds": 100, "databaseWipeoutIntervalSeconds": 21600, "debugModeForTeams": true, "configReloadIntervalSeconds": 30, + "elementWorth": { + "common": 1, + "petrol": 3, + "food": 2, + "medic": 5, + "science": 8, + "industry": 4, + "money": 6, + "goods": 3 + }, "resourceWorth": { "common": { - "rock": 1, - "wood": 1, - "mineral": 2, - "stones": 2, - "liquid": 1, - "oil": 4, - "gas": 3, - "grain": 1, - "livestock": 2, - "fish": 1, - "plant": 1, - "goods": 4, - "animals": 2, - "science": 6, - "factory": 5, - "acid": 1 + "rock": 100, + "wood": 120, + "plant": 120, + "grain": 150, + "liquid": 180, + "fish": 200, + "stones": 220, + "animals": 300, + "livestock": 350, + "acid": 400, + "mineral": 450, + "gas": 800, + "oil": 1000, + "goods": 1200, + "factory": 1500, + "science": 2000 }, "rare": { - "rock": 3, - "wood": 3, - "mineral": 4, - "stones": 5, - "liquid": 2, - "oil": 7, - "gas": 6, - "grain": 3, - "livestock": 3, - "fish": 5, - "plant": 3, - "goods": 8, - "animals": 5, - "science": 15, - "factory": 12, - "acid": 3 + "rock": 300, + "wood": 400, + "plant": 500, + "grain": 450, + "liquid": 600, + "fish": 900, + "stones": 1200, + "animals": 1500, + "livestock": 1400, + "acid": 1800, + "mineral": 2500, + "gas": 3000, + "oil": 3500, + "goods": 4000, + "factory": 4500, + "science": 5000 } } } \ No newline at end of file diff --git a/public/index.html b/public/index.html index cfd2e3e..d3632fb 100644 --- a/public/index.html +++ b/public/index.html @@ -89,17 +89,35 @@
- - - Resistance - 0 - + +
+ Résistance +
+
+ 0 + planètes +
+
+ 0 + pts victoire +
+
+
- - 0 - First Order - - +
+ Premier Ordre +
+
+ 0 + pts victoire +
+
+ 0 + planètes +
+
+
+
@@ -134,6 +152,10 @@ Prochaine graine dans --:--:-- +
+ Base de données depuis + +
@@ -178,6 +200,28 @@ + +
+
⚡ Bonus de recharge planétaire
+
+ + Résistance + 0.00 + % + + + + 0.00 + % + Premier Ordre + +
+
+ Recharge effective (votre équipe) : + +
+
+
diff --git a/public/src/api.js b/public/src/api.js index 65a1b9e..8b17cb2 100644 --- a/public/src/api.js +++ b/public/src/api.js @@ -65,3 +65,31 @@ export async function apiTickEconScores(seed, blue, red) { if (!res.ok) throw new Error("econ_tick_failed"); return res.json(); } + +export async function apiFetchElementBonus() { + const res = await fetch("/api/element-bonus"); + if (!res.ok) throw new Error("element_bonus_fetch_failed"); + return res.json(); +} + +export async function apiTickElementBonus(seed, blue, red) { + const res = await fetch("/api/element-bonus/tick", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ seed, blue, red }), + }); + if (!res.ok) throw new Error("element_bonus_tick_failed"); + return res.json(); +} + +export async function apiFetchDbInfo() { + const res = await fetch("/api/db-info"); + if (!res.ok) throw new Error("db_info_fetch_failed"); + return res.json(); +} + +export async function apiFetchVictoryPoints() { + const res = await fetch("/api/victory-points"); + if (!res.ok) throw new Error("vp_fetch_failed"); + return res.json(); +} diff --git a/public/src/economy.js b/public/src/economy.js index d6e37bb..20b652a 100644 --- a/public/src/economy.js +++ b/public/src/economy.js @@ -1,4 +1,4 @@ -import { resources } from "./planetEconomy.js"; +import { resources, elements } from "./planetEconomy.js"; // ── Sort state ──────────────────────────────────────────────────────────────── @@ -64,6 +64,46 @@ export function computeTeamIncome(team, cells, resourceWorth) { return { total, byResource }; } +// ── Element bonus calculation ───────────────────────────────────────────────── + +/** + * Reverse map: French element label → config key + * e.g. "Matières premières" → "common", "Hydrocarbures" → "petrol" + */ +const ELEMENT_LABEL_TO_KEY = Object.fromEntries( + Object.entries(elements).map(([key, label]) => [label, key]) +); + +/** + * Compute cumulative element bonus for a team based on their planets' production. + * planet.production stores French label strings as keys (values from elements const). + * bonus = sum_over_planets( sum_over_elements( elementShare% / 100 * elementWorth[key] ) ) + * + * @param {string} team + * @param {Map} cells + * @param {object} elementWorth - { common: 1, petrol: 3, ... } + * @returns {number} bonus value (use as %) + */ +export function computeTeamElementBonus(team, cells, elementWorth) { + let bonus = 0; + for (const [, meta] of cells) { + if (meta.discoveredBy !== team) continue; + if (!meta.hasPlanet || !meta.planet) continue; + const { production } = meta.planet; + if (!production) continue; + for (const [elementLabel, pct] of Object.entries(production)) { + // production keys are French labels; map back to config key + const elementKey = ELEMENT_LABEL_TO_KEY[elementLabel] ?? elementLabel; + const worth = elementWorth?.[elementKey] ?? 0; + if (worth === 0) continue; + bonus += (pct / 100) * worth; + } + } + return bonus; +} + +export { elements }; + // ── Resource table for the sidebar ─────────────────────────────────────────── /** diff --git a/public/src/game.js b/public/src/game.js index a38e790..d0497e3 100644 --- a/public/src/game.js +++ b/public/src/game.js @@ -1,7 +1,7 @@ import { fnv1a32, hash2u32, mulberry32 } from "./rng.js"; import { formatPlanet, generatePlanet } from "./planetGeneration.js"; -import { apiFetchConfig, apiFetchScores, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores } from "./api.js"; -import { computeTeamIncome, renderResourceTable, setEconSort, getEconSort } from "./economy.js"; +import { apiFetchConfig, apiFetchScores, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints } from "./api.js"; +import { computeTeamIncome, computeTeamElementBonus, renderResourceTable, setEconSort, getEconSort } from "./economy.js"; // ── Constants ───────────────────────────────────────────────────────────────── @@ -27,6 +27,7 @@ export const GAME_CONFIG = { configReloadIntervalSeconds: 30, worldSeed: "", seedPeriodEndsAtUtc: "", + elementWorth: {}, resourceWorth: { common: {}, rare: {} }, }; window.GAME_CONFIG = GAME_CONFIG; @@ -149,6 +150,12 @@ const nextPeriodEl = document.getElementById("nextPeriodUtc"); const resetCountEl = document.getElementById("refreshCountdown"); const scoreBlueEl = document.getElementById("scoreBlue"); const scoreRedEl = document.getElementById("scoreRed"); +const vpBlueEl = document.getElementById("vpBlue"); +const vpRedEl = document.getElementById("vpRed"); +const dbCreatedAtEl = document.getElementById("dbCreatedAt"); +const elemBonusBlueEl = document.getElementById("elemBonusBlue"); +const elemBonusRedEl = document.getElementById("elemBonusRed"); +const effectiveCooldownEl = document.getElementById("effectiveCooldown"); const incomeBlueEl = document.getElementById("incomeBlue"); const incomeRedEl = document.getElementById("incomeRed"); const resourceTableEl = document.getElementById("resourceTableBody"); @@ -188,6 +195,9 @@ export function applyConfigPayload(data) { GAME_CONFIG.configReloadIntervalSeconds = Math.max(5, Number(data.configReloadIntervalSeconds) || 30); GAME_CONFIG.worldSeed = String(data.worldSeed ?? ""); GAME_CONFIG.seedPeriodEndsAtUtc = String(data.seedPeriodEndsAtUtc ?? ""); + if (data.elementWorth && typeof data.elementWorth === "object") { + GAME_CONFIG.elementWorth = data.elementWorth; + } if (data.resourceWorth && typeof data.resourceWorth === "object") { GAME_CONFIG.resourceWorth = data.resourceWorth; } @@ -232,8 +242,77 @@ export function lockTeamSwitcher() { export async function fetchAndApplyScores() { try { const { blue, red } = await apiFetchScores(); - scoreBlueEl.textContent = String(blue ?? 0); - scoreRedEl.textContent = String(red ?? 0); + if (scoreBlueEl) scoreBlueEl.textContent = String(blue ?? 0); + if (scoreRedEl) scoreRedEl.textContent = String(red ?? 0); + } catch { /* ignore */ } +} + +export async function loadVictoryPoints() { + try { + const { blue, red } = await apiFetchVictoryPoints(); + if (vpBlueEl) vpBlueEl.textContent = String(blue ?? 0); + if (vpRedEl) vpRedEl.textContent = String(red ?? 0); + } catch { /* ignore */ } +} + +// ── Element bonus ───────────────────────────────────────────────────────────── + +let elemBonusBlue = 0; +let elemBonusRed = 0; + +function updateEffectiveCooldownDisplay() { + const base = GAME_CONFIG.clickCooldownSeconds; + const bonus = currentTeam === "blue" ? elemBonusBlue : elemBonusRed; + const effective = base / (1 + bonus / 100); + if (elemBonusBlueEl) elemBonusBlueEl.textContent = elemBonusBlue.toFixed(2); + if (elemBonusRedEl) elemBonusRedEl.textContent = elemBonusRed.toFixed(2); + if (effectiveCooldownEl) { + effectiveCooldownEl.textContent = effective < 1 + ? `${(effective * 1000).toFixed(0)}ms` + : `${effective.toFixed(2)}s`; + } +} + +export async function loadElementBonus() { + try { + const { blue, red } = await apiFetchElementBonus(); + elemBonusBlue = blue ?? 0; + elemBonusRed = red ?? 0; + updateEffectiveCooldownDisplay(); + } catch { /* ignore */ } +} + +export async function tickElementBonus() { + const worth = GAME_CONFIG.elementWorth; + const blueBonus = computeTeamElementBonus("blue", cells, worth); + const redBonus = computeTeamElementBonus("red", cells, worth); + try { + const result = await apiTickElementBonus(seedStr, blueBonus, redBonus); + elemBonusBlue = result.blue ?? blueBonus; + elemBonusRed = result.red ?? redBonus; + } catch { + elemBonusBlue = blueBonus; + elemBonusRed = redBonus; + } + updateEffectiveCooldownDisplay(); +} + +export function getEffectiveCooldown() { + const base = GAME_CONFIG.clickCooldownSeconds; + const bonus = currentTeam === "blue" ? elemBonusBlue : elemBonusRed; + return base / (1 + bonus / 100); +} + +export async function loadDbInfo() { + try { + const { createdAt } = await apiFetchDbInfo(); + if (dbCreatedAtEl && createdAt) { + const d = new Date(createdAt); + dbCreatedAtEl.textContent = + d.toLocaleDateString("fr-FR", { day: "2-digit", month: "2-digit", year: "numeric" }) + + " " + + d.toLocaleTimeString("fr-FR", { hour: "2-digit", minute: "2-digit" }); + } } catch { /* ignore */ } } @@ -385,7 +464,7 @@ function tickCooldown() { } export function startCooldown() { - const secs = GAME_CONFIG.clickCooldownSeconds; + const secs = getEffectiveCooldown(); if (secs <= 0) { teamCooldownEndMs[currentTeam] = 0; countdownWrap.classList.add("hidden"); diff --git a/public/src/main.js b/public/src/main.js index 34eeb9c..dfeaf19 100644 --- a/public/src/main.js +++ b/public/src/main.js @@ -9,6 +9,10 @@ import { updateEconomyDisplay, tickEconScore, loadEconScores, + loadVictoryPoints, + loadDbInfo, + loadElementBonus, + tickElementBonus, refreshFromServer, refreshGridDisplay, loadPlayfieldMask, @@ -58,6 +62,7 @@ function scheduleScorePoll() { scorePollTimer = window.setTimeout(async () => { await fetchAndApplyScores(); tickEconScore(ECON_TICK_SECONDS); + tickElementBonus(); scheduleScorePoll(); }, ECON_TICK_SECONDS * 1_000); } @@ -144,6 +149,9 @@ async function boot() { await fetchGridForSeed(seedStr); await fetchAndApplyScores(); await loadEconScores(); + await loadVictoryPoints(); + await loadDbInfo(); + await loadElementBonus(); updateEconomyDisplay(); } catch { hint.textContent = "API unavailable — start the Node server (docker-compose up --build)."; @@ -159,6 +167,9 @@ async function boot() { scheduleConfigPoll(); scheduleScorePoll(); + // Refresh VP every 30 s so new awards are reflected promptly + setInterval(loadVictoryPoints, 30_000); + // Refresh grid every second so all clients see new tiles promptly setInterval(refreshGridDisplay, 1_000); } diff --git a/public/style.css b/public/style.css index d1f9287..6fcbaff 100644 --- a/public/style.css +++ b/public/style.css @@ -276,61 +276,94 @@ body { .scoreBoard { display: flex; align-items: center; - justify-content: center; - gap: 16px; - padding: 10px 16px; + justify-content: space-between; + gap: 8px; + padding: 10px 12px; border-radius: 14px; border: 1px solid rgba(255, 255, 255, 0.1); background: rgba(255, 255, 255, 0.04); - font-size: 22px; - font-weight: 800; font-family: "Courier New", Courier, monospace; font-variant-numeric: tabular-nums; - letter-spacing: 0.5px; +} + +.team-logo { + width: 54px; + height: 54px; + display: block; + flex-shrink: 0; } .scoreTeam { display: flex; + flex-direction: column; align-items: center; - justify-content: center; - gap: 10px; -} - -.team-logo { - width: 64px; - height: 64px; - display: block; + gap: 4px; + flex: 1; } .scoreTeamName { - font-size: 12px; + font-size: 10px; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; - opacity: 0.8; + opacity: 0.75; } -.scoreTeam--blue .scoreTeamName { - color: rgba(90, 200, 255, 0.9); +.scoreTeam--blue .scoreTeamName { color: rgba(90, 200, 255, 0.9); } +.scoreTeam--red .scoreTeamName { color: rgba(220, 75, 85, 0.9); } + +.scoreStats { + display: flex; + gap: 10px; + align-items: flex-end; +} + +.scoreStat { + display: flex; + flex-direction: column; + align-items: center; + gap: 1px; +} + +.scoreStatVal { + font-size: 20px; + font-weight: 800; + line-height: 1; +} + +.scoreStatLabel { + font-size: 9px; + font-weight: 600; + text-transform: uppercase; + letter-spacing: 0.05em; + opacity: 0.55; } .scoreTeam--blue .scoreValue { color: rgba(90, 200, 255, 1); - text-shadow: 0 0 20px rgba(90, 200, 255, 0.4); + text-shadow: 0 0 16px rgba(90, 200, 255, 0.4); } -.scoreTeam--red .scoreTeamName { - color: rgba(220, 75, 85, 0.9); +.scoreTeam--blue .scoreVP { + color: rgba(130, 230, 130, 1); + text-shadow: 0 0 16px rgba(90, 200, 130, 0.35); } .scoreTeam--red .scoreValue { color: rgba(220, 75, 85, 1); - text-shadow: 0 0 20px rgba(220, 75, 85, 0.4); + text-shadow: 0 0 16px rgba(220, 75, 85, 0.4); +} + +.scoreTeam--red .scoreVP { + color: rgba(230, 150, 80, 1); + text-shadow: 0 0 16px rgba(220, 130, 60, 0.35); } .scoreSep { - opacity: 0.4; + opacity: 0.35; font-size: 18px; + font-weight: 800; + flex-shrink: 0; } /* ── Main app layout ──────────────────────────────────────────────────────── */ @@ -402,33 +435,36 @@ body { .infoTable { display: flex; flex-direction: column; - gap: 6px; + gap: 4px; font-family: "Courier New", Courier, monospace; } .infoRow { display: flex; align-items: baseline; - gap: 8px; + gap: 0; min-height: 22px; } .infoKey { - flex: 0 0 auto; + flex: 0 0 55%; font-size: 11px; opacity: 0.65; white-space: nowrap; - min-width: 130px; text-align: right; + padding-right: 8px; + overflow: hidden; + text-overflow: ellipsis; } .infoVal { - flex: 1; + flex: 0 0 45%; font-size: 11px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; font-family: "Courier New", Courier, monospace; + text-align: left; } .infoVal code { @@ -801,6 +837,75 @@ button:hover { font-weight: 700; } +/* ── Element bonus section ────────────────────────────────────────────────── */ + +.elemBonusSection { + display: flex; + flex-direction: column; + gap: 6px; + padding: 10px 14px; + border-radius: 12px; + border: 1px solid rgba(255, 220, 100, 0.15); + background: rgba(255, 200, 50, 0.04); + font-family: "Courier New", Courier, monospace; + font-size: 12px; +} + +.elemBonusSectionTitle { + font-size: 11px; + font-weight: 700; + text-transform: uppercase; + letter-spacing: 0.06em; + opacity: 0.7; + margin-bottom: 2px; +} + +.elemBonusRow { + display: flex; + align-items: center; + justify-content: space-between; + gap: 6px; +} + +.elemBonusTeam { + display: flex; + align-items: center; + gap: 5px; +} + +.elemBonusTeam--blue .elemBonusLabel { color: rgba(90, 200, 255, 0.75); font-size: 10px; } +.elemBonusTeam--blue .elemBonusVal { color: rgba(90, 200, 255, 1); font-weight: 800; font-size: 13px; } +.elemBonusTeam--red .elemBonusLabel { color: rgba(220, 75, 85, 0.75); font-size: 10px; } +.elemBonusTeam--red .elemBonusVal { color: rgba(220, 75, 85, 1); font-weight: 800; font-size: 13px; } + +.elemBonusUnit { + font-size: 10px; + opacity: 0.6; +} + +.elemBonusSep { + opacity: 0.3; +} + +.elemBonusDetail { + display: flex; + align-items: center; + gap: 6px; + border-top: 1px solid rgba(255, 255, 255, 0.06); + padding-top: 6px; + margin-top: 2px; + font-size: 11px; +} + +.elemBonusDetailLabel { + opacity: 0.55; +} + +.elemBonusDetailVal { + font-weight: 700; + color: rgba(255, 220, 100, 0.9); +} + /* ── Options / admin section ──────────────────────────────────────────────── */ .infoSection--options { @@ -984,11 +1089,14 @@ canvas { /* Scoreboard wraps nicely on narrow screens */ .scoreBoard { flex-wrap: wrap; - font-size: 18px; } .team-logo { - width: 44px; - height: 44px; + width: 40px; + height: 40px; + } + + .scoreStatVal { + font-size: 16px; } } \ No newline at end of file diff --git a/server/configLoader.js b/server/configLoader.js index 933643d..77fb3ad 100644 --- a/server/configLoader.js +++ b/server/configLoader.js @@ -13,6 +13,7 @@ let cached = { databaseWipeoutIntervalSeconds: 21600, debugModeForTeams: true, configReloadIntervalSeconds: 30, + elementWorth: {}, resourceWorth: { common: {}, rare: {} }, }; @@ -47,6 +48,9 @@ export function loadConfigFile() { if (typeof j.configReloadIntervalSeconds === "number" && j.configReloadIntervalSeconds >= 5) { cached.configReloadIntervalSeconds = j.configReloadIntervalSeconds; } + if (j.elementWorth && typeof j.elementWorth === "object") { + cached.elementWorth = j.elementWorth; + } if (j.resourceWorth && typeof j.resourceWorth === "object") { cached.resourceWorth = j.resourceWorth; } diff --git a/server/db/gameDb.js b/server/db/gameDb.js index 7f5f514..602348a 100644 --- a/server/db/gameDb.js +++ b/server/db/gameDb.js @@ -37,6 +37,31 @@ export async function initGameSchema() { PRIMARY KEY (world_seed, team) ); `); + await pool.query(` + CREATE TABLE IF NOT EXISTS team_element_bonus ( + world_seed TEXT NOT NULL, + team TEXT NOT NULL CHECK (team IN ('blue', 'red')), + bonus DOUBLE PRECISION NOT NULL DEFAULT 0, + PRIMARY KEY (world_seed, team) + ); + `); + await pool.query(` + CREATE TABLE IF NOT EXISTS db_metadata ( + id SERIAL PRIMARY KEY, + created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() + ); + INSERT INTO db_metadata (created_at) + SELECT NOW() WHERE NOT EXISTS (SELECT 1 FROM db_metadata); + `); + await pool.query(` + CREATE TABLE IF NOT EXISTS victory_points ( + id SERIAL PRIMARY KEY, + awarded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), + world_seed TEXT NOT NULL, + team TEXT NOT NULL CHECK (team IN ('blue', 'red')) + ); + CREATE INDEX IF NOT EXISTS idx_vp_team ON victory_points (team); + `); await pool.query(` ALTER TABLE grid_cells ADD COLUMN IF NOT EXISTS discovered_by TEXT; UPDATE grid_cells SET discovered_by = 'blue' WHERE discovered_by IS NULL; @@ -65,9 +90,30 @@ export async function ensureSeedEpoch() { return worldSeed; } if (seedSlot !== lastSeedSlot) { + // Award a victory point to the team with the highest econ score before wiping + try { + const expiredSeed = `swg-${lastSeedSlot}`; + const econRows = await pool.query( + `SELECT team, score FROM team_econ_scores WHERE world_seed = $1`, + [expiredSeed] + ); + const scores = { blue: 0, red: 0 }; + for (const row of econRows.rows) scores[row.team] = Number(row.score); + if (scores.blue > 0 || scores.red > 0) { + const winner = scores.blue >= scores.red ? "blue" : "red"; + await pool.query( + `INSERT INTO victory_points (world_seed, team) VALUES ($1, $2)`, + [expiredSeed, winner] + ); + console.log(`[world] VP awarded to ${winner} for seed ${expiredSeed} (blue=${scores.blue.toFixed(3)}, red=${scores.red.toFixed(3)})`); + } + } catch (e) { + console.error("[world] VP award error:", e); + } 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_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.`); lastSeedSlot = seedSlot; } @@ -147,6 +193,46 @@ export async function addEconScore(worldSeed, team, delta) { ); } +export async function getElementBonus(worldSeed) { + const { rows } = await pool.query( + `SELECT team, bonus FROM team_element_bonus WHERE world_seed = $1`, + [worldSeed] + ); + const result = { blue: 0, red: 0 }; + for (const row of rows) result[row.team] = Number(row.bonus); + return result; +} + +export async function setElementBonus(worldSeed, team, bonus) { + await pool.query( + `INSERT INTO team_element_bonus (world_seed, team, bonus) + VALUES ($1, $2, $3) + ON CONFLICT (world_seed, team) DO UPDATE + SET bonus = EXCLUDED.bonus`, + [worldSeed, team, bonus] + ); +} + +// ── DB metadata ─────────────────────────────────────────────────────────────── + +export async function getDbCreatedAt() { + const { rows } = await pool.query( + `SELECT created_at FROM db_metadata ORDER BY id ASC LIMIT 1` + ); + return rows[0]?.created_at ?? null; +} + +// ── Victory points ──────────────────────────────────────────────────────────── + +export async function getVictoryPoints() { + const { rows } = await pool.query( + `SELECT team, COUNT(*) AS cnt FROM victory_points GROUP BY team` + ); + const result = { blue: 0, red: 0 }; + for (const row of rows) result[row.team] = Number(row.cnt); + return result; +} + // ── Scores ──────────────────────────────────────────────────────────────────── export async function getScores(worldSeed) { @@ -157,4 +243,4 @@ export async function getScores(worldSeed) { [worldSeed] ); return rows; -} \ No newline at end of file +} diff --git a/server/routes/game.js b/server/routes/game.js index ae7d387..ced709a 100644 --- a/server/routes/game.js +++ b/server/routes/game.js @@ -11,6 +11,10 @@ import { getScores, getEconScores, addEconScore, + getElementBonus, + setElementBonus, + getDbCreatedAt, + getVictoryPoints, } from "../db/gameDb.js"; import { computeCell, rowToCellPayload } from "../helpers/cell.js"; @@ -46,6 +50,7 @@ router.get("/config", async (req, res) => { seedPeriodStartsAtUtc: ws.seedPeriodStartsAtUtc, teamCooldownRemaining, resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} }, + elementWorth: cfg.elementWorth ?? {}, }); } catch (e) { console.error(e); @@ -180,6 +185,58 @@ router.post("/econ-scores/tick", async (req, res) => { } }); +// 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/scores router.get("/scores", async (_req, res) => { try {