358 lines
13 KiB
JavaScript
358 lines
13 KiB
JavaScript
import { pool } from "./pools.js";
|
|
import { loadConfigFile, getConfig } from "../configLoader.js";
|
|
import { computeWorldSeedState } from "../worldSeed.js";
|
|
|
|
let lastSeedSlot = null;
|
|
|
|
// ── Schema ────────────────────────────────────────────────────────────────────
|
|
|
|
export async function initGameSchema() {
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS grid_cells (
|
|
id SERIAL PRIMARY KEY,
|
|
world_seed TEXT NOT NULL,
|
|
x SMALLINT NOT NULL CHECK (x >= 0 AND x < 100),
|
|
y SMALLINT NOT NULL CHECK (y >= 0 AND y < 100),
|
|
exploitable BOOLEAN NOT NULL,
|
|
has_planet BOOLEAN NOT NULL,
|
|
planet_json JSONB,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
UNIQUE (world_seed, x, y)
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_grid_cells_seed ON grid_cells (world_seed);
|
|
`);
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS team_cooldowns (
|
|
world_seed TEXT NOT NULL,
|
|
team TEXT NOT NULL CHECK (team IN ('blue', 'red')),
|
|
last_reveal TIMESTAMPTZ,
|
|
PRIMARY KEY (world_seed, team)
|
|
);
|
|
`);
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS user_cooldowns (
|
|
world_seed TEXT NOT NULL,
|
|
user_id INTEGER NOT NULL,
|
|
team TEXT NOT NULL CHECK (team IN ('blue', 'red')),
|
|
last_reveal TIMESTAMPTZ,
|
|
PRIMARY KEY (world_seed, user_id)
|
|
);
|
|
`);
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS team_econ_scores (
|
|
world_seed TEXT NOT NULL,
|
|
team TEXT NOT NULL CHECK (team IN ('blue', 'red')),
|
|
score DOUBLE PRECISION NOT NULL DEFAULT 0,
|
|
PRIMARY KEY (world_seed, team)
|
|
);
|
|
`);
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS team_element_bonus (
|
|
world_seed TEXT NOT NULL,
|
|
team TEXT NOT NULL CHECK (team IN ('blue', 'red')),
|
|
bonus DOUBLE PRECISION NOT NULL DEFAULT 0,
|
|
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,
|
|
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
|
);
|
|
INSERT INTO db_metadata (created_at)
|
|
SELECT NOW() WHERE NOT EXISTS (SELECT 1 FROM db_metadata);
|
|
`);
|
|
await pool.query(`
|
|
CREATE TABLE IF NOT EXISTS victory_points (
|
|
id SERIAL PRIMARY KEY,
|
|
awarded_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
|
world_seed TEXT NOT NULL,
|
|
team TEXT NOT NULL CHECK (team IN ('blue', 'red'))
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_vp_team ON victory_points (team);
|
|
`);
|
|
await pool.query(`
|
|
ALTER TABLE grid_cells ADD COLUMN IF NOT EXISTS discovered_by TEXT;
|
|
UPDATE grid_cells SET discovered_by = 'blue' WHERE discovered_by IS NULL;
|
|
ALTER TABLE grid_cells ALTER COLUMN discovered_by SET DEFAULT 'blue';
|
|
DO $$
|
|
BEGIN
|
|
IF NOT EXISTS (
|
|
SELECT 1 FROM pg_constraint WHERE conname = 'grid_cells_discovered_by_check'
|
|
) THEN
|
|
ALTER TABLE grid_cells ADD CONSTRAINT grid_cells_discovered_by_check
|
|
CHECK (discovered_by IN ('blue', 'red'));
|
|
END IF;
|
|
END $$;
|
|
ALTER TABLE grid_cells ALTER COLUMN discovered_by SET NOT NULL;
|
|
`);
|
|
}
|
|
|
|
// ── World-seed epoch ──────────────────────────────────────────────────────────
|
|
|
|
export async function ensureSeedEpoch() {
|
|
loadConfigFile();
|
|
const rot = getConfig().databaseWipeoutIntervalSeconds;
|
|
const { seedSlot, worldSeed } = computeWorldSeedState(rot);
|
|
if (lastSeedSlot === null) {
|
|
lastSeedSlot = seedSlot;
|
|
return worldSeed;
|
|
}
|
|
if (seedSlot !== lastSeedSlot) {
|
|
// Award a victory point to the team with the highest econ score before wiping
|
|
try {
|
|
const expiredSeed = `swg-${lastSeedSlot}`;
|
|
const econRows = await pool.query(
|
|
`SELECT team, score FROM team_econ_scores WHERE world_seed = $1`,
|
|
[expiredSeed]
|
|
);
|
|
const scores = { blue: 0, red: 0 };
|
|
for (const row of econRows.rows) scores[row.team] = Number(row.score);
|
|
if (scores.blue > 0 || scores.red > 0) {
|
|
const winner = scores.blue >= scores.red ? "blue" : "red";
|
|
await pool.query(
|
|
`INSERT INTO victory_points (world_seed, team) VALUES ($1, $2)`,
|
|
[expiredSeed, winner]
|
|
);
|
|
console.log(`[world] VP awarded to ${winner} for seed ${expiredSeed} (blue=${scores.blue.toFixed(3)}, red=${scores.red.toFixed(3)})`);
|
|
}
|
|
} catch (e) {
|
|
console.error("[world] VP award error:", e);
|
|
}
|
|
await pool.query("TRUNCATE grid_cells RESTART IDENTITY");
|
|
await pool.query("DELETE FROM team_cooldowns WHERE world_seed != $1", [worldSeed]);
|
|
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;
|
|
}
|
|
return worldSeed;
|
|
}
|
|
|
|
// ── Grid cells ────────────────────────────────────────────────────────────────
|
|
|
|
export async function getGridCells(worldSeed) {
|
|
const { rows } = await pool.query(
|
|
`SELECT x, y, exploitable, has_planet, planet_json, discovered_by
|
|
FROM grid_cells WHERE world_seed = $1`,
|
|
[worldSeed]
|
|
);
|
|
return rows;
|
|
}
|
|
|
|
export async function insertCell(seed, x, y, exploitable, hasPlanet, planetJson, team) {
|
|
const { rows } = await pool.query(
|
|
`INSERT INTO grid_cells (world_seed, x, y, exploitable, has_planet, planet_json, discovered_by)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
ON CONFLICT (world_seed, x, y) DO NOTHING
|
|
RETURNING x, y, exploitable, has_planet, planet_json, discovered_by`,
|
|
[seed, x, y, exploitable, hasPlanet, planetJson, team]
|
|
);
|
|
return rows[0] ?? null;
|
|
}
|
|
|
|
export async function getExistingCell(seed, x, y) {
|
|
const { rows } = await pool.query(
|
|
`SELECT x, y, exploitable, has_planet, planet_json, discovered_by
|
|
FROM grid_cells WHERE world_seed = $1 AND x = $2 AND y = $3`,
|
|
[seed, x, y]
|
|
);
|
|
return rows[0] ?? null;
|
|
}
|
|
|
|
// ── Team cooldowns ────────────────────────────────────────────────────────────
|
|
|
|
export async function getTeamCooldown(worldSeed, team) {
|
|
const { rows } = await pool.query(
|
|
`SELECT last_reveal FROM team_cooldowns WHERE world_seed = $1 AND team = $2`,
|
|
[worldSeed, team]
|
|
);
|
|
return rows[0] ?? null;
|
|
}
|
|
|
|
export async function upsertTeamCooldown(worldSeed, team) {
|
|
await pool.query(
|
|
`INSERT INTO team_cooldowns (world_seed, team, last_reveal)
|
|
VALUES ($1, $2, NOW())
|
|
ON CONFLICT (world_seed, team) DO UPDATE SET last_reveal = NOW()`,
|
|
[worldSeed, team]
|
|
);
|
|
}
|
|
|
|
// ── User cooldowns (per-user, replaces team-wide cooldown for reveal) ─────────
|
|
|
|
export async function getUserCooldown(worldSeed, userId) {
|
|
const { rows } = await pool.query(
|
|
`SELECT last_reveal FROM user_cooldowns WHERE world_seed = $1 AND user_id = $2`,
|
|
[worldSeed, userId]
|
|
);
|
|
return rows[0] ?? null;
|
|
}
|
|
|
|
export async function upsertUserCooldown(worldSeed, userId, team) {
|
|
await pool.query(
|
|
`INSERT INTO user_cooldowns (world_seed, user_id, team, last_reveal)
|
|
VALUES ($1, $2, $3, NOW())
|
|
ON CONFLICT (world_seed, user_id) DO UPDATE SET last_reveal = NOW()`,
|
|
[worldSeed, userId, team]
|
|
);
|
|
}
|
|
|
|
// ── Economic scores ───────────────────────────────────────────────────────────
|
|
|
|
export async function getEconScores(worldSeed) {
|
|
const { rows } = await pool.query(
|
|
`SELECT team, score FROM team_econ_scores WHERE world_seed = $1`,
|
|
[worldSeed]
|
|
);
|
|
const result = { blue: 0, red: 0 };
|
|
for (const row of rows) result[row.team] = Number(row.score);
|
|
return result;
|
|
}
|
|
|
|
export async function addEconScore(worldSeed, team, delta) {
|
|
if (delta <= 0) return;
|
|
await pool.query(
|
|
`INSERT INTO team_econ_scores (world_seed, team, score)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (world_seed, team) DO UPDATE
|
|
SET score = team_econ_scores.score + EXCLUDED.score`,
|
|
[worldSeed, team, delta]
|
|
);
|
|
}
|
|
|
|
export async function getElementBonus(worldSeed) {
|
|
const { rows } = await pool.query(
|
|
`SELECT team, bonus FROM team_element_bonus WHERE world_seed = $1`,
|
|
[worldSeed]
|
|
);
|
|
const result = { blue: 0, red: 0 };
|
|
for (const row of rows) result[row.team] = Number(row.bonus);
|
|
return result;
|
|
}
|
|
|
|
export async function setElementBonus(worldSeed, team, bonus) {
|
|
await pool.query(
|
|
`INSERT INTO team_element_bonus (world_seed, team, bonus)
|
|
VALUES ($1, $2, $3)
|
|
ON CONFLICT (world_seed, team) DO UPDATE
|
|
SET bonus = EXCLUDED.bonus`,
|
|
[worldSeed, team, bonus]
|
|
);
|
|
}
|
|
|
|
// ── DB metadata ───────────────────────────────────────────────────────────────
|
|
|
|
export async function getDbCreatedAt() {
|
|
const { rows } = await pool.query(
|
|
`SELECT created_at FROM db_metadata ORDER BY id ASC LIMIT 1`
|
|
);
|
|
return rows[0]?.created_at ?? null;
|
|
}
|
|
|
|
// ── Victory points ────────────────────────────────────────────────────────────
|
|
|
|
export async function getVictoryPoints() {
|
|
const { rows } = await pool.query(
|
|
`SELECT team, COUNT(*) AS cnt FROM victory_points GROUP BY team`
|
|
);
|
|
const result = { blue: 0, red: 0 };
|
|
for (const row of rows) result[row.team] = Number(row.cnt);
|
|
return result;
|
|
}
|
|
|
|
// ── Scores ────────────────────────────────────────────────────────────────────
|
|
|
|
export async function getScores(worldSeed) {
|
|
const { rows } = await pool.query(
|
|
`SELECT discovered_by, COUNT(*) AS cnt
|
|
FROM grid_cells WHERE world_seed = $1
|
|
GROUP BY discovered_by`,
|
|
[worldSeed]
|
|
);
|
|
return rows;
|
|
}
|
|
|
|
// ── Active player counts (players who have played in the current epoch) ───────
|
|
|
|
export async function getActivePlayerCounts(worldSeed) {
|
|
const { rows } = await pool.query(
|
|
`SELECT team, COUNT(DISTINCT user_id)::int AS count
|
|
FROM user_cooldowns WHERE world_seed = $1
|
|
GROUP BY team`,
|
|
[worldSeed]
|
|
);
|
|
const result = { blue: 0, red: 0 };
|
|
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]
|
|
);
|
|
}
|