Private
Public Access
1
0

refacto: Changing gameplay so teams don't know what the other team got + both teams can now chart the same cell without knowing + planet capture gameplay implemented

This commit is contained in:
gauvainboiche
2026-04-01 17:17:52 +02:00
parent 5aa347eb13
commit 99d34c58c6
12 changed files with 432 additions and 186 deletions

View File

@@ -27,6 +27,10 @@ import {
getTeamActionsRow,
resetTeamActions,
decrementTeamActions,
decrementTeamActionsBy,
checkTeamVisibility,
insertTeamVisibility,
getTeamVisibleCells,
} from "../db/gameDb.js";
import {
nextNoonUtc,
@@ -78,7 +82,7 @@ router.get("/config", async (req, res) => {
const milDeductions = await getMilitaryDeductions(worldSeed);
const milNet = milPower - (milDeductions[team] ?? 0);
const milBonus = Math.floor(Math.max(0, milNet) / 10000);
teamActionsRemaining = 100 + milBonus;
teamActionsRemaining = cfg.teamActionQuota + milBonus;
} else {
teamActionsRemaining = teamRow.actions_remaining;
}
@@ -104,7 +108,7 @@ router.get("/config", async (req, res) => {
}
});
// GET /api/grid/:seed
// GET /api/grid/:seed (auth-aware: returns only team-visible cells)
router.get("/grid/:seed", async (req, res) => {
const seed = decodeURIComponent(req.params.seed || "");
try {
@@ -118,7 +122,14 @@ router.get("/grid/:seed", async (req, res) => {
).seedPeriodEndsAtUtc,
});
}
const rows = await getGridCells(seed);
let rows = [];
const authHeader = req.headers["authorization"];
if (authHeader && authHeader.startsWith("Bearer ")) {
try {
const payload = jwt.verify(authHeader.slice(7), JWT_SECRET);
rows = await getTeamVisibleCells(worldSeed, payload.team);
} catch { /* invalid token — return empty grid */ }
}
res.json({ seed, cells: rows });
} catch (e) {
console.error(e);
@@ -126,10 +137,10 @@ router.get("/grid/:seed", async (req, res) => {
}
});
// POST /api/cell/reveal
// POST /api/cell/reveal — reveals a cell for THIS team only (private visibility)
router.post("/cell/reveal", authMiddleware, async (req, res) => {
const seed = String(req.body?.seed ?? "");
const team = req.user.team; // taken from verified JWT, not request body
const team = req.user.team;
const userId = req.user.userId;
const x = Number(req.body?.x);
const y = Number(req.body?.y);
@@ -144,50 +155,42 @@ router.post("/cell/reveal", authMiddleware, async (req, res) => {
return res.status(410).json({ error: "seed_expired", worldSeed });
}
const cfg = getConfig();
if (cfg.dailyActionQuota > 0) {
const bonus = await getElementBonus(worldSeed);
const teamBonus = bonus[team] ?? 0;
const effectiveQuota = Math.floor(cfg.dailyActionQuota * (1 + teamBonus / 100));
const now = new Date();
const quotaRow = await getUserActionsRow(userId);
// Check if already revealed by this team (free re-view, no action cost)
const alreadyVisible = await checkTeamVisibility(worldSeed, team, x, y);
if (!quotaRow || new Date(quotaRow.quota_reset_at) <= now) {
// First action of the day (or expired): reset to effectiveQuota - 1 (consuming 1 now)
await resetUserActions(userId, effectiveQuota - 1, nextNoonUtc().toISOString());
} else {
// Quota exists and still valid — try to consume
const updated = await decrementUserActions(userId);
if (!updated) {
return res.status(429).json({
error: "quota_exhausted",
actionsRemaining: 0,
});
if (!alreadyVisible) {
// First reveal: deduct one user action
const cfg = getConfig();
if (cfg.dailyActionQuota > 0) {
const bonus = await getElementBonus(worldSeed);
const teamBonus = bonus[team] ?? 0;
const effectiveQuota = Math.floor(cfg.dailyActionQuota * (1 + teamBonus / 100));
const now = new Date();
const quotaRow = await getUserActionsRow(userId);
if (!quotaRow || new Date(quotaRow.quota_reset_at) <= now) {
await resetUserActions(userId, effectiveQuota - 1, nextNoonUtc().toISOString());
} else {
const updated = await decrementUserActions(userId);
if (!updated) {
return res.status(429).json({ error: "quota_exhausted", actionsRemaining: 0 });
}
}
}
// Mark as visible for this team
await insertTeamVisibility(worldSeed, team, x, y);
}
// Ensure cell data is in grid_cells (discovered_by stays NULL = neutral)
const cell = computeCell(seed, x, y);
const planetJson = cell.planet ? JSON.stringify(cell.planet) : null;
const inserted = await insertCell(seed, x, y, cell.exploitable, cell.hasPlanet, planetJson, team);
await insertCell(seed, x, y, cell.exploitable, cell.hasPlanet, planetJson);
// Track activity for active-player count (kept for stat purposes)
// Track activity for active-player count
await upsertUserCooldown(worldSeed, userId, team);
if (inserted) {
return res.json(rowToCellPayload(inserted));
}
const existing = await getExistingCell(seed, x, y);
if (!existing) return res.status(500).json({ error: "insert_race" });
if (existing.discovered_by !== team) {
return res.status(409).json({
error: "taken_by_other_team",
discoveredBy: existing.discovered_by,
cell: rowToCellPayload(existing),
});
}
return res.json(rowToCellPayload(existing));
} catch (e) {
console.error(e);
@@ -195,6 +198,76 @@ router.post("/cell/reveal", authMiddleware, async (req, res) => {
}
});
// POST /api/cell/capture — spend team actions to capture a planet
router.post("/cell/capture", authMiddleware, async (req, res) => {
const seed = String(req.body?.seed ?? "");
const team = req.user.team;
const x = Number(req.body?.x);
const y = Number(req.body?.y);
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 });
const existing = await getExistingCell(worldSeed, x, y);
if (!existing) return res.status(404).json({ error: "cell_not_found" });
if (!existing.has_planet) return res.status(409).json({ error: "no_planet" });
if (existing.discovered_by === team) return res.status(409).json({ error: "already_owned" });
// Compute capture cost (1 action per 10 billion population, doubled if opponent-controlled)
const planet = existing.planet_json;
const billions = planet?.population?.billions ?? 0;
const baseCost = Math.max(1, Math.ceil(billions / 10));
const isOpponentControlled = existing.discovered_by !== null && existing.discovered_by !== team;
const cost = isOpponentControlled ? baseCost * 2 : baseCost;
// Consume team actions
const cfg = getConfig();
const now = new Date();
const teamRow = await getTeamActionsRow(team);
if (!teamRow || new Date(teamRow.quota_reset_at) <= now) {
// Compute fresh quota
const rows = await getGridCells(worldSeed);
const milPower = computeTeamMilitaryPower(team, rows, cfg.militaryPower ?? {});
const milDeductions = await getMilitaryDeductions(worldSeed);
const milNet = milPower - (milDeductions[team] ?? 0);
const milBonus = Math.floor(Math.max(0, milNet) / 10000);
const totalActions = cfg.teamActionQuota + milBonus;
if (totalActions < cost) {
return res.status(429).json({ error: "team_quota_exhausted", cost, teamActionsRemaining: totalActions });
}
await resetTeamActions(team, totalActions - cost, nextNoonUtc().toISOString());
} else {
if (teamRow.actions_remaining < cost) {
return res.status(429).json({ error: "team_quota_exhausted", cost, teamActionsRemaining: teamRow.actions_remaining });
}
const updated = await decrementTeamActionsBy(team, cost);
if (!updated) {
return res.status(429).json({ error: "team_quota_exhausted", cost, teamActionsRemaining: 0 });
}
}
// Transfer ownership to capturing team
await setTileOwner(worldSeed, x, y, team);
const updatedCell = await getExistingCell(worldSeed, x, y);
const updatedTeamRow = await getTeamActionsRow(team);
res.json({
success: true,
cell: rowToCellPayload(updatedCell),
teamActionsRemaining: updatedTeamRow?.actions_remaining ?? null,
});
} catch (e) {
console.error(e);
res.status(500).json({ error: "database_error" });
}
});
// GET /api/team-quota?team=blue|red
router.get("/team-quota", async (req, res) => {
const team = typeof req.query.team === "string" ? req.query.team : "";
@@ -213,7 +286,7 @@ router.get("/team-quota", async (req, res) => {
const milDeductions = await getMilitaryDeductions(worldSeed);
const milNet = milPower - (milDeductions[team] ?? 0);
const milBonus = Math.floor(Math.max(0, milNet) / 10000);
actionsRemaining = 100 + milBonus;
actionsRemaining = cfg.teamActionQuota + milBonus;
} else {
actionsRemaining = teamRow.actions_remaining;
}
@@ -364,11 +437,11 @@ router.post("/military/attack", authMiddleware, async (req, res) => {
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
// Target cell must exist, have a planet, 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" });
if (!existing.discovered_by || existing.discovered_by === attackingTeam) {
return res.status(409).json({ error: "cannot_attack_own_or_neutral_tile" });
}
// Deduct 1 billion (1.0 in "billions" unit) from the attacking team