feat(gameplay): Adding % bonus for planet type
This commit is contained in:
@@ -1,44 +1,54 @@
|
|||||||
{
|
{
|
||||||
"clickCooldownSeconds": 2,
|
"clickCooldownSeconds": 100,
|
||||||
"databaseWipeoutIntervalSeconds": 21600,
|
"databaseWipeoutIntervalSeconds": 21600,
|
||||||
"debugModeForTeams": true,
|
"debugModeForTeams": true,
|
||||||
"configReloadIntervalSeconds": 30,
|
"configReloadIntervalSeconds": 30,
|
||||||
|
"elementWorth": {
|
||||||
|
"common": 1,
|
||||||
|
"petrol": 3,
|
||||||
|
"food": 2,
|
||||||
|
"medic": 5,
|
||||||
|
"science": 8,
|
||||||
|
"industry": 4,
|
||||||
|
"money": 6,
|
||||||
|
"goods": 3
|
||||||
|
},
|
||||||
"resourceWorth": {
|
"resourceWorth": {
|
||||||
"common": {
|
"common": {
|
||||||
"rock": 1,
|
"rock": 100,
|
||||||
"wood": 1,
|
"wood": 120,
|
||||||
"mineral": 2,
|
"plant": 120,
|
||||||
"stones": 2,
|
"grain": 150,
|
||||||
"liquid": 1,
|
"liquid": 180,
|
||||||
"oil": 4,
|
"fish": 200,
|
||||||
"gas": 3,
|
"stones": 220,
|
||||||
"grain": 1,
|
"animals": 300,
|
||||||
"livestock": 2,
|
"livestock": 350,
|
||||||
"fish": 1,
|
"acid": 400,
|
||||||
"plant": 1,
|
"mineral": 450,
|
||||||
"goods": 4,
|
"gas": 800,
|
||||||
"animals": 2,
|
"oil": 1000,
|
||||||
"science": 6,
|
"goods": 1200,
|
||||||
"factory": 5,
|
"factory": 1500,
|
||||||
"acid": 1
|
"science": 2000
|
||||||
},
|
},
|
||||||
"rare": {
|
"rare": {
|
||||||
"rock": 3,
|
"rock": 300,
|
||||||
"wood": 3,
|
"wood": 400,
|
||||||
"mineral": 4,
|
"plant": 500,
|
||||||
"stones": 5,
|
"grain": 450,
|
||||||
"liquid": 2,
|
"liquid": 600,
|
||||||
"oil": 7,
|
"fish": 900,
|
||||||
"gas": 6,
|
"stones": 1200,
|
||||||
"grain": 3,
|
"animals": 1500,
|
||||||
"livestock": 3,
|
"livestock": 1400,
|
||||||
"fish": 5,
|
"acid": 1800,
|
||||||
"plant": 3,
|
"mineral": 2500,
|
||||||
"goods": 8,
|
"gas": 3000,
|
||||||
"animals": 5,
|
"oil": 3500,
|
||||||
"science": 15,
|
"goods": 4000,
|
||||||
"factory": 12,
|
"factory": 4500,
|
||||||
"acid": 3
|
"science": 5000
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -89,17 +89,35 @@
|
|||||||
|
|
||||||
<!-- Team score display -->
|
<!-- Team score display -->
|
||||||
<div class="scoreBoard" id="scoreBoard">
|
<div class="scoreBoard" id="scoreBoard">
|
||||||
<span class="scoreTeam scoreTeam--blue">
|
|
||||||
<img src="./graphism/logo_resistance.svg" alt="Resistance" class="team-logo" />
|
<img src="./graphism/logo_resistance.svg" alt="Resistance" class="team-logo" />
|
||||||
<span class="scoreTeamName">Resistance</span>
|
<div class="scoreTeam scoreTeam--blue">
|
||||||
<span class="scoreValue" id="scoreBlue">0</span>
|
<span class="scoreTeamName">Résistance</span>
|
||||||
</span>
|
<div class="scoreStats">
|
||||||
|
<div class="scoreStat">
|
||||||
|
<span class="scoreStatVal scoreValue" id="scoreBlue">0</span>
|
||||||
|
<span class="scoreStatLabel">planètes</span>
|
||||||
|
</div>
|
||||||
|
<div class="scoreStat">
|
||||||
|
<span class="scoreStatVal scoreVP" id="vpBlue">0</span>
|
||||||
|
<span class="scoreStatLabel">pts victoire</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<span class="scoreSep">—</span>
|
<span class="scoreSep">—</span>
|
||||||
<span class="scoreTeam scoreTeam--red">
|
<div class="scoreTeam scoreTeam--red">
|
||||||
<span class="scoreValue" id="scoreRed">0</span>
|
<span class="scoreTeamName">Premier Ordre</span>
|
||||||
<span class="scoreTeamName">First Order</span>
|
<div class="scoreStats">
|
||||||
|
<div class="scoreStat">
|
||||||
|
<span class="scoreStatVal scoreVP" id="vpRed">0</span>
|
||||||
|
<span class="scoreStatLabel">pts victoire</span>
|
||||||
|
</div>
|
||||||
|
<div class="scoreStat">
|
||||||
|
<span class="scoreStatVal scoreValue" id="scoreRed">0</span>
|
||||||
|
<span class="scoreStatLabel">planètes</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<img src="./graphism/logo_first_order.svg" alt="First Order" class="team-logo" />
|
<img src="./graphism/logo_first_order.svg" alt="First Order" class="team-logo" />
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Info rows -->
|
<!-- Info rows -->
|
||||||
@@ -134,6 +152,10 @@
|
|||||||
<span class="infoKey muted">Prochaine graine dans</span>
|
<span class="infoKey muted">Prochaine graine dans</span>
|
||||||
<code class="infoVal" id="refreshCountdown">--:--:--</code>
|
<code class="infoVal" id="refreshCountdown">--:--:--</code>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="infoRow">
|
||||||
|
<span class="infoKey muted">Base de données depuis</span>
|
||||||
|
<code class="infoVal" id="dbCreatedAt">—</code>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Planet stats (collapsible) -->
|
<!-- Planet stats (collapsible) -->
|
||||||
@@ -178,6 +200,28 @@
|
|||||||
</div>
|
</div>
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
|
<!-- Element bonus section -->
|
||||||
|
<div class="elemBonusSection">
|
||||||
|
<div class="elemBonusSectionTitle">⚡ Bonus de recharge planétaire</div>
|
||||||
|
<div class="elemBonusRow">
|
||||||
|
<span class="elemBonusTeam elemBonusTeam--blue">
|
||||||
|
<span class="elemBonusLabel">Résistance</span>
|
||||||
|
<span class="elemBonusVal" id="elemBonusBlue">0.00</span>
|
||||||
|
<span class="elemBonusUnit">%</span>
|
||||||
|
</span>
|
||||||
|
<span class="elemBonusSep">—</span>
|
||||||
|
<span class="elemBonusTeam elemBonusTeam--red">
|
||||||
|
<span class="elemBonusVal" id="elemBonusRed">0.00</span>
|
||||||
|
<span class="elemBonusUnit">%</span>
|
||||||
|
<span class="elemBonusLabel">Premier Ordre</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="elemBonusDetail">
|
||||||
|
<span class="elemBonusDetailLabel">Recharge effective (votre équipe) :</span>
|
||||||
|
<span class="elemBonusDetailVal" id="effectiveCooldown">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Admin / options section -->
|
<!-- Admin / options section -->
|
||||||
<div class="infoSection infoSection--options">
|
<div class="infoSection infoSection--options">
|
||||||
<details class="optionsDetails">
|
<details class="optionsDetails">
|
||||||
|
|||||||
@@ -65,3 +65,31 @@ export async function apiTickEconScores(seed, blue, red) {
|
|||||||
if (!res.ok) throw new Error("econ_tick_failed");
|
if (!res.ok) throw new Error("econ_tick_failed");
|
||||||
return res.json();
|
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();
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { resources } from "./planetEconomy.js";
|
import { resources, elements } from "./planetEconomy.js";
|
||||||
|
|
||||||
// ── Sort state ────────────────────────────────────────────────────────────────
|
// ── Sort state ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -64,6 +64,46 @@ export function computeTeamIncome(team, cells, resourceWorth) {
|
|||||||
return { total, byResource };
|
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<string, { discoveredBy: string, hasPlanet: boolean, planet: object|null }>} 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 ───────────────────────────────────────────
|
// ── Resource table for the sidebar ───────────────────────────────────────────
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { fnv1a32, hash2u32, mulberry32 } from "./rng.js";
|
import { fnv1a32, hash2u32, mulberry32 } from "./rng.js";
|
||||||
import { formatPlanet, generatePlanet } from "./planetGeneration.js";
|
import { formatPlanet, generatePlanet } from "./planetGeneration.js";
|
||||||
import { apiFetchConfig, apiFetchScores, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores } from "./api.js";
|
import { apiFetchConfig, apiFetchScores, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints } from "./api.js";
|
||||||
import { computeTeamIncome, renderResourceTable, setEconSort, getEconSort } from "./economy.js";
|
import { computeTeamIncome, computeTeamElementBonus, renderResourceTable, setEconSort, getEconSort } from "./economy.js";
|
||||||
|
|
||||||
// ── Constants ─────────────────────────────────────────────────────────────────
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@@ -27,6 +27,7 @@ export const GAME_CONFIG = {
|
|||||||
configReloadIntervalSeconds: 30,
|
configReloadIntervalSeconds: 30,
|
||||||
worldSeed: "",
|
worldSeed: "",
|
||||||
seedPeriodEndsAtUtc: "",
|
seedPeriodEndsAtUtc: "",
|
||||||
|
elementWorth: {},
|
||||||
resourceWorth: { common: {}, rare: {} },
|
resourceWorth: { common: {}, rare: {} },
|
||||||
};
|
};
|
||||||
window.GAME_CONFIG = GAME_CONFIG;
|
window.GAME_CONFIG = GAME_CONFIG;
|
||||||
@@ -149,6 +150,12 @@ const nextPeriodEl = document.getElementById("nextPeriodUtc");
|
|||||||
const resetCountEl = document.getElementById("refreshCountdown");
|
const resetCountEl = document.getElementById("refreshCountdown");
|
||||||
const scoreBlueEl = document.getElementById("scoreBlue");
|
const scoreBlueEl = document.getElementById("scoreBlue");
|
||||||
const scoreRedEl = document.getElementById("scoreRed");
|
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 incomeBlueEl = document.getElementById("incomeBlue");
|
||||||
const incomeRedEl = document.getElementById("incomeRed");
|
const incomeRedEl = document.getElementById("incomeRed");
|
||||||
const resourceTableEl = document.getElementById("resourceTableBody");
|
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.configReloadIntervalSeconds = Math.max(5, Number(data.configReloadIntervalSeconds) || 30);
|
||||||
GAME_CONFIG.worldSeed = String(data.worldSeed ?? "");
|
GAME_CONFIG.worldSeed = String(data.worldSeed ?? "");
|
||||||
GAME_CONFIG.seedPeriodEndsAtUtc = String(data.seedPeriodEndsAtUtc ?? "");
|
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") {
|
if (data.resourceWorth && typeof data.resourceWorth === "object") {
|
||||||
GAME_CONFIG.resourceWorth = data.resourceWorth;
|
GAME_CONFIG.resourceWorth = data.resourceWorth;
|
||||||
}
|
}
|
||||||
@@ -232,8 +242,77 @@ export function lockTeamSwitcher() {
|
|||||||
export async function fetchAndApplyScores() {
|
export async function fetchAndApplyScores() {
|
||||||
try {
|
try {
|
||||||
const { blue, red } = await apiFetchScores();
|
const { blue, red } = await apiFetchScores();
|
||||||
scoreBlueEl.textContent = String(blue ?? 0);
|
if (scoreBlueEl) scoreBlueEl.textContent = String(blue ?? 0);
|
||||||
scoreRedEl.textContent = String(red ?? 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 */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -385,7 +464,7 @@ function tickCooldown() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function startCooldown() {
|
export function startCooldown() {
|
||||||
const secs = GAME_CONFIG.clickCooldownSeconds;
|
const secs = getEffectiveCooldown();
|
||||||
if (secs <= 0) {
|
if (secs <= 0) {
|
||||||
teamCooldownEndMs[currentTeam] = 0;
|
teamCooldownEndMs[currentTeam] = 0;
|
||||||
countdownWrap.classList.add("hidden");
|
countdownWrap.classList.add("hidden");
|
||||||
|
|||||||
@@ -9,6 +9,10 @@ import {
|
|||||||
updateEconomyDisplay,
|
updateEconomyDisplay,
|
||||||
tickEconScore,
|
tickEconScore,
|
||||||
loadEconScores,
|
loadEconScores,
|
||||||
|
loadVictoryPoints,
|
||||||
|
loadDbInfo,
|
||||||
|
loadElementBonus,
|
||||||
|
tickElementBonus,
|
||||||
refreshFromServer,
|
refreshFromServer,
|
||||||
refreshGridDisplay,
|
refreshGridDisplay,
|
||||||
loadPlayfieldMask,
|
loadPlayfieldMask,
|
||||||
@@ -58,6 +62,7 @@ function scheduleScorePoll() {
|
|||||||
scorePollTimer = window.setTimeout(async () => {
|
scorePollTimer = window.setTimeout(async () => {
|
||||||
await fetchAndApplyScores();
|
await fetchAndApplyScores();
|
||||||
tickEconScore(ECON_TICK_SECONDS);
|
tickEconScore(ECON_TICK_SECONDS);
|
||||||
|
tickElementBonus();
|
||||||
scheduleScorePoll();
|
scheduleScorePoll();
|
||||||
}, ECON_TICK_SECONDS * 1_000);
|
}, ECON_TICK_SECONDS * 1_000);
|
||||||
}
|
}
|
||||||
@@ -144,6 +149,9 @@ async function boot() {
|
|||||||
await fetchGridForSeed(seedStr);
|
await fetchGridForSeed(seedStr);
|
||||||
await fetchAndApplyScores();
|
await fetchAndApplyScores();
|
||||||
await loadEconScores();
|
await loadEconScores();
|
||||||
|
await loadVictoryPoints();
|
||||||
|
await loadDbInfo();
|
||||||
|
await loadElementBonus();
|
||||||
updateEconomyDisplay();
|
updateEconomyDisplay();
|
||||||
} catch {
|
} catch {
|
||||||
hint.textContent = "API unavailable — start the Node server (docker-compose up --build).";
|
hint.textContent = "API unavailable — start the Node server (docker-compose up --build).";
|
||||||
@@ -159,6 +167,9 @@ async function boot() {
|
|||||||
scheduleConfigPoll();
|
scheduleConfigPoll();
|
||||||
scheduleScorePoll();
|
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
|
// Refresh grid every second so all clients see new tiles promptly
|
||||||
setInterval(refreshGridDisplay, 1_000);
|
setInterval(refreshGridDisplay, 1_000);
|
||||||
}
|
}
|
||||||
|
|||||||
170
public/style.css
170
public/style.css
@@ -276,61 +276,94 @@ body {
|
|||||||
.scoreBoard {
|
.scoreBoard {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: space-between;
|
||||||
gap: 16px;
|
gap: 8px;
|
||||||
padding: 10px 16px;
|
padding: 10px 12px;
|
||||||
border-radius: 14px;
|
border-radius: 14px;
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||||
background: rgba(255, 255, 255, 0.04);
|
background: rgba(255, 255, 255, 0.04);
|
||||||
font-size: 22px;
|
|
||||||
font-weight: 800;
|
|
||||||
font-family: "Courier New", Courier, monospace;
|
font-family: "Courier New", Courier, monospace;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
letter-spacing: 0.5px;
|
}
|
||||||
|
|
||||||
|
.team-logo {
|
||||||
|
width: 54px;
|
||||||
|
height: 54px;
|
||||||
|
display: block;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scoreTeam {
|
.scoreTeam {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: 4px;
|
||||||
gap: 10px;
|
flex: 1;
|
||||||
}
|
|
||||||
|
|
||||||
.team-logo {
|
|
||||||
width: 64px;
|
|
||||||
height: 64px;
|
|
||||||
display: block;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.scoreTeamName {
|
.scoreTeamName {
|
||||||
font-size: 12px;
|
font-size: 10px;
|
||||||
font-weight: 700;
|
font-weight: 700;
|
||||||
text-transform: uppercase;
|
text-transform: uppercase;
|
||||||
letter-spacing: 0.08em;
|
letter-spacing: 0.08em;
|
||||||
opacity: 0.8;
|
opacity: 0.75;
|
||||||
}
|
}
|
||||||
|
|
||||||
.scoreTeam--blue .scoreTeamName {
|
.scoreTeam--blue .scoreTeamName { color: rgba(90, 200, 255, 0.9); }
|
||||||
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 {
|
.scoreTeam--blue .scoreValue {
|
||||||
color: rgba(90, 200, 255, 1);
|
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 {
|
.scoreTeam--blue .scoreVP {
|
||||||
color: rgba(220, 75, 85, 0.9);
|
color: rgba(130, 230, 130, 1);
|
||||||
|
text-shadow: 0 0 16px rgba(90, 200, 130, 0.35);
|
||||||
}
|
}
|
||||||
|
|
||||||
.scoreTeam--red .scoreValue {
|
.scoreTeam--red .scoreValue {
|
||||||
color: rgba(220, 75, 85, 1);
|
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 {
|
.scoreSep {
|
||||||
opacity: 0.4;
|
opacity: 0.35;
|
||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
|
font-weight: 800;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ── Main app layout ──────────────────────────────────────────────────────── */
|
/* ── Main app layout ──────────────────────────────────────────────────────── */
|
||||||
@@ -402,33 +435,36 @@ body {
|
|||||||
.infoTable {
|
.infoTable {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 6px;
|
gap: 4px;
|
||||||
font-family: "Courier New", Courier, monospace;
|
font-family: "Courier New", Courier, monospace;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoRow {
|
.infoRow {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: baseline;
|
align-items: baseline;
|
||||||
gap: 8px;
|
gap: 0;
|
||||||
min-height: 22px;
|
min-height: 22px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoKey {
|
.infoKey {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 55%;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
opacity: 0.65;
|
opacity: 0.65;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
min-width: 130px;
|
|
||||||
text-align: right;
|
text-align: right;
|
||||||
|
padding-right: 8px;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoVal {
|
.infoVal {
|
||||||
flex: 1;
|
flex: 0 0 45%;
|
||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
font-family: "Courier New", Courier, monospace;
|
font-family: "Courier New", Courier, monospace;
|
||||||
|
text-align: left;
|
||||||
}
|
}
|
||||||
|
|
||||||
.infoVal code {
|
.infoVal code {
|
||||||
@@ -801,6 +837,75 @@ button:hover {
|
|||||||
font-weight: 700;
|
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 ──────────────────────────────────────────────── */
|
/* ── Options / admin section ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
.infoSection--options {
|
.infoSection--options {
|
||||||
@@ -984,11 +1089,14 @@ canvas {
|
|||||||
/* Scoreboard wraps nicely on narrow screens */
|
/* Scoreboard wraps nicely on narrow screens */
|
||||||
.scoreBoard {
|
.scoreBoard {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
font-size: 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.team-logo {
|
.team-logo {
|
||||||
width: 44px;
|
width: 40px;
|
||||||
height: 44px;
|
height: 40px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.scoreStatVal {
|
||||||
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -13,6 +13,7 @@ let cached = {
|
|||||||
databaseWipeoutIntervalSeconds: 21600,
|
databaseWipeoutIntervalSeconds: 21600,
|
||||||
debugModeForTeams: true,
|
debugModeForTeams: true,
|
||||||
configReloadIntervalSeconds: 30,
|
configReloadIntervalSeconds: 30,
|
||||||
|
elementWorth: {},
|
||||||
resourceWorth: { common: {}, rare: {} },
|
resourceWorth: { common: {}, rare: {} },
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -47,6 +48,9 @@ export function loadConfigFile() {
|
|||||||
if (typeof j.configReloadIntervalSeconds === "number" && j.configReloadIntervalSeconds >= 5) {
|
if (typeof j.configReloadIntervalSeconds === "number" && j.configReloadIntervalSeconds >= 5) {
|
||||||
cached.configReloadIntervalSeconds = j.configReloadIntervalSeconds;
|
cached.configReloadIntervalSeconds = j.configReloadIntervalSeconds;
|
||||||
}
|
}
|
||||||
|
if (j.elementWorth && typeof j.elementWorth === "object") {
|
||||||
|
cached.elementWorth = j.elementWorth;
|
||||||
|
}
|
||||||
if (j.resourceWorth && typeof j.resourceWorth === "object") {
|
if (j.resourceWorth && typeof j.resourceWorth === "object") {
|
||||||
cached.resourceWorth = j.resourceWorth;
|
cached.resourceWorth = j.resourceWorth;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -37,6 +37,31 @@ export async function initGameSchema() {
|
|||||||
PRIMARY KEY (world_seed, team)
|
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(`
|
await pool.query(`
|
||||||
ALTER TABLE grid_cells ADD COLUMN IF NOT EXISTS discovered_by TEXT;
|
ALTER TABLE grid_cells ADD COLUMN IF NOT EXISTS discovered_by TEXT;
|
||||||
UPDATE grid_cells SET discovered_by = 'blue' WHERE discovered_by IS NULL;
|
UPDATE grid_cells SET discovered_by = 'blue' WHERE discovered_by IS NULL;
|
||||||
@@ -65,9 +90,30 @@ export async function ensureSeedEpoch() {
|
|||||||
return worldSeed;
|
return worldSeed;
|
||||||
}
|
}
|
||||||
if (seedSlot !== lastSeedSlot) {
|
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("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 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]);
|
||||||
console.log(`[world] Slot ${lastSeedSlot} → ${seedSlot}; grid wiped, old cooldowns cleared.`);
|
console.log(`[world] Slot ${lastSeedSlot} → ${seedSlot}; grid wiped, old cooldowns cleared.`);
|
||||||
lastSeedSlot = seedSlot;
|
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 ────────────────────────────────────────────────────────────────────
|
// ── Scores ────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
export async function getScores(worldSeed) {
|
export async function getScores(worldSeed) {
|
||||||
|
|||||||
@@ -11,6 +11,10 @@ import {
|
|||||||
getScores,
|
getScores,
|
||||||
getEconScores,
|
getEconScores,
|
||||||
addEconScore,
|
addEconScore,
|
||||||
|
getElementBonus,
|
||||||
|
setElementBonus,
|
||||||
|
getDbCreatedAt,
|
||||||
|
getVictoryPoints,
|
||||||
} from "../db/gameDb.js";
|
} from "../db/gameDb.js";
|
||||||
import { computeCell, rowToCellPayload } from "../helpers/cell.js";
|
import { computeCell, rowToCellPayload } from "../helpers/cell.js";
|
||||||
|
|
||||||
@@ -46,6 +50,7 @@ router.get("/config", async (req, res) => {
|
|||||||
seedPeriodStartsAtUtc: ws.seedPeriodStartsAtUtc,
|
seedPeriodStartsAtUtc: ws.seedPeriodStartsAtUtc,
|
||||||
teamCooldownRemaining,
|
teamCooldownRemaining,
|
||||||
resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} },
|
resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} },
|
||||||
|
elementWorth: cfg.elementWorth ?? {},
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error(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
|
// GET /api/scores
|
||||||
router.get("/scores", async (_req, res) => {
|
router.get("/scores", async (_req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Reference in New Issue
Block a user