feat: Adding animations for map actions + getting rid of debugMode
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"dailyActionQuota": 100,
|
"dailyActionQuota": 1000,
|
||||||
"teamActionQuota": 100,
|
"teamActionQuota": 100,
|
||||||
"databaseWipeoutIntervalSeconds": 604800,
|
"databaseWipeoutIntervalSeconds": 604800,
|
||||||
"configReloadIntervalSeconds": 30,
|
"configReloadIntervalSeconds": 30,
|
||||||
|
|||||||
@@ -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:-*}
|
||||||
|
|||||||
@@ -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 -->
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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: {} },
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
Reference in New Issue
Block a user