refacto: Changing actions reset windows + display

This commit is contained in:
gauvainboiche
2026-04-02 15:44:58 +02:00
parent e573ea0d37
commit 3cdb6385a7
9 changed files with 70 additions and 16 deletions
+1
View File
@@ -1,6 +1,7 @@
{ {
"dailyActionQuota": 100, "dailyActionQuota": 100,
"teamActionQuota": 100, "teamActionQuota": 100,
"actionsResetIntervalHours": 24,
"databaseWipeoutIntervalSeconds": 604800, "databaseWipeoutIntervalSeconds": 604800,
"configReloadIntervalSeconds": 30, "configReloadIntervalSeconds": 30,
"elementWorth": { "elementWorth": {
+2 -2
View File
@@ -170,8 +170,8 @@
<code class="infoVal" id="worldSeedDisplay"></code> <code class="infoVal" id="worldSeedDisplay"></code>
</div> </div>
<div class="infoRow"> <div class="infoRow">
<span class="infoKey muted">Prochaine graine (UTC)</span> <span class="infoKey muted">Réinitialisation des actions dans</span>
<code class="infoVal" id="nextPeriodUtc"></code> <code class="infoVal" id="actionsResetCountdown">--:--:--</code>
</div> </div>
<div class="infoRow"> <div class="infoRow">
<span class="infoKey muted">Prochaine graine dans</span> <span class="infoKey muted">Prochaine graine dans</span>
+29 -4
View File
@@ -22,6 +22,7 @@ const COLOR_OPPONENT_GREY = "rgba(95, 98, 110, 0.72)";
export const GAME_CONFIG = { export const GAME_CONFIG = {
dailyActionQuota: 100, dailyActionQuota: 100,
actionsResetIntervalHours: 12,
databaseWipeoutIntervalSeconds: 21600, databaseWipeoutIntervalSeconds: 21600,
configReloadIntervalSeconds: 30, configReloadIntervalSeconds: 30,
worldSeed: "", worldSeed: "",
@@ -203,7 +204,7 @@ const countdownEl = document.getElementById("countdown");
const countdownWrap = document.getElementById("countdownWrap"); const countdownWrap = document.getElementById("countdownWrap");
const cooldownCfgEl = document.getElementById("cooldownConfig"); const cooldownCfgEl = document.getElementById("cooldownConfig");
const seedDisplayEl = document.getElementById("worldSeedDisplay"); const seedDisplayEl = document.getElementById("worldSeedDisplay");
const nextPeriodEl = document.getElementById("nextPeriodUtc"); const actionsResetCountdownEl = document.getElementById("actionsResetCountdown");
const resetCountEl = document.getElementById("refreshCountdown"); const resetCountEl = document.getElementById("refreshCountdown");
const vpBlueEl = document.getElementById("vpBlue"); const vpBlueEl = document.getElementById("vpBlue");
const vpRedEl = document.getElementById("vpRed"); const vpRedEl = document.getElementById("vpRed");
@@ -263,6 +264,9 @@ export function isOwnTile(key) { const m = cellMeta(key); return m !== nul
export function applyConfigPayload(data) { export function applyConfigPayload(data) {
GAME_CONFIG.dailyActionQuota = Number(data.dailyActionQuota) || 100; GAME_CONFIG.dailyActionQuota = Number(data.dailyActionQuota) || 100;
if (data.actionsResetIntervalHours) {
GAME_CONFIG.actionsResetIntervalHours = Number(data.actionsResetIntervalHours) || 12;
}
GAME_CONFIG.databaseWipeoutIntervalSeconds = Number(data.databaseWipeoutIntervalSeconds) || 21600; GAME_CONFIG.databaseWipeoutIntervalSeconds = Number(data.databaseWipeoutIntervalSeconds) || 21600;
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 ?? "");
@@ -287,11 +291,9 @@ export function applyConfigPayload(data) {
cooldownCfgEl.textContent = String(GAME_CONFIG.dailyActionQuota); cooldownCfgEl.textContent = String(GAME_CONFIG.dailyActionQuota);
seedDisplayEl.textContent = GAME_CONFIG.worldSeed || "—"; seedDisplayEl.textContent = GAME_CONFIG.worldSeed || "—";
nextPeriodEl.textContent = GAME_CONFIG.seedPeriodEndsAtUtc
? new Date(GAME_CONFIG.seedPeriodEndsAtUtc).toISOString().replace("T", " ").slice(0, 19) + "Z"
: "—";
updateResetCountdown(); updateResetCountdown();
updateActionsResetCountdown();
} }
@@ -311,6 +313,29 @@ export function updateResetCountdown() {
String(ss).padStart(2, "0"); String(ss).padStart(2, "0");
} }
/** Computes time until next actions-reset based on the configured interval and updates the display. */
export function updateActionsResetCountdown() {
if (!actionsResetCountdownEl) return;
const intervalHours = GAME_CONFIG.actionsResetIntervalHours ?? 12;
const now = Date.now();
const date = new Date(now);
const utcHours = date.getUTCHours();
const slotsPassed = Math.floor(utcHours / intervalHours);
const nextSlotHour = (slotsPassed + 1) * intervalHours;
const next = nextSlotHour < 24
? Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate(), nextSlotHour, 0, 0, 0)
: Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate() + 1, 0, 0, 0, 0);
const diff = next - now;
const s = Math.floor(diff / 1000);
const hh = Math.floor(s / 3600);
const mm = Math.floor((s % 3600) / 60);
const ss = s % 60;
actionsResetCountdownEl.textContent =
String(hh).padStart(2, "0") + ":" +
String(mm).padStart(2, "0") + ":" +
String(ss).padStart(2, "0");
}
// ── Scores ──────────────────────────────────────────────────────────────────── // ── Scores ────────────────────────────────────────────────────────────────────
+3 -1
View File
@@ -2,6 +2,7 @@ import {
GAME_CONFIG, GAME_CONFIG,
seedStr, seedStr,
updateResetCountdown, updateResetCountdown,
updateActionsResetCountdown,
fetchConfig, fetchConfig,
fetchGridForSeed, fetchGridForSeed,
fetchAndApplyActivePlayers, fetchAndApplyActivePlayers,
@@ -221,8 +222,9 @@ async function boot() {
draw(); draw();
if (resetTimer) clearInterval(resetTimer); if (resetTimer) clearInterval(resetTimer);
resetTimer = setInterval(updateResetCountdown, 1_000); resetTimer = setInterval(() => { updateResetCountdown(); updateActionsResetCountdown(); }, 1_000);
updateResetCountdown(); updateResetCountdown();
updateActionsResetCountdown();
startRealtimeFlow(); startRealtimeFlow();
+7
View File
@@ -8,9 +8,12 @@ const CONFIG_FILE_PATH =
process.env.CONFIG_FILE_PATH ?? path.join(__dirname, "..", "config", "game.settings.json"); process.env.CONFIG_FILE_PATH ?? path.join(__dirname, "..", "config", "game.settings.json");
/** @type {{ dailyActionQuota: number, teamActionQuota: number, databaseWipeoutIntervalSeconds: number, configReloadIntervalSeconds: number, resourceWorth: object, militaryPower: object }} */ /** @type {{ dailyActionQuota: number, teamActionQuota: number, databaseWipeoutIntervalSeconds: number, configReloadIntervalSeconds: number, resourceWorth: object, militaryPower: object }} */
const VALID_RESET_HOURS = new Set([1, 2, 3, 4, 6, 8, 12, 24]);
let cached = { let cached = {
dailyActionQuota: 100, dailyActionQuota: 100,
teamActionQuota: 100, teamActionQuota: 100,
actionsResetIntervalHours: 12,
databaseWipeoutIntervalSeconds: 21600, databaseWipeoutIntervalSeconds: 21600,
configReloadIntervalSeconds: 30, configReloadIntervalSeconds: 30,
elementWorth: {}, elementWorth: {},
@@ -44,6 +47,10 @@ export function loadConfigFile() {
if (typeof j.teamActionQuota === "number" && j.teamActionQuota >= 1) { if (typeof j.teamActionQuota === "number" && j.teamActionQuota >= 1) {
cached.teamActionQuota = Math.floor(j.teamActionQuota); cached.teamActionQuota = Math.floor(j.teamActionQuota);
} }
if (typeof j.actionsResetIntervalHours === "number") {
const v = Math.floor(j.actionsResetIntervalHours);
if (VALID_RESET_HOURS.has(v)) cached.actionsResetIntervalHours = v;
}
if (typeof j.databaseWipeoutIntervalSeconds === "number" && j.databaseWipeoutIntervalSeconds >= 60) { if (typeof j.databaseWipeoutIntervalSeconds === "number" && j.databaseWipeoutIntervalSeconds >= 60) {
cached.databaseWipeoutIntervalSeconds = j.databaseWipeoutIntervalSeconds; cached.databaseWipeoutIntervalSeconds = j.databaseWipeoutIntervalSeconds;
} }
+2 -2
View File
@@ -1,7 +1,7 @@
import { pool } from "./pools.js"; import { pool } from "./pools.js";
import { loadConfigFile, getConfig } from "../configLoader.js"; import { loadConfigFile, getConfig } from "../configLoader.js";
import { computeWorldSeedState } from "../worldSeed.js"; import { computeWorldSeedState } from "../worldSeed.js";
import { nextNoonUtc, resetAllUserActions, getUsersByIds } from "./usersDb.js"; import { nextResetUtc, resetAllUserActions, getUsersByIds } from "./usersDb.js";
let lastSeedSlot = null; let lastSeedSlot = null;
@@ -187,7 +187,7 @@ export async function ensureSeedEpoch() {
await pool.query("DELETE FROM team_cell_visibility WHERE world_seed != $1", [worldSeed]); await pool.query("DELETE FROM team_cell_visibility WHERE world_seed != $1", [worldSeed]);
// Reset both team and user quotas to server defaults (no bonuses) for the new period // Reset both team and user quotas to server defaults (no bonuses) for the new period
const cfg = getConfig(); const cfg = getConfig();
const nextNoon = nextNoonUtc().toISOString(); const nextNoon = nextResetUtc(cfg.actionsResetIntervalHours ?? 12).toISOString();
await pool.query( await pool.query(
`INSERT INTO team_action_quota (team, actions_remaining, quota_reset_at) `INSERT INTO team_action_quota (team, actions_remaining, quota_reset_at)
VALUES ('blue', $1, $2), ('red', $1, $2) VALUES ('blue', $1, $2), ('red', $1, $2)
+20 -4
View File
@@ -29,12 +29,28 @@ export async function initUserActionQuotaSchema() {
/** Returns the next noon (12:00:00) UTC after the current moment. */ /** Returns the next noon (12:00:00) UTC after the current moment. */
export function nextNoonUtc() { export function nextNoonUtc() {
return nextResetUtc(12);
}
/**
* Returns the next reset timestamp UTC based on a repeating interval.
* @param {number} intervalHours - must be a divisor of 24 (1,2,3,4,6,8,12,24)
*/
export function nextResetUtc(intervalHours) {
const now = new Date(); const now = new Date();
const noon = new Date(Date.UTC( const utcHours = now.getUTCHours();
now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 12, 0, 0, 0 const slotsPassed = Math.floor(utcHours / intervalHours);
const nextSlotHour = (slotsPassed + 1) * intervalHours;
if (nextSlotHour < 24) {
return new Date(Date.UTC(
now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(),
nextSlotHour, 0, 0, 0
));
}
return new Date(Date.UTC(
now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1,
0, 0, 0, 0
)); ));
if (noon <= now) noon.setUTCDate(noon.getUTCDate() + 1);
return noon;
} }
// ── Queries ─────────────────────────────────────────────────────────────────── // ── Queries ───────────────────────────────────────────────────────────────────
+2
View File
@@ -14,6 +14,7 @@ function makeConfigSignature(cfg) {
return JSON.stringify({ return JSON.stringify({
dailyActionQuota: cfg.dailyActionQuota, dailyActionQuota: cfg.dailyActionQuota,
teamActionQuota: cfg.teamActionQuota, teamActionQuota: cfg.teamActionQuota,
actionsResetIntervalHours: cfg.actionsResetIntervalHours,
databaseWipeoutIntervalSeconds: cfg.databaseWipeoutIntervalSeconds, databaseWipeoutIntervalSeconds: cfg.databaseWipeoutIntervalSeconds,
configReloadIntervalSeconds: cfg.configReloadIntervalSeconds, configReloadIntervalSeconds: cfg.configReloadIntervalSeconds,
elementWorth: cfg.elementWorth ?? {}, elementWorth: cfg.elementWorth ?? {},
@@ -41,6 +42,7 @@ function scheduleConfigPoll() {
config: { config: {
dailyActionQuota: cfg.dailyActionQuota, dailyActionQuota: cfg.dailyActionQuota,
teamActionQuota: cfg.teamActionQuota, teamActionQuota: cfg.teamActionQuota,
actionsResetIntervalHours: cfg.actionsResetIntervalHours,
databaseWipeoutIntervalSeconds: cfg.databaseWipeoutIntervalSeconds, databaseWipeoutIntervalSeconds: cfg.databaseWipeoutIntervalSeconds,
configReloadIntervalSeconds: cfg.configReloadIntervalSeconds, configReloadIntervalSeconds: cfg.configReloadIntervalSeconds,
elementWorth: cfg.elementWorth ?? {}, elementWorth: cfg.elementWorth ?? {},
+4 -3
View File
@@ -34,7 +34,7 @@ import {
getTeamVisibleCells, getTeamVisibleCells,
} from "../db/gameDb.js"; } from "../db/gameDb.js";
import { import {
nextNoonUtc, nextResetUtc,
getUserActionsRow, getUserActionsRow,
resetUserActions, resetUserActions,
decrementUserActions, decrementUserActions,
@@ -100,6 +100,7 @@ router.get("/config", async (req, res) => {
seedPeriodStartsAtUtc: ws.seedPeriodStartsAtUtc, seedPeriodStartsAtUtc: ws.seedPeriodStartsAtUtc,
actionsRemaining, actionsRemaining,
teamActionsRemaining, teamActionsRemaining,
actionsResetIntervalHours: cfg.actionsResetIntervalHours ?? 12,
resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} }, resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} },
elementWorth: cfg.elementWorth ?? {}, elementWorth: cfg.elementWorth ?? {},
militaryPower: cfg.militaryPower ?? {}, militaryPower: cfg.militaryPower ?? {},
@@ -170,7 +171,7 @@ router.post("/cell/reveal", authMiddleware, async (req, res) => {
const now = new Date(); const now = new Date();
const quotaRow = await getUserActionsRow(userId); const quotaRow = await getUserActionsRow(userId);
if (!quotaRow || new Date(quotaRow.quota_reset_at) <= now) { if (!quotaRow || new Date(quotaRow.quota_reset_at) <= now) {
await resetUserActions(userId, effectiveQuota - 1, nextNoonUtc().toISOString()); await resetUserActions(userId, effectiveQuota - 1, nextResetUtc(cfg.actionsResetIntervalHours ?? 12).toISOString());
} else { } else {
const updated = await decrementUserActions(userId); const updated = await decrementUserActions(userId);
if (!updated) { if (!updated) {
@@ -247,7 +248,7 @@ router.post("/cell/capture", authMiddleware, async (req, res) => {
if (totalActions < cost) { if (totalActions < cost) {
return res.status(429).json({ error: "team_quota_exhausted", cost, teamActionsRemaining: totalActions }); return res.status(429).json({ error: "team_quota_exhausted", cost, teamActionsRemaining: totalActions });
} }
await resetTeamActions(team, totalActions - cost, nextNoonUtc().toISOString()); await resetTeamActions(team, totalActions - cost, nextResetUtc(cfg.actionsResetIntervalHours ?? 12).toISOString());
} else { } else {
if (teamRow.actions_remaining < cost) { if (teamRow.actions_remaining < cost) {
return res.status(429).json({ error: "team_quota_exhausted", cost, teamActionsRemaining: teamRow.actions_remaining }); return res.status(429).json({ error: "team_quota_exhausted", cost, teamActionsRemaining: teamRow.actions_remaining });