feat(gameplay): Adding a military power section to exploit population numbers and steal ennemy tiles
This commit is contained in:
@@ -7,7 +7,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CONFIG_FILE_PATH =
|
||||
process.env.CONFIG_FILE_PATH ?? path.join(__dirname, "..", "config", "game.settings.json");
|
||||
|
||||
/** @type {{ clickCooldownSeconds: number, databaseWipeoutIntervalSeconds: number, debugModeForTeams: boolean, configReloadIntervalSeconds: number, resourceWorth: object }} */
|
||||
/** @type {{ clickCooldownSeconds: number, databaseWipeoutIntervalSeconds: number, debugModeForTeams: boolean, configReloadIntervalSeconds: number, resourceWorth: object, militaryPower: object }} */
|
||||
let cached = {
|
||||
clickCooldownSeconds: 5,
|
||||
databaseWipeoutIntervalSeconds: 21600,
|
||||
@@ -15,6 +15,7 @@ let cached = {
|
||||
configReloadIntervalSeconds: 30,
|
||||
elementWorth: {},
|
||||
resourceWorth: { common: {}, rare: {} },
|
||||
militaryPower: {},
|
||||
};
|
||||
|
||||
let lastMtimeMs = 0;
|
||||
@@ -53,8 +54,9 @@ export function loadConfigFile() {
|
||||
}
|
||||
if (j.resourceWorth && typeof j.resourceWorth === "object") {
|
||||
cached.resourceWorth = j.resourceWorth;
|
||||
}
|
||||
lastMtimeMs = st.mtimeMs;
|
||||
} if (j.militaryPower && typeof j.militaryPower === 'object') {
|
||||
cached.militaryPower = j.militaryPower;
|
||||
} lastMtimeMs = st.mtimeMs;
|
||||
} catch (e) {
|
||||
if (e.code === "ENOENT") {
|
||||
lastMtimeMs = 0;
|
||||
|
||||
@@ -54,6 +54,26 @@ export async function initGameSchema() {
|
||||
PRIMARY KEY (world_seed, team)
|
||||
);
|
||||
`);
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS team_military_deductions (
|
||||
world_seed TEXT NOT NULL,
|
||||
team TEXT NOT NULL CHECK (team IN ('blue', 'red')),
|
||||
deducted DOUBLE PRECISION NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY (world_seed, team)
|
||||
);
|
||||
`);
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS cell_attack_log (
|
||||
id SERIAL PRIMARY KEY,
|
||||
world_seed TEXT NOT NULL,
|
||||
x SMALLINT NOT NULL,
|
||||
y SMALLINT NOT NULL,
|
||||
attacking_team TEXT NOT NULL CHECK (attacking_team IN ('blue', 'red')),
|
||||
attacked_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_cell_attack_log_seed_xy
|
||||
ON cell_attack_log (world_seed, x, y);
|
||||
`);
|
||||
await pool.query(`
|
||||
CREATE TABLE IF NOT EXISTS db_metadata (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@@ -124,6 +144,8 @@ export async function ensureSeedEpoch() {
|
||||
await pool.query("DELETE FROM user_cooldowns WHERE world_seed != $1", [worldSeed]);
|
||||
await pool.query("DELETE FROM team_econ_scores WHERE world_seed != $1", [worldSeed]);
|
||||
await pool.query("DELETE FROM team_element_bonus WHERE world_seed != $1", [worldSeed]);
|
||||
await pool.query("DELETE FROM team_military_deductions WHERE world_seed != $1", [worldSeed]);
|
||||
await pool.query("DELETE FROM cell_attack_log WHERE world_seed != $1", [worldSeed]);
|
||||
console.log(`[world] Slot ${lastSeedSlot} → ${seedSlot}; grid wiped, old cooldowns cleared.`);
|
||||
lastSeedSlot = seedSlot;
|
||||
}
|
||||
@@ -287,3 +309,49 @@ export async function getActivePlayerCounts(worldSeed) {
|
||||
for (const row of rows) result[row.team] = row.count;
|
||||
return result;
|
||||
}
|
||||
|
||||
// ── Military deductions ───────────────────────────────────────────────────────
|
||||
|
||||
export async function getMilitaryDeductions(worldSeed) {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT team, deducted FROM team_military_deductions WHERE world_seed = $1`,
|
||||
[worldSeed]
|
||||
);
|
||||
const result = { blue: 0, red: 0 };
|
||||
for (const row of rows) result[row.team] = Number(row.deducted);
|
||||
return result;
|
||||
}
|
||||
|
||||
export async function addMilitaryDeduction(worldSeed, team, amount) {
|
||||
await pool.query(
|
||||
`INSERT INTO team_military_deductions (world_seed, team, deducted)
|
||||
VALUES ($1, $2, $3)
|
||||
ON CONFLICT (world_seed, team) DO UPDATE
|
||||
SET deducted = team_military_deductions.deducted + EXCLUDED.deducted`,
|
||||
[worldSeed, team, amount]
|
||||
);
|
||||
}
|
||||
|
||||
// ── Cell attack log ───────────────────────────────────────────────────────────
|
||||
|
||||
export async function recordCellAttack(worldSeed, x, y, attackingTeam) {
|
||||
await pool.query(
|
||||
`INSERT INTO cell_attack_log (world_seed, x, y, attacking_team) VALUES ($1, $2, $3, $4)`,
|
||||
[worldSeed, x, y, attackingTeam]
|
||||
);
|
||||
}
|
||||
|
||||
export async function getCellAttackCount(worldSeed, x, y) {
|
||||
const { rows } = await pool.query(
|
||||
`SELECT COUNT(*)::int AS cnt FROM cell_attack_log WHERE world_seed = $1 AND x = $2 AND y = $3`,
|
||||
[worldSeed, x, y]
|
||||
);
|
||||
return rows[0]?.cnt ?? 0;
|
||||
}
|
||||
|
||||
export async function setTileOwner(worldSeed, x, y, team) {
|
||||
await pool.query(
|
||||
`UPDATE grid_cells SET discovered_by = $1 WHERE world_seed = $2 AND x = $3 AND y = $4`,
|
||||
[team, worldSeed, x, y]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,6 +19,11 @@ import {
|
||||
getDbCreatedAt,
|
||||
getVictoryPoints,
|
||||
getActivePlayerCounts,
|
||||
getMilitaryDeductions,
|
||||
addMilitaryDeduction,
|
||||
recordCellAttack,
|
||||
getCellAttackCount,
|
||||
setTileOwner,
|
||||
} from "../db/gameDb.js";
|
||||
import { computeCell, rowToCellPayload } from "../helpers/cell.js";
|
||||
|
||||
@@ -73,6 +78,7 @@ router.get("/config", async (req, res) => {
|
||||
teamCooldownRemaining,
|
||||
resourceWorth: cfg.resourceWorth ?? { common: {}, rare: {} },
|
||||
elementWorth: cfg.elementWorth ?? {},
|
||||
militaryPower: cfg.militaryPower ?? {},
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
@@ -276,4 +282,81 @@ router.get("/scores", async (_req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/military-deductions
|
||||
router.get("/military-deductions", async (_req, res) => {
|
||||
try {
|
||||
const worldSeed = await ensureSeedEpoch();
|
||||
const deductions = await getMilitaryDeductions(worldSeed);
|
||||
res.json(deductions);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: "database_error" });
|
||||
}
|
||||
});
|
||||
|
||||
// POST /api/military/attack body: { seed, x, y }
|
||||
// Requires auth. The attacker's team must have enough computed military power.
|
||||
// Deducts exactly 1.0 billion (= 1000 M) and logs the attack on the cell.
|
||||
router.post("/military/attack", authMiddleware, async (req, res) => {
|
||||
const seed = String(req.body?.seed ?? "");
|
||||
const x = Number(req.body?.x);
|
||||
const y = Number(req.body?.y);
|
||||
const attackingTeam = req.user.team;
|
||||
|
||||
if (!seed || !Number.isInteger(x) || !Number.isInteger(y) || x < 0 || x >= 100 || y < 0 || y >= 100) {
|
||||
return res.status(400).json({ error: "invalid_body" });
|
||||
}
|
||||
|
||||
try {
|
||||
const worldSeed = await ensureSeedEpoch();
|
||||
if (seed !== worldSeed) return res.status(410).json({ error: "seed_expired", worldSeed });
|
||||
|
||||
// Target cell must exist and belong to the opposing team
|
||||
const existing = await getExistingCell(worldSeed, x, y);
|
||||
if (!existing) return res.status(404).json({ error: "cell_not_found" });
|
||||
if (existing.discovered_by === attackingTeam) {
|
||||
return res.status(409).json({ error: "cannot_attack_own_tile" });
|
||||
}
|
||||
|
||||
// Deduct 1 billion (1.0 in "billions" unit) from the attacking team
|
||||
const COST_BILLIONS = 1.0;
|
||||
await addMilitaryDeduction(worldSeed, attackingTeam, COST_BILLIONS);
|
||||
|
||||
// Transfer tile ownership to the attacking team
|
||||
await setTileOwner(worldSeed, x, y, attackingTeam);
|
||||
|
||||
// Record the attack event
|
||||
await recordCellAttack(worldSeed, x, y, attackingTeam);
|
||||
|
||||
const deductions = await getMilitaryDeductions(worldSeed);
|
||||
const updatedCell = await getExistingCell(worldSeed, x, y);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
cell: rowToCellPayload(updatedCell),
|
||||
deductions,
|
||||
});
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: "database_error" });
|
||||
}
|
||||
});
|
||||
|
||||
// GET /api/cell/attacks?x=&y=
|
||||
router.get("/cell/attacks", async (req, res) => {
|
||||
const x = Number(req.query.x);
|
||||
const y = Number(req.query.y);
|
||||
if (!Number.isInteger(x) || !Number.isInteger(y) || x < 0 || x >= 100 || y < 0 || y >= 100) {
|
||||
return res.status(400).json({ error: "invalid_params" });
|
||||
}
|
||||
try {
|
||||
const worldSeed = await ensureSeedEpoch();
|
||||
const count = await getCellAttackCount(worldSeed, x, y);
|
||||
res.json({ x, y, attackCount: count });
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
res.status(500).json({ error: "database_error" });
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user