103 lines
4.5 KiB
JavaScript
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);
|
|
}
|