Files
star-wars-wild-space/server/econTick.js
T
2026-04-03 14:25:19 +02:00

103 lines
4.5 KiB
JavaScript

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);
}