refacto: Changing actions reset windows + display
This commit is contained in:
@@ -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
@@ -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
@@ -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
@@ -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();
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 ───────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
@@ -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 ?? {},
|
||||||
|
|||||||
@@ -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 });
|
||||||
|
|||||||
Reference in New Issue
Block a user