From 3cdb6385a79406aaf55c2060cb21bb76a062af20 Mon Sep 17 00:00:00 2001 From: gauvainboiche Date: Thu, 2 Apr 2026 15:44:58 +0200 Subject: [PATCH] refacto: Changing actions reset windows + display --- config/game.settings.json | 1 + public/index.html | 4 ++-- public/src/game.js | 33 +++++++++++++++++++++++++++++---- public/src/main.js | 4 +++- server/configLoader.js | 7 +++++++ server/db/gameDb.js | 4 ++-- server/db/usersDb.js | 24 ++++++++++++++++++++---- server/index.js | 2 ++ server/routes/game.js | 7 ++++--- 9 files changed, 70 insertions(+), 16 deletions(-) diff --git a/config/game.settings.json b/config/game.settings.json index 36ba2f4..a173bb7 100644 --- a/config/game.settings.json +++ b/config/game.settings.json @@ -1,6 +1,7 @@ { "dailyActionQuota": 100, "teamActionQuota": 100, + "actionsResetIntervalHours": 24, "databaseWipeoutIntervalSeconds": 604800, "configReloadIntervalSeconds": 30, "elementWorth": { diff --git a/public/index.html b/public/index.html index 6e0c674..c6b4185 100644 --- a/public/index.html +++ b/public/index.html @@ -170,8 +170,8 @@
- Prochaine graine (UTC) - + Réinitialisation des actions dans + --:--:--
Prochaine graine dans diff --git a/public/src/game.js b/public/src/game.js index c7c3732..7e853e1 100644 --- a/public/src/game.js +++ b/public/src/game.js @@ -22,6 +22,7 @@ const COLOR_OPPONENT_GREY = "rgba(95, 98, 110, 0.72)"; export const GAME_CONFIG = { dailyActionQuota: 100, + actionsResetIntervalHours: 12, databaseWipeoutIntervalSeconds: 21600, configReloadIntervalSeconds: 30, worldSeed: "", @@ -203,7 +204,7 @@ const countdownEl = document.getElementById("countdown"); const countdownWrap = document.getElementById("countdownWrap"); const cooldownCfgEl = document.getElementById("cooldownConfig"); const seedDisplayEl = document.getElementById("worldSeedDisplay"); -const nextPeriodEl = document.getElementById("nextPeriodUtc"); +const actionsResetCountdownEl = document.getElementById("actionsResetCountdown"); const resetCountEl = document.getElementById("refreshCountdown"); const vpBlueEl = document.getElementById("vpBlue"); const vpRedEl = document.getElementById("vpRed"); @@ -263,6 +264,9 @@ export function isOwnTile(key) { const m = cellMeta(key); return m !== nul export function applyConfigPayload(data) { 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.configReloadIntervalSeconds = Math.max(5, Number(data.configReloadIntervalSeconds) || 30); GAME_CONFIG.worldSeed = String(data.worldSeed ?? ""); @@ -287,11 +291,9 @@ export function applyConfigPayload(data) { cooldownCfgEl.textContent = String(GAME_CONFIG.dailyActionQuota); seedDisplayEl.textContent = GAME_CONFIG.worldSeed || "—"; - nextPeriodEl.textContent = GAME_CONFIG.seedPeriodEndsAtUtc - ? new Date(GAME_CONFIG.seedPeriodEndsAtUtc).toISOString().replace("T", " ").slice(0, 19) + "Z" - : "—"; updateResetCountdown(); + updateActionsResetCountdown(); } @@ -311,6 +313,29 @@ export function updateResetCountdown() { 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 ──────────────────────────────────────────────────────────────────── diff --git a/public/src/main.js b/public/src/main.js index db4809c..64849b8 100644 --- a/public/src/main.js +++ b/public/src/main.js @@ -2,6 +2,7 @@ import { GAME_CONFIG, seedStr, updateResetCountdown, + updateActionsResetCountdown, fetchConfig, fetchGridForSeed, fetchAndApplyActivePlayers, @@ -221,8 +222,9 @@ async function boot() { draw(); if (resetTimer) clearInterval(resetTimer); - resetTimer = setInterval(updateResetCountdown, 1_000); + resetTimer = setInterval(() => { updateResetCountdown(); updateActionsResetCountdown(); }, 1_000); updateResetCountdown(); + updateActionsResetCountdown(); startRealtimeFlow(); diff --git a/server/configLoader.js b/server/configLoader.js index 397bfe8..6299f16 100644 --- a/server/configLoader.js +++ b/server/configLoader.js @@ -8,9 +8,12 @@ const CONFIG_FILE_PATH = 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 }} */ +const VALID_RESET_HOURS = new Set([1, 2, 3, 4, 6, 8, 12, 24]); + let cached = { dailyActionQuota: 100, teamActionQuota: 100, + actionsResetIntervalHours: 12, databaseWipeoutIntervalSeconds: 21600, configReloadIntervalSeconds: 30, elementWorth: {}, @@ -44,6 +47,10 @@ export function loadConfigFile() { if (typeof j.teamActionQuota === "number" && j.teamActionQuota >= 1) { 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) { cached.databaseWipeoutIntervalSeconds = j.databaseWipeoutIntervalSeconds; } diff --git a/server/db/gameDb.js b/server/db/gameDb.js index 69721b1..8b28ca4 100644 --- a/server/db/gameDb.js +++ b/server/db/gameDb.js @@ -1,7 +1,7 @@ import { pool } from "./pools.js"; import { loadConfigFile, getConfig } from "../configLoader.js"; import { computeWorldSeedState } from "../worldSeed.js"; -import { nextNoonUtc, resetAllUserActions, getUsersByIds } from "./usersDb.js"; +import { nextResetUtc, resetAllUserActions, getUsersByIds } from "./usersDb.js"; let lastSeedSlot = null; @@ -187,7 +187,7 @@ export async function ensureSeedEpoch() { 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 const cfg = getConfig(); - const nextNoon = nextNoonUtc().toISOString(); + const nextNoon = nextResetUtc(cfg.actionsResetIntervalHours ?? 12).toISOString(); await pool.query( `INSERT INTO team_action_quota (team, actions_remaining, quota_reset_at) VALUES ('blue', $1, $2), ('red', $1, $2) diff --git a/server/db/usersDb.js b/server/db/usersDb.js index b2403f0..f26d0a2 100644 --- a/server/db/usersDb.js +++ b/server/db/usersDb.js @@ -29,12 +29,28 @@ export async function initUserActionQuotaSchema() { /** Returns the next noon (12:00:00) UTC after the current moment. */ 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 noon = new Date(Date.UTC( - now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate(), 12, 0, 0, 0 + const utcHours = now.getUTCHours(); + 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 ─────────────────────────────────────────────────────────────────── diff --git a/server/index.js b/server/index.js index 5a5d70d..fda0ec0 100644 --- a/server/index.js +++ b/server/index.js @@ -14,6 +14,7 @@ function makeConfigSignature(cfg) { return JSON.stringify({ dailyActionQuota: cfg.dailyActionQuota, teamActionQuota: cfg.teamActionQuota, + actionsResetIntervalHours: cfg.actionsResetIntervalHours, databaseWipeoutIntervalSeconds: cfg.databaseWipeoutIntervalSeconds, configReloadIntervalSeconds: cfg.configReloadIntervalSeconds, elementWorth: cfg.elementWorth ?? {}, @@ -41,6 +42,7 @@ function scheduleConfigPoll() { config: { dailyActionQuota: cfg.dailyActionQuota, teamActionQuota: cfg.teamActionQuota, + actionsResetIntervalHours: cfg.actionsResetIntervalHours, databaseWipeoutIntervalSeconds: cfg.databaseWipeoutIntervalSeconds, configReloadIntervalSeconds: cfg.configReloadIntervalSeconds, elementWorth: cfg.elementWorth ?? {}, diff --git a/server/routes/game.js b/server/routes/game.js index a02e94f..2ae6108 100644 --- a/server/routes/game.js +++ b/server/routes/game.js @@ -34,7 +34,7 @@ import { getTeamVisibleCells, } from "../db/gameDb.js"; import { - nextNoonUtc, + nextResetUtc, getUserActionsRow, resetUserActions, decrementUserActions, @@ -100,6 +100,7 @@ router.get("/config", async (req, res) => { seedPeriodStartsAtUtc: ws.seedPeriodStartsAtUtc, actionsRemaining, teamActionsRemaining, + actionsResetIntervalHours: cfg.actionsResetIntervalHours ?? 12, resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} }, elementWorth: cfg.elementWorth ?? {}, militaryPower: cfg.militaryPower ?? {}, @@ -170,7 +171,7 @@ router.post("/cell/reveal", authMiddleware, async (req, res) => { const now = new Date(); const quotaRow = await getUserActionsRow(userId); 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 { const updated = await decrementUserActions(userId); if (!updated) { @@ -247,7 +248,7 @@ router.post("/cell/capture", authMiddleware, async (req, res) => { if (totalActions < cost) { 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 { if (teamRow.actions_remaining < cost) { return res.status(429).json({ error: "team_quota_exhausted", cost, teamActionsRemaining: teamRow.actions_remaining });