import { getConfig } from "./configLoader.js"; import { ensureSeedEpoch, getGridCells, addEconScore, setElementBonus, getElementBonus, getMilitaryDeductions, resetTeamActions, } from "./db/gameDb.js"; import { computeTeamIncome, computeTeamElementBonus, computeTeamMilitaryPower } from "./helpers/economy.js"; import { nextResetUtc, resetUserActionsByTeam } from "./db/usersDb.js"; import { buildRealtimeSnapshot } from "./realtimeSnapshot.js"; import { broadcast, broadcastToTeam } from "./ws/hub.js"; const TICK_SECONDS = 5; const TEAMS = ["blue", "red"]; let lastTickSeed = null; let lastQuotaSlot = null; function currentQuotaSlot(intervalSeconds, epochSec) { const nowSec = Math.floor(Date.now() / 1000); return Math.floor((nowSec - epochSec) / intervalSeconds); } /** * Starts the server-side economy tick loop. * Runs every TICK_SECONDS, reads the current grid from the DB, computes * income and element bonus for each team, and persists the results. * This runs independently of any connected browser. */ export function startEconTick() { setInterval(async () => { try { const worldSeed = await ensureSeedEpoch(); if (lastTickSeed && lastTickSeed !== worldSeed) { broadcast("seed-changed", { worldSeed }); } lastTickSeed = worldSeed; const rows = await getGridCells(worldSeed); const cfg = getConfig(); const resourceWorth = cfg.resourceWorth ?? { common: {}, rare: {} }; const elementWorth = cfg.elementWorth ?? {}; // ── Economic score deltas ───────────────────────────────────────────── const blueIncome = computeTeamIncome("blue", rows, resourceWorth); const redIncome = computeTeamIncome("red", rows, resourceWorth); const blueDelta = blueIncome * TICK_SECONDS; const redDelta = redIncome * TICK_SECONDS; if (blueDelta > 0) await addEconScore(worldSeed, "blue", blueDelta); if (redDelta > 0) await addEconScore(worldSeed, "red", redDelta); // ── Element bonus (overwrites; reflects current grid state) ────────── const blueBonus = computeTeamElementBonus("blue", rows, elementWorth); const redBonus = computeTeamElementBonus("red", rows, elementWorth); await setElementBonus(worldSeed, "blue", blueBonus); await setElementBonus(worldSeed, "red", redBonus); // ── Actions quota reset (on every new interval slot) ───────────────── const intervalSec = cfg.actionsResetIntervalSeconds ?? 3600; const epochSec = cfg.timingEpochSec ?? 0; const quotaSlot = currentQuotaSlot(intervalSec, epochSec); if (lastQuotaSlot !== null && quotaSlot !== lastQuotaSlot) { const nextReset = nextResetUtc(intervalSec, epochSec).toISOString(); const elementBonus = await getElementBonus(worldSeed); const milDeductions = await getMilitaryDeductions(worldSeed); for (const team of TEAMS) { // ── Team quota: base + military bonus, written to DB ────────────── const milPower = computeTeamMilitaryPower(team, rows, cfg.militaryPower ?? {}); const milNet = milPower - (milDeductions[team] ?? 0); // milNet is in "billions of military-weighted population". // The UI displays milNet * 1000; the bonus formula applies to that display scale: // floor(displayedValue / 1000) = floor(milNet * 1000 / 1000) = floor(milNet). const milBonus = Math.floor(Math.max(0, milNet)); const teamQuota = cfg.teamActionQuota + milBonus; await resetTeamActions(team, teamQuota, nextReset); broadcastToTeam(team, "team-quota-updated", { team, actionsRemaining: teamQuota }); // ── Per-user quota: element bonus scales the daily quota ────────── const teamElemBonus = elementBonus[team] ?? 0; const effectiveUserQuota = Math.floor(cfg.dailyActionQuota * (1 + teamElemBonus / 100)); await resetUserActionsByTeam(team, effectiveUserQuota, nextReset); } console.log(`[quota] Slot ${lastQuotaSlot} → ${quotaSlot}; user & team quotas reset.`); } lastQuotaSlot = quotaSlot; const snapshot = await buildRealtimeSnapshot(worldSeed); broadcast("snapshot", snapshot); } catch (e) { console.error("[econ tick]", e.message); } }, TICK_SECONDS * 1_000); }