Private
Public Access
1
0

feat: Adding animations for map actions + getting rid of debugMode

This commit is contained in:
gauvainboiche
2026-04-01 19:14:58 +02:00
parent f161ccb0f0
commit ddd5f6ae72
10 changed files with 59 additions and 16 deletions

View File

@@ -85,7 +85,6 @@ Open `.env` and set every value — see [Environment variables](#environment-var
``` ```
- `POSTGRES_PASSWORD` - `POSTGRES_PASSWORD`
- `POSTGRES_USERS_PASSWORD` - `POSTGRES_USERS_PASSWORD`
- `ADMIN_PASSWORD`
### 3 — Review the game config ### 3 — Review the game config

View File

@@ -1,5 +1,5 @@
{ {
"dailyActionQuota": 100, "dailyActionQuota": 1000,
"teamActionQuota": 100, "teamActionQuota": 100,
"databaseWipeoutIntervalSeconds": 604800, "databaseWipeoutIntervalSeconds": 604800,
"configReloadIntervalSeconds": 30, "configReloadIntervalSeconds": 30,

View File

@@ -43,7 +43,6 @@ services:
DATABASE_URL: "postgres://${POSTGRES_USER:-game}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-star_wars_grid}" DATABASE_URL: "postgres://${POSTGRES_USER:-game}:${POSTGRES_PASSWORD}@db:5432/${POSTGRES_DB:-star_wars_grid}"
USERS_DATABASE_URL: "postgres://${POSTGRES_USERS_USER:-users}:${POSTGRES_USERS_PASSWORD}@users_db:5432/${POSTGRES_USERS_DB:-star_wars_users}" USERS_DATABASE_URL: "postgres://${POSTGRES_USERS_USER:-users}:${POSTGRES_USERS_PASSWORD}@users_db:5432/${POSTGRES_USERS_DB:-star_wars_users}"
JWT_SECRET: ${JWT_SECRET} JWT_SECRET: ${JWT_SECRET}
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
PORT: "${PORT:-8080}" PORT: "${PORT:-8080}"
CONFIG_FILE_PATH: /app/config/game.settings.json CONFIG_FILE_PATH: /app/config/game.settings.json
CORS_ORIGIN: ${CORS_ORIGIN:-*} CORS_ORIGIN: ${CORS_ORIGIN:-*}

View File

@@ -297,6 +297,7 @@
<!-- Mobile burger button --> <!-- Mobile burger button -->
<button type="button" id="burgerBtn" class="burgerBtn" aria-label="Open menu"></button> <button type="button" id="burgerBtn" class="burgerBtn" aria-label="Open menu"></button>
<canvas id="canvas" width="1000" height="1000"></canvas> <canvas id="canvas" width="1000" height="1000"></canvas>
<div id="mapAnim" class="mapAnim" aria-hidden="true"></div>
<div id="hint" class="hint">Cliquez sur une tuile. Les stats seront vides à moins de cliquer.</div> <div id="hint" class="hint">Cliquez sur une tuile. Les stats seront vides à moins de cliquer.</div>
<!-- Military attack confirmation modal --> <!-- Military attack confirmation modal -->

View File

@@ -23,7 +23,6 @@ const COLOR_OPPONENT_GREY = "rgba(95, 98, 110, 0.72)";
export const GAME_CONFIG = { export const GAME_CONFIG = {
dailyActionQuota: 100, dailyActionQuota: 100,
databaseWipeoutIntervalSeconds: 21600, databaseWipeoutIntervalSeconds: 21600,
debugModeForTeams: false,
configReloadIntervalSeconds: 30, configReloadIntervalSeconds: 30,
worldSeed: "", worldSeed: "",
seedPeriodEndsAtUtc: "", seedPeriodEndsAtUtc: "",
@@ -237,6 +236,7 @@ const teamQuotaEl = document.getElementById("teamActionsRemaining");
const captorInfoEl = document.getElementById("captorInfo"); const captorInfoEl = document.getElementById("captorInfo");
const playerListPopupEl = document.getElementById("playerListPopup"); const playerListPopupEl = document.getElementById("playerListPopup");
const playerListContentEl = document.getElementById("playerListContent"); const playerListContentEl = document.getElementById("playerListContent");
const mapAnimEl = document.getElementById("mapAnim");
// ── Cell helpers ────────────────────────────────────────────────────────────── // ── Cell helpers ──────────────────────────────────────────────────────────────
export function cellKey(x, y) { return `${x},${y}`; } export function cellKey(x, y) { return `${x},${y}`; }
@@ -260,7 +260,6 @@ 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;
GAME_CONFIG.databaseWipeoutIntervalSeconds = Number(data.databaseWipeoutIntervalSeconds) || 21600; GAME_CONFIG.databaseWipeoutIntervalSeconds = Number(data.databaseWipeoutIntervalSeconds) || 21600;
GAME_CONFIG.debugModeForTeams = Boolean(data.debugModeForTeams);
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 ?? "");
GAME_CONFIG.seedPeriodEndsAtUtc = String(data.seedPeriodEndsAtUtc ?? ""); GAME_CONFIG.seedPeriodEndsAtUtc = String(data.seedPeriodEndsAtUtc ?? "");
@@ -837,6 +836,29 @@ export function draw() {
ctx.restore(); ctx.restore();
} }
// ── Map action animation ─────────────────────────────────────────────────────
const mapAnimTextByType = {
"reveal-empty": "🔍",
"reveal-planet": "👁️",
"capture": "⚔️",
};
/**
* Triggers a spinning emoji animation at the centre of the map zone.
* @param {"reveal-empty"|"reveal-planet"|"capture"} type
*/
function triggerMapAnimation(type) {
if (!mapAnimEl) return;
mapAnimEl.textContent = mapAnimTextByType[type] || "🔍";
mapAnimEl.classList.remove("mapAnim--active");
void mapAnimEl.offsetWidth; // force reflow to restart the animation
mapAnimEl.classList.add("mapAnim--active");
mapAnimEl.addEventListener("animationend", () => {
mapAnimEl.classList.remove("mapAnim--active");
}, { once: true });
}
// ── Cursor ──────────────────────────────────────────────────────────────────── // ── Cursor ────────────────────────────────────────────────────────────────────
function pickCell(ev) { function pickCell(ev) {
@@ -1040,6 +1062,7 @@ async function onCanvasClick(ev) {
} }
if (!res.ok) throw new Error("reveal"); if (!res.ok) throw new Error("reveal");
applyRevealPayload(await res.json()); applyRevealPayload(await res.json());
triggerMapAnimation(cells.get(key)?.hasPlanet ? "reveal-planet" : "reveal-empty");
startCooldown(); startCooldown();
updateEconomyDisplay(); updateEconomyDisplay();
draw(); draw();
@@ -1101,6 +1124,7 @@ async function onCanvasClick(ev) {
teamActionsRemaining = data.teamActionsRemaining; teamActionsRemaining = data.teamActionsRemaining;
updateTeamQuotaDisplay(); updateTeamQuotaDisplay();
} }
triggerMapAnimation("capture");
hint.textContent = `🏴 Planète (${cell.x},${cell.y}) capturée !`; hint.textContent = `🏴 Planète (${cell.x},${cell.y}) capturée !`;
showLocalSelection(cell.x, cell.y); showLocalSelection(cell.x, cell.y);
updateEconomyDisplay(); updateEconomyDisplay();

View File

@@ -1295,4 +1295,31 @@ canvas {
.playerListEmpty { .playerListEmpty {
color: rgba(233, 238, 246, 0.4); color: rgba(233, 238, 246, 0.4);
font-style: italic; font-style: italic;
}
/* ── Map action animation ─────────────────────────────────────────────────── */
@keyframes mapAnimPop {
0% { transform: translate(-50%, -50%) scale(0.1); opacity: 0; }
20% { transform: translate(-50%, -50%) scale(1.1); opacity: 1; }
80% { transform: translate(-50%, -50%) scale(1); opacity: 1; }
100% { transform: translate(-50%, -50%) scale(1.2); opacity: 0; }
}
.mapAnim {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
font-size: 88px;
line-height: 1;
pointer-events: none;
z-index: 20;
display: none;
user-select: none;
}
.mapAnim--active {
display: block;
animation: mapAnimPop 0.5s ease-out forwards;
} }

View File

@@ -7,12 +7,11 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
const CONFIG_FILE_PATH = 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, debugModeForTeams: boolean, configReloadIntervalSeconds: number, resourceWorth: object, militaryPower: object }} */ /** @type {{ dailyActionQuota: number, teamActionQuota: number, databaseWipeoutIntervalSeconds: number, configReloadIntervalSeconds: number, resourceWorth: object, militaryPower: object }} */
let cached = { let cached = {
dailyActionQuota: 100, dailyActionQuota: 100,
teamActionQuota: 100, teamActionQuota: 100,
databaseWipeoutIntervalSeconds: 21600, databaseWipeoutIntervalSeconds: 21600,
debugModeForTeams: true,
configReloadIntervalSeconds: 30, configReloadIntervalSeconds: 30,
elementWorth: {}, elementWorth: {},
resourceWorth: { common: {}, rare: {} }, resourceWorth: { common: {}, rare: {} },
@@ -48,8 +47,6 @@ export function loadConfigFile() {
if (typeof j.databaseWipeoutIntervalSeconds === "number" && j.databaseWipeoutIntervalSeconds >= 60) { if (typeof j.databaseWipeoutIntervalSeconds === "number" && j.databaseWipeoutIntervalSeconds >= 60) {
cached.databaseWipeoutIntervalSeconds = j.databaseWipeoutIntervalSeconds; cached.databaseWipeoutIntervalSeconds = j.databaseWipeoutIntervalSeconds;
} }
const dbg = parseBool(j.debugModeForTeams);
if (dbg !== null) cached.debugModeForTeams = dbg;
if (typeof j.configReloadIntervalSeconds === "number" && j.configReloadIntervalSeconds >= 5) { if (typeof j.configReloadIntervalSeconds === "number" && j.configReloadIntervalSeconds >= 5) {
cached.configReloadIntervalSeconds = j.configReloadIntervalSeconds; cached.configReloadIntervalSeconds = j.configReloadIntervalSeconds;
} }

View File

@@ -9,7 +9,6 @@ export async function initUsersSchema() {
username TEXT NOT NULL UNIQUE, username TEXT NOT NULL UNIQUE,
password_hash TEXT NOT NULL, password_hash TEXT NOT NULL,
team TEXT NOT NULL CHECK (team IN ('blue', 'red')), team TEXT NOT NULL CHECK (team IN ('blue', 'red')),
role TEXT NOT NULL DEFAULT 'user' CHECK (role IN ('user', 'admin')),
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW() created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
); );
`); `);
@@ -44,7 +43,7 @@ export async function createUser(username, passwordHash, team) {
const { rows } = await usersPool.query( const { rows } = await usersPool.query(
`INSERT INTO users (username, password_hash, team) `INSERT INTO users (username, password_hash, team)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
RETURNING id, username, team, role`, RETURNING id, username, team`,
[username, passwordHash, team] [username, passwordHash, team]
); );
return rows[0]; return rows[0];
@@ -52,7 +51,7 @@ export async function createUser(username, passwordHash, team) {
export async function getUserByUsername(username) { export async function getUserByUsername(username) {
const { rows } = await usersPool.query( const { rows } = await usersPool.query(
`SELECT id, username, team, role, password_hash FROM users WHERE username = $1`, `SELECT id, username, team, password_hash FROM users WHERE username = $1`,
[username] [username]
); );
return rows[0] ?? null; return rows[0] ?? null;
@@ -60,7 +59,7 @@ export async function getUserByUsername(username) {
export async function getUserById(id) { export async function getUserById(id) {
const { rows } = await usersPool.query( const { rows } = await usersPool.query(
`SELECT id, username, team, role FROM users WHERE id = $1`, `SELECT id, username, team FROM users WHERE id = $1`,
[id] [id]
); );
return rows[0] ?? null; return rows[0] ?? null;

View File

@@ -15,7 +15,6 @@ function makeConfigSignature(cfg) {
dailyActionQuota: cfg.dailyActionQuota, dailyActionQuota: cfg.dailyActionQuota,
teamActionQuota: cfg.teamActionQuota, teamActionQuota: cfg.teamActionQuota,
databaseWipeoutIntervalSeconds: cfg.databaseWipeoutIntervalSeconds, databaseWipeoutIntervalSeconds: cfg.databaseWipeoutIntervalSeconds,
debugModeForTeams: cfg.debugModeForTeams,
configReloadIntervalSeconds: cfg.configReloadIntervalSeconds, configReloadIntervalSeconds: cfg.configReloadIntervalSeconds,
elementWorth: cfg.elementWorth ?? {}, elementWorth: cfg.elementWorth ?? {},
resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} }, resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} },
@@ -43,7 +42,6 @@ function scheduleConfigPoll() {
dailyActionQuota: cfg.dailyActionQuota, dailyActionQuota: cfg.dailyActionQuota,
teamActionQuota: cfg.teamActionQuota, teamActionQuota: cfg.teamActionQuota,
databaseWipeoutIntervalSeconds: cfg.databaseWipeoutIntervalSeconds, databaseWipeoutIntervalSeconds: cfg.databaseWipeoutIntervalSeconds,
debugModeForTeams: cfg.debugModeForTeams,
configReloadIntervalSeconds: cfg.configReloadIntervalSeconds, configReloadIntervalSeconds: cfg.configReloadIntervalSeconds,
elementWorth: cfg.elementWorth ?? {}, elementWorth: cfg.elementWorth ?? {},
resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} }, resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} },

View File

@@ -94,7 +94,6 @@ router.get("/config", async (req, res) => {
res.json({ res.json({
dailyActionQuota: cfg.dailyActionQuota, dailyActionQuota: cfg.dailyActionQuota,
databaseWipeoutIntervalSeconds: rot, databaseWipeoutIntervalSeconds: rot,
debugModeForTeams: cfg.debugModeForTeams,
configReloadIntervalSeconds: cfg.configReloadIntervalSeconds, configReloadIntervalSeconds: cfg.configReloadIntervalSeconds,
worldSeed, worldSeed,
seedPeriodEndsAtUtc: ws.seedPeriodEndsAtUtc, seedPeriodEndsAtUtc: ws.seedPeriodEndsAtUtc,