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

@@ -1,6 +1,7 @@
{
"dailyActionQuota": 100,
"databaseWipeoutIntervalSeconds": 604800,
"teamActionQuota": 100,
"databaseWipeoutIntervalSeconds": 600,
"configReloadIntervalSeconds": 30,
"elementWorth": {
"common": 0.1,

View File

@@ -22,14 +22,14 @@
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="1.122"
inkscape:cx="75.311944"
inkscape:cy="524.95544"
inkscape:window-width="1718"
inkscape:window-height="1341"
inkscape:window-x="1712"
inkscape:window-y="231"
inkscape:window-maximized="0"
inkscape:zoom="0.79337381"
inkscape:cx="255.23908"
inkscape:cy="554.59355"
inkscape:window-width="3440"
inkscape:window-height="1351"
inkscape:window-x="-9"
inkscape:window-y="222"
inkscape:window-maximized="1"
inkscape:current-layer="svg1" /><defs
id="defs1"><clipPath
clipPathUnits="userSpaceOnUse"
@@ -354,8 +354,9 @@
.st32{display:none;fill:#1D7A18;}
.st33{fill:#631077;}
</style><path
style="opacity:1;fill:#000000;fill-opacity:1;stroke-width:250;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke fill markers"
d="m 288.32442,328.43138 c 0,0 -23.84135,-35.205 -12.47771,-55.7041 11.36363,-20.49911 28.07486,-26.06952 31.19429,-36.09626 3.11943,-10.02674 13.36899,-39.66132 22.05883,-43.00357 8.68984,-3.34224 16.93404,-6.90731 26.29233,-6.90731 9.35829,0 25.8467,4.45633 33.86809,2.22817 8.02139,-2.22817 34.53655,-13.81462 45.45455,-12.70054 10.918,1.11409 38.77005,5.79323 43.67201,11.36364 4.90197,5.57041 18.27095,28.96613 17.37968,34.75936 -0.89126,5.79323 -2.45098,18.93939 -2.00534,28.07487 0.44563,9.13547 4.01069,19.83066 3.11943,24.95543 -0.89127,5.12478 -5.57041,17.15687 -5.57041,17.15687 0,0 11.80927,2.67379 13.14616,-4.23352 1.3369,-6.9073 7.13013,-16.26559 11.14082,-20.94474 4.0107,-4.67914 26.29234,-25.62388 27.85205,-30.74866 1.55972,-5.12478 8.46703,-22.72727 16.04279,-27.40642 7.57575,-4.67914 40.10695,-10.47237 55.92691,-9.35829 15.81997,1.11408 40.7754,2.45098 52.13904,6.68449 11.36364,4.23352 28.96613,16.04279 32.75401,22.28164 3.78788,6.23886 12.03209,20.2763 12.47772,24.28699 0.44563,4.0107 -7.13013,21.61319 -7.13013,21.61319 0,0 10.47238,14.70589 16.48842,16.93405 6.01604,2.22816 10.69519,1.78253 14.48306,6.01604 3.78788,4.23351 -0.22281,9.58111 3.78788,10.91801 4.0107,1.33689 13.81462,2.89661 14.9287,8.02139 1.11408,5.12477 -4.90196,12.92335 -3.56506,18.49376 1.3369,5.57041 5.12478,18.49376 4.90196,22.95009 -0.22282,4.45633 -7.57576,19.16221 -7.57576,19.16221 0,0 13.36899,2.45098 10.69519,11.14082 -2.6738,8.68984 -13.5918,14.03743 -13.5918,14.03743 0,0 -4.67914,18.49376 -0.22282,33.86809 4.45633,15.37434 14.9287,42.55794 14.9287,53.69876 0,11.14082 -3.78788,80.88235 -7.13012,102.49554 -3.34225,21.61319 -18.04813,78.43138 -28.96613,97.14795 -10.91801,18.71658 -16.71123,28.74332 -22.28164,32.97683 -5.57042,4.23351 -40.77541,20.27629 -40.77541,20.27629 l -47.68271,9.58111 c 0,0 12.47772,31.1943 10.47237,38.99287 -2.00534,7.79857 -1.33689,13.14617 -8.68984,14.70588 -7.35294,1.55972 -27.85205,-0.66845 -32.08556,-2.22816 -4.23351,-1.55972 -9.80392,-6.23886 -15.81996,-6.46168 -6.01604,-0.22281 -18.9394,-6.90731 -25.62389,-5.57041 -6.68449,1.3369 -11.80927,1.3369 -14.48306,7.35294 -2.6738,6.01605 -17.6025,36.09626 -17.6025,36.09626 0,0 -21.83601,-5.34759 -26.51515,1.11408 -4.67915,6.46168 2.00535,16.04278 -2.22817,19.16221 -4.23351,3.11943 -18.04812,1.55972 -21.16755,-4.90196 -3.11943,-6.46167 0.44563,-13.36898 -7.13013,-15.15151 -7.57576,-1.78253 -26.29233,-2.89662 -29.18895,-0.22282 -2.89661,2.6738 -22.50445,12.47772 -28.29768,13.81462 -5.79323,1.3369 -18.93939,9.13547 -23.61854,7.57576 -4.67914,-1.55972 -7.57576,-16.93405 -16.93405,-19.16222 -9.35828,-2.22816 -13.5918,-7.79857 -13.81461,-11.58645 -0.22282,-3.78788 -5.57041,-21.39037 -10.02674,-25.17825 -4.45633,-3.78788 -6.46168,-5.79323 -11.80927,-0.66845 -5.34759,5.12478 -13.5918,15.81996 -19.60784,14.48307 -6.01605,-1.3369 -16.48842,-1.3369 -16.93405,-6.23886 -0.44563,-4.90196 3.56506,-26.96079 8.68984,-38.10161 5.12478,-11.14082 10.47237,-26.06952 10.02674,-31.86274 -0.44563,-5.79323 0.66845,-19.16221 -10.69519,-27.18361 -11.36364,-8.02139 -19.83066,-16.48841 -27.85205,-17.15686 -8.02139,-0.66845 -23.39572,0 -22.50446,-7.79857 0.89127,-7.79858 20.05348,-44.56328 17.82532,-55.7041 -2.22817,-11.14082 -61.0517,-72.19252 -107.39751,-69.51872 -46.34581,2.6738 -96.256685,21.39037 -103.38681,49.46524 -7.130125,28.07487 -21.836007,165.7754 8.02139,182.70945 29.857398,16.93405 134.13547,48.12834 147.95009,63.72549 13.81462,15.59715 105.16934,95.36542 126.11408,108.73441 20.94475,13.36898 219.25134,24.95543 238.85919,18.71657 19.60784,-6.23886 134.13547,-74.42068 170.67736,-91.80035 36.54189,-17.37968 90.01783,-15.59715 135.91801,-23.61854 45.90017,-8.02139 76.64884,-60.16043 73.08378,-83.77897 -3.56507,-23.61854 -94.02853,-135.02674 -68.18182,-204.99109 25.8467,-69.96435 75.31194,-163.54724 75.31194,-194.2959 0,-30.74867 0.44563,-141.2656 -33.42246,-147.50446 -33.86809,-6.23886 -101.15865,16.48841 -119.42959,0.89127 -18.27095,-15.59715 -52.13904,-75.31195 -65.95366,-91.80036 -13.81461,-16.48842 -78.877,-95.365422 -146.16756,-102.495547 -67.29055,-7.130125 -189.39394,41.889484 -228.60962,41.889484 -39.21569,0 -107.39751,-54.367202 -144.38503,-47.68271 -36.98752,6.684492 -148.395725,37.878788 -152.406421,81.996433 -4.010695,44.11765 -16.934046,159.53655 23.172906,183.15508 40.106955,23.61854 110.962565,81.10517 149.732625,64.61676 38.77005,-16.48841 50.3565,-40.55258 50.3565,-40.55258 z"
id="outer_regions"
inkscape:label="outer_regions"><title
style="opacity:1;fill:#000000;fill-opacity:1;stroke-width:250;stroke-linecap:round;stroke-linejoin:round;paint-order:stroke fill markers"
inkscape:label="outer_regions"
d="m 224.75391,38.650391 c -2.54074,-0.03101 -4.97344,0.146672 -7.28516,0.564453 -36.98752,6.684492 -148.395554,37.880402 -152.40625,81.998046 -4.010695,44.11765 -8.53489,149.07962 23.171875,183.1543 31.902735,34.28528 65.067715,81.91168 59.714845,168.0039 -6.71811,108.04971 -90.949717,101.4651 -100.265626,138.14649 -7.130125,28.07487 -21.837866,165.77493 8.019531,182.70898 29.857398,16.93405 134.136555,48.12746 147.951175,63.72461 13.81462,15.59715 105.16854,95.36539 126.11328,108.73438 20.94475,13.36898 219.25152,24.95565 238.85937,18.71679 19.60784,-6.23886 134.13585,-74.42111 170.67774,-91.80078 36.54189,-17.37968 90.01779,-15.5958 135.91797,-23.61718 45.90017,-8.02139 76.64904,-60.16076 73.08398,-83.7793 -3.56507,-23.61854 -94.02835,-135.02589 -68.18164,-204.99024 25.8467,-69.96435 75.3125,-163.54821 75.3125,-194.29687 0,-30.74867 0.44426,-141.26505 -33.42383,-147.50391 -33.86809,-6.23886 -101.15875,16.48777 -119.42969,0.89063 -18.27095,-15.59715 -52.1385,-75.31237 -65.95312,-91.80078 C 722.81625,131.01549 657.75345,52.139891 590.46289,45.009766 523.17234,37.879641 401.0692,86.898437 361.85352,86.898438 c -36.76471,0 -98.98855,-47.78294 -137.09961,-48.248047 z M 432.49414,176.18164 c 0.79591,-0.0242 1.53833,-0.003 2.2207,0.0664 10.918,1.11409 38.76992,5.79287 43.67188,11.36328 4.90197,5.57041 18.27018,28.96653 17.3789,34.75976 -0.89125,5.79323 -2.44954,18.93874 -2.0039,28.07422 0.44563,9.13547 4.0104,19.83031 3.11914,24.95508 -0.89127,5.12478 -5.57031,17.1582 -5.57031,17.1582 0,0 11.80959,2.67294 13.14648,-4.23437 1.3369,-6.9073 7.12994,-16.26616 11.14063,-20.94531 4.0107,-4.67914 26.29185,-25.62327 27.85156,-30.74805 1.55972,-5.12478 8.46721,-22.7271 16.04297,-27.40625 7.57575,-4.67914 40.10582,-10.4715 55.92578,-9.35742 15.81997,1.11408 40.77698,2.45008 52.14062,6.68359 11.36364,4.23352 28.96603,16.0424 32.75391,22.28125 3.78788,6.23886 12.03093,20.27642 12.47656,24.28711 0.44563,4.0107 -7.1289,21.61328 -7.1289,21.61328 0,0 10.47224,14.70544 16.48828,16.9336 6.01604,2.22816 10.69455,1.78406 14.48242,6.01757 3.78788,4.23351 -0.22358,9.58107 3.78711,10.91797 4.0107,1.33689 13.81561,2.89671 14.92969,8.02149 1.11408,5.12477 -4.90136,12.92177 -3.56446,18.49218 1.3369,5.57041 5.12321,18.49485 4.90039,22.95118 -0.22282,4.45633 -7.57422,19.16211 -7.57421,19.16211 0,0 13.36911,2.45078 10.69531,11.14062 -2.6738,8.68984 -13.5918,14.03711 -13.5918,14.03711 0,0 -4.67898,18.49481 -0.22266,33.86914 4.45633,15.37434 14.92774,42.55645 14.92774,53.69727 0,11.14082 -3.78862,80.8829 -7.13086,102.49609 -3.34225,21.61319 -18.04684,78.43187 -28.96484,97.14844 -10.91801,18.71658 -16.71084,28.74305 -22.28125,32.97656 -5.57042,4.23351 -40.7754,20.27539 -40.7754,20.27539 l -47.68359,9.58203 c 0,0 12.47801,31.19362 10.47266,38.99219 -2.00534,7.79857 -1.33651,13.14732 -8.68946,14.70703 -7.35294,1.55972 -27.85242,-0.6688 -32.08593,-2.22851 -4.23351,-1.55972 -9.80428,-6.24008 -15.82032,-6.4629 -6.01604,-0.22281 -18.93855,-6.90721 -25.62304,-5.57031 -6.68449,1.3369 -11.81059,1.33748 -14.48438,7.35352 -2.6738,6.01605 -17.60156,36.0957 -17.60156,36.0957 0,0 -21.83648,-5.34644 -26.51562,1.11523 -4.67915,6.46169 2.005,16.04268 -2.22852,19.16211 -4.23351,3.11943 -18.04854,1.55934 -21.16797,-4.90234 -3.11943,-6.46167 0.44685,-13.36981 -7.12891,-15.15234 -7.57576,-1.78253 -26.29283,-2.89646 -29.18945,-0.22266 -2.89661,2.6738 -22.50364,12.47755 -28.29687,13.81445 -5.79323,1.3369 -18.93999,9.13589 -23.61914,7.57618 -4.67914,-1.55972 -7.57531,-16.93394 -16.9336,-19.16211 -9.35828,-2.22816 -13.59164,-7.79806 -13.81445,-11.58594 -0.22282,-3.78788 -5.57101,-21.38986 -10.02735,-25.17774 -4.45632,-3.78788 -6.461,-5.7947 -11.80859,-0.66992 -5.34759,5.12478 -13.59333,15.82127 -19.60937,14.48438 -6.01605,-1.3369 -16.48797,-1.33828 -16.9336,-6.24024 -0.44563,-4.90196 3.56468,-26.96074 8.68946,-38.10156 5.12478,-11.14082 10.47297,-26.06811 10.02734,-31.86133 -0.44563,-5.79323 0.66833,-19.16219 -10.69531,-27.18359 -11.36364,-8.02139 -19.83018,-16.48975 -27.85157,-17.1582 -8.02139,-0.66845 -23.39516,0.002 -22.5039,-7.79688 0.89127,-7.79858 20.05238,-44.56426 17.82422,-55.70508 -5.91385,-29.56912 -90.50493,-52.46721 -73.97461,-107.84375 8.91265,-29.85739 47.68222,-46.34576 41.44336,-78.87695 -3.29922,-17.20304 4.73668,-71.76212 12.0332,-74.86523 38.77005,-16.48841 50.35547,-40.55274 50.35547,-40.55274 0,0 -23.8402,-35.20598 -12.47656,-55.70508 11.36363,-20.49911 28.07393,-26.06896 31.19336,-36.0957 3.11943,-10.02674 13.36875,-39.66166 22.05859,-43.00391 8.68984,-3.34224 16.93468,-6.90625 26.29297,-6.90625 9.35829,0 25.8458,4.45668 33.86719,2.22852 7.52005,-2.08891 31.2958,-12.40447 43.23437,-12.76758 z"
sodipodi:nodetypes="ssssssssssssssssssssssssssscsssssssscsssssscscsssssccsssssscsssssssssssssssssssssscsssssss"><title
id="title36">outer_regions</title></path></svg>

Before

Width:  |  Height:  |  Size: 76 KiB

After

Width:  |  Height:  |  Size: 76 KiB

View File

@@ -93,10 +93,6 @@
<div class="scoreTeam scoreTeam--blue">
<span class="scoreTeamName">Résistance</span>
<div class="scoreStats">
<div class="scoreStat">
<span class="scoreStatVal scoreValue" id="scoreBlue">0</span>
<span class="scoreStatLabel">Tuiles</span>
</div>
<div class="scoreStat">
<span class="scoreStatVal scoreVP" id="vpBlue">0</span>
<span class="scoreStatLabel">Points</span>
@@ -111,10 +107,6 @@
<span class="scoreStatVal scoreVP" id="vpRed">0</span>
<span class="scoreStatLabel">Points</span>
</div>
<div class="scoreStat">
<span class="scoreStatVal scoreValue" id="scoreRed">0</span>
<span class="scoreStatLabel">Tuiles</span>
</div>
</div>
</div>
</div>
@@ -267,10 +259,10 @@
En explorant la galaxie, vous révélez des tuiles qui peuvent contenir des planètes. Chaque planète possède des ressources naturelles (minerais, bois, pétrole, etc.) dont la valeur contribue au revenu de votre équipe.</p>
<p><strong>⚡ Bonus d'exploration</strong><br/>
Les planètes produisent des éléments (matières premières, carburant, nourriture, science…) qui offrent un bonus cumulatif. Ce bonus réduit le temps de recharge entre deux clics, permettant d'explorer plus vite.</p>
Les planètes produisent des éléments (matières premières, carburant, nourriture, science…) qui offrent un bonus cumulatif. Ce bonus augmente le pourcentage d'actions disponibles pour vous.</p>
<p><strong>⚔️ Puissance militaire</strong><br/>
La population des planètes conquises fournit des unités militaires. Lorsque vous accumulez suffisamment de troupes, vous pouvez lancer une attaque sur une tuile ennemie pour la capturer. Chaque attaque consomme une partie de vos forces.</p>
La population des planètes conquises fournit de la puissance militaire. Cette puissance augmente le quota d'actions d'équipe pour capturer des planètes.</p>
</div>
</details>
@@ -313,6 +305,19 @@
</div>
</div>
</div>
<!-- Planet capture confirmation modal -->
<div class="attackOverlay hidden" id="captureOverlay">
<div class="attackModal">
<div class="attackModal__icon">🏴</div>
<div class="attackModal__title">Capturer la Planète</div>
<div class="attackModal__body" id="captureModalBody"></div>
<div class="attackModal__actions">
<button type="button" class="attackModal__btn attackModal__btn--cancel" id="captureModalNo">Annuler</button>
<button type="button" class="attackModal__btn attackModal__btn--confirm" id="captureModalYes">Capturer !</button>
</div>
</div>
</div>
</main>
</div>

View File

@@ -10,15 +10,11 @@ export async function apiFetchConfig(team) {
return res.json();
}
export async function apiFetchScores() {
const res = await fetch("/api/scores");
if (!res.ok) throw new Error("scores_fetch_failed");
return res.json();
}
/** Returns the raw Response so the caller can inspect status codes (410, etc.). */
export async function apiFetchGrid(seed) {
return fetch(`/api/grid/${encodeURIComponent(seed)}`);
const token = localStorage.getItem("authToken");
const headers = token ? { Authorization: `Bearer ${token}` } : {};
return fetch(`/api/grid/${encodeURIComponent(seed)}`, { headers });
}
/** Returns the raw Response so the caller can inspect status codes (409, 410, etc.). */
@@ -136,3 +132,16 @@ export async function apiFetchCellAttackCount(x, y) {
if (!res.ok) throw new Error("cell_attacks_fetch_failed");
return res.json();
}
/** Returns the raw Response so the caller can inspect status codes. */
export async function apiCaptureCell(seed, x, y) {
const token = localStorage.getItem("authToken");
return fetch("/api/cell/capture", {
method: "POST",
headers: {
"Content-Type": "application/json",
...(token ? { Authorization: `Bearer ${token}` } : {}),
},
body: JSON.stringify({ seed, x, y }),
});
}

View File

@@ -60,7 +60,7 @@ export function computeTeamIncome(team, cells, resourceWorth) {
let total = 0;
for (const [, meta] of cells) {
if (meta.discoveredBy !== team) continue;
if (meta.controlledBy !== team) continue;
if (!meta.hasPlanet || !meta.planet) continue;
const { naturalResources } = meta.planet;
if (!naturalResources) continue;
@@ -102,7 +102,7 @@ const ELEMENT_LABEL_TO_KEY = Object.fromEntries(
export function computeTeamElementBonus(team, cells, elementWorth) {
let bonus = 0;
for (const [, meta] of cells) {
if (meta.discoveredBy !== team) continue;
if (meta.controlledBy !== team) continue;
if (!meta.hasPlanet || !meta.planet) continue;
const { production } = meta.planet;
if (!production) continue;
@@ -130,7 +130,7 @@ export function computeTeamElementBonusDetailed(team, cells, elementWorth) {
const byElement = new Map();
let total = 0;
for (const [, meta] of cells) {
if (meta.discoveredBy !== team) continue;
if (meta.controlledBy !== team) continue;
if (!meta.hasPlanet || !meta.planet) continue;
const { production } = meta.planet;
if (!production) continue;
@@ -298,7 +298,7 @@ export function computeTeamMilitaryDetailed(team, cells, militaryPower) {
const byType = new Map();
let total = 0;
for (const [, meta] of cells) {
if (meta.discoveredBy !== team) continue;
if (meta.controlledBy !== team) continue;
if (!meta.hasPlanet || !meta.planet) continue;
const pop = meta.planet.population;
if (!pop) continue;

View File

@@ -1,6 +1,6 @@
import { fnv1a32, hash2u32, mulberry32 } from "./rng.js";
import { formatPlanet, generatePlanet } from "./planetGeneration.js";
import { apiFetchConfig, apiFetchScores, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints, apiFetchActivePlayers, apiFetchMilitaryDeductions, apiMilitaryAttack } from "./api.js";
import { apiFetchConfig, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints, apiFetchActivePlayers, apiFetchMilitaryDeductions, apiMilitaryAttack, apiCaptureCell } from "./api.js";
import { computeTeamIncome, computeTeamElementBonus, computeTeamElementBonusDetailed, renderResourceTable, renderElementBonusTable, setEconSort, getEconSort, setElemSort, getElemSort, computeTeamMilitaryDetailed, renderMilitaryTable, setMilSort, getMilSort } from "./economy.js";
// ── Constants ─────────────────────────────────────────────────────────────────
@@ -34,7 +34,7 @@ export const GAME_CONFIG = {
};
window.GAME_CONFIG = GAME_CONFIG;
/** @type {Map<string, { discoveredBy: string, hasPlanet: boolean, planet: object|null }>} */
/** @type {Map<string, { controlledBy: string|null, hasPlanet: boolean, planet: object|null }>} */
export const cells = new Map();
export let currentTeam = "blue";
@@ -206,8 +206,6 @@ const cooldownCfgEl = document.getElementById("cooldownConfig");
const seedDisplayEl = document.getElementById("worldSeedDisplay");
const nextPeriodEl = document.getElementById("nextPeriodUtc");
const resetCountEl = document.getElementById("refreshCountdown");
const scoreBlueEl = document.getElementById("scoreBlue");
const scoreRedEl = document.getElementById("scoreRed");
const vpBlueEl = document.getElementById("vpBlue");
const vpRedEl = document.getElementById("vpRed");
const dbCreatedAtEl = document.getElementById("dbCreatedAt");
@@ -231,6 +229,10 @@ const attackOverlayEl = document.getElementById("attackOverlay");
const attackModalBodyEl = document.getElementById("attackModalBody");
const attackModalYesEl = document.getElementById("attackModalYes");
const attackModalNoEl = document.getElementById("attackModalNo");
const captureOverlayEl = document.getElementById("captureOverlay");
const captureModalBodyEl = document.getElementById("captureModalBody");
const captureModalYesEl = document.getElementById("captureModalYes");
const captureModalNoEl = document.getElementById("captureModalNo");
const teamQuotaEl = document.getElementById("teamActionsRemaining");
// ── Cell helpers ──────────────────────────────────────────────────────────────
@@ -247,8 +249,8 @@ function hasPlanetAt(x, y) {
}
export function cellMeta(key) { return cells.get(key) ?? null; }
export function isOpponentTile(key) { const m = cellMeta(key); return m !== null && m.discoveredBy !== currentTeam; }
export function isOwnTile(key) { const m = cellMeta(key); return m !== null && m.discoveredBy === currentTeam; }
export function isOpponentTile(key) { const m = cellMeta(key); return m !== null && m.controlledBy !== null && m.controlledBy !== currentTeam; }
export function isOwnTile(key) { const m = cellMeta(key); return m !== null && m.controlledBy === currentTeam; }
// ── Config display ────────────────────────────────────────────────────────────
@@ -306,14 +308,6 @@ export function updateResetCountdown() {
// ── Scores ────────────────────────────────────────────────────────────────────
export async function fetchAndApplyScores() {
try {
const { blue, red } = await apiFetchScores();
if (scoreBlueEl) scoreBlueEl.textContent = String(blue ?? 0);
if (scoreRedEl) scoreRedEl.textContent = String(red ?? 0);
} catch { /* ignore */ }
}
export async function loadVictoryPoints() {
try {
const { blue, red } = await apiFetchVictoryPoints();
@@ -537,7 +531,7 @@ export async function fetchGridForSeed(seed, depth = 0) {
cells.clear();
for (const row of data.cells || []) {
cells.set(cellKey(row.x, row.y), {
discoveredBy: row.discovered_by ?? row.discoveredBy,
controlledBy: row.discovered_by ?? row.discoveredBy ?? null,
hasPlanet: Boolean(row.has_planet),
planet: row.planet_json ?? null,
});
@@ -622,20 +616,22 @@ export function draw() {
const meta = cellMeta(k);
const alpha = tileAlpha(k);
ctx.globalAlpha = alpha;
if (!meta) ctx.fillStyle = COLOR_RING_IDLE;
else if (meta.discoveredBy !== currentTeam) ctx.fillStyle = COLOR_OPPONENT_GREY;
else ctx.fillStyle = currentTeam === "blue" ? COLOR_BLUE_DISCOVERED : COLOR_RED_DISCOVERED;
if (!meta) ctx.fillStyle = COLOR_RING_IDLE;
else if (!meta.hasPlanet) ctx.fillStyle = COLOR_OPPONENT_GREY; // empty or unexploitable
else if (meta.controlledBy === null) ctx.fillStyle = COLOR_OPPONENT_GREY; // neutral planet
else if (meta.controlledBy === currentTeam) ctx.fillStyle = currentTeam === "blue" ? COLOR_BLUE_DISCOVERED : COLOR_RED_DISCOVERED;
else ctx.fillStyle = currentTeam === "blue" ? COLOR_RED_DISCOVERED : COLOR_BLUE_DISCOVERED; // opponent-controlled
ctx.fillRect(x * cw, y * ch, cw, ch);
ctx.globalAlpha = 1;
}
}
// 5. Draw planet dots on own discovered tiles
// 5. Draw planet dots on all revealed tiles with planets
for (const [key, meta] of cells) {
if (meta.discoveredBy !== currentTeam) continue;
if (!meta.hasPlanet) continue;
const [xs, ys] = key.split(",").map(Number);
if (xs < xMin || xs > xMax || ys < yMin || ys > yMax) continue;
if (!isExploitable(xs, ys) || !meta.hasPlanet) continue;
if (!isExploitable(xs, ys)) continue;
const h32 = hash2u32(xs, ys, seedU32 ^ 0x1337);
const rng = mulberry32(h32);
ctx.fillStyle = `hsl(${Math.floor(rng() * 360)} 85% 65%)`;
@@ -682,13 +678,15 @@ function refreshCursor(ev) {
return;
}
const key = cellKey(cell.x, cell.y);
if (isOpponentTile(key)) {
const milPower = GAME_CONFIG.militaryPower;
const myMilRaw = computeTeamMilitaryDetailed(currentTeam, cells, milPower).total;
const myDeduct = currentTeam === "blue" ? milDeductBlue : milDeductRed;
canvas.style.cursor = (myMilRaw - myDeduct >= 1.0) ? "crosshair" : "default";
} else {
const meta = cells.get(key);
if (!meta) {
// Not yet revealed — can reveal if quota available
canvas.style.cursor = cooldownActive() ? "default" : "pointer";
} else if (meta.hasPlanet && meta.controlledBy !== currentTeam) {
// Capturable (neutral or opponent-controlled) planet
canvas.style.cursor = "pointer";
} else {
canvas.style.cursor = "default";
}
}
@@ -702,7 +700,7 @@ function applyRevealPayload(cell) {
const _revealKey = cellKey(cell.x, cell.y);
markTileReveal(_revealKey);
cells.set(_revealKey, {
discoveredBy: cell.discoveredBy ?? currentTeam,
controlledBy: cell.discoveredBy ?? null,
hasPlanet: Boolean(cell.hasPlanet),
planet: cell.planet ?? null,
});
@@ -717,21 +715,26 @@ function applyRevealPayload(cell) {
details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : Vide`;
return;
}
const controlStatus = cell.discoveredBy === null
? "Neutre (non capturée)"
: cell.discoveredBy === currentTeam
? `Contrôlée (${currentTeam === "blue" ? "Résistance" : "Premier Ordre"})`
: `Contrôlée par l'adversaire`;
hint.textContent = `(${cell.x},${cell.y}) Planète présente`;
details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : Planète\n\n${formatPlanet(cell.planet)}`;
details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : ${controlStatus}\n\n${formatPlanet(cell.planet)}`;
}
function showLocalSelection(x, y) {
const k = cellKey(x, y);
const meta = cellMeta(k);
details.classList.remove("details--hidden");
if (!isOwnTile(k)) return;
if (!meta) return;
if (!isExploitable(x, y)) {
hint.textContent = `(${x},${y}) Inexploitable`;
details.textContent = `Tuile (${x},${y})\n\nStatus : Inexploitable`;
return;
}
const meta = cellMeta(k);
if (!meta?.hasPlanet) {
if (!meta.hasPlanet) {
hint.textContent = `(${x},${y}) Vide`;
details.textContent = `Tuile (${x},${y})\n\nStatus : Vide`;
return;
@@ -741,8 +744,13 @@ function showLocalSelection(x, y) {
const h = hash2u32(x, y, seedU32 ^ 0xa5a5a5a5);
planet = generatePlanet(mulberry32(h));
}
const controlStatus = meta.controlledBy === null
? "Neutre (non capturée)"
: meta.controlledBy === currentTeam
? `Contrôlée (${currentTeam === "blue" ? "Résistance" : "Premier Ordre"})`
: `Contrôlée par l'adversaire`;
hint.textContent = `(${x},${y}) Planète présente`;
details.textContent = `Tuile (${x},${y})\n\nStatus : Planète\n\n${formatPlanet(planet)}`;
details.textContent = `Tuile (${x},${y})\n\nStatus : ${controlStatus}\n\n${formatPlanet(planet)}`;
}
// ── Military attack modal ────────────────────────────────────────────────────
@@ -772,89 +780,133 @@ function showAttackModal(milNetM, x, y) {
});
}
// ── Planet capture modal ─────────────────────────────────────────────────────
/**
* Shows the capture confirmation modal.
* Returns a Promise<boolean> — true if confirmed, false if cancelled.
*/
function showCaptureModal(planet, controlledBy, cost, x, y) {
const opponentName = currentTeam === "blue" ? "Premier Ordre" : "Résistance";
const controlLabel = controlledBy === null
? "Planète neutre"
: `Planète contrôlée par ${opponentName} (coût doublé)`;
captureModalBodyEl.textContent =
`Planète (${x}, ${y}) — ${planet?.name ?? "Inconnue"}\n\n` +
`${controlLabel}\n\n` +
`Coût : ${cost} action${cost > 1 ? "s" : ""} d'équipe\n` +
`Actions d'équipe disponibles : ${teamActionsRemaining ?? "?"}\n\n` +
`La planète passera sous votre contrôle. Ses ressources, bonus et puissance militaire\n` +
`seront calculés pour votre équipe.`;
captureOverlayEl.classList.remove("hidden");
return new Promise((resolve) => {
function cleanup() {
captureOverlayEl.classList.add("hidden");
captureModalYesEl.removeEventListener("click", onYes);
captureModalNoEl.removeEventListener("click", onNo);
}
function onYes() { cleanup(); resolve(true); }
function onNo() { cleanup(); resolve(false); }
captureModalYesEl.addEventListener("click", onYes);
captureModalNoEl.addEventListener("click", onNo);
});
}
// ── Canvas click handler ──────────────────────────────────────────────────────
async function onCanvasClick(ev) {
const cell = pickCell(ev);
if (!cell || !isExploitable(cell.x, cell.y)) return;
const key = cellKey(cell.x, cell.y);
const meta = cells.get(key);
// ── Opponent tile: offer military attack if team has ≥ 1 billion soldiers ──
if (isOpponentTile(key)) {
const milPower = GAME_CONFIG.militaryPower;
const myMilRaw = computeTeamMilitaryDetailed(currentTeam, cells, milPower).total;
const myDeduct = currentTeam === "blue" ? milDeductBlue : milDeductRed;
const myMilNet = myMilRaw - myDeduct; // in billions
if (myMilNet >= 1.0) {
const confirmed = await showAttackModal(myMilNet * 1000, cell.x, cell.y);
if (!confirmed) return;
try {
const res = await apiMilitaryAttack(seedStr, cell.x, cell.y);
if (res.status === 410) {
hint.textContent = "Le serveur change sa base de planètes — synchronisation...";
await refreshFromServer(); return;
}
if (res.status === 409) {
hint.textContent = "Impossible d'attaquer votre propre tuile."; return;
}
if (!res.ok) { hint.textContent = "Erreur lors de l'attaque militaire."; return; }
const data = await res.json();
if (currentTeam === "blue") milDeductBlue = data.deductions.blue ?? milDeductBlue;
else milDeductRed = data.deductions.red ?? milDeductRed;
// Transfer tile in local cells Map
const existing = cells.get(key);
markTileReveal(key);
cells.set(key, { ...existing, discoveredBy: currentTeam });
hint.textContent = `⚔️ Tuile (${cell.x},${cell.y}) conquise !`;
updateEconomyDisplay();
fetchAndApplyScores();
draw();
} catch {
hint.textContent = "Erreur réseau lors de l'attaque militaire.";
}
} else {
hint.textContent = `Puissance militaire insuffisante — il faut ≥ 1 000 (actuellement ${(myMilNet * 1000).toFixed(1)}).`;
}
return;
}
if (isOwnTile(key)) { showLocalSelection(cell.x, cell.y); return; }
if (cooldownActive()) {
hint.textContent = "Quota d'actions épuisé pour aujourd'hui — revenez après 12h00 UTC.";
return;
}
try {
const res = await apiRevealCell(seedStr, cell.x, cell.y, currentTeam);
if (res.status === 409) {
hint.textContent = "Cette tuile a déjà été découverte par votre adversaire.";
await fetchGridForSeed(seedStr);
draw();
return;
}
if (res.status === 429) {
actionsRemaining = 0;
updateActionsDisplay();
// ── Not yet revealed: first click = spend user action to reveal ──
if (!meta) {
if (cooldownActive()) {
hint.textContent = "Quota d'actions épuisé pour aujourd'hui — revenez après 12h00 UTC.";
return;
}
try {
const res = await apiRevealCell(seedStr, cell.x, cell.y, currentTeam);
if (res.status === 429) {
actionsRemaining = 0;
updateActionsDisplay();
hint.textContent = "Quota d'actions épuisé pour aujourd'hui — revenez après 12h00 UTC.";
return;
}
if (res.status === 410) {
hint.textContent = "Le serveur change sa base de planètes — synchronisation...";
await refreshFromServer(); return;
}
if (!res.ok) throw new Error("reveal");
applyRevealPayload(await res.json());
startCooldown();
updateEconomyDisplay();
draw();
} catch (e) {
if (e?.code === "SEED_EXPIRED") { await refreshFromServer(); return; }
hint.textContent = "Could not save reveal — check server / database.";
}
return;
}
// ── Already revealed: empty cell or own planet — show info only ──
if (!meta.hasPlanet || meta.controlledBy === currentTeam) {
showLocalSelection(cell.x, cell.y);
return;
}
// ── Revealed planet, neutral or opponent-controlled: offer capture ──
showLocalSelection(cell.x, cell.y);
const popBillions = meta.planet?.population?.billions ?? 0;
const baseCost = Math.max(1, Math.ceil(popBillions / 10));
const cost = (meta.controlledBy !== null) ? baseCost * 2 : baseCost;
if (teamActionsRemaining !== null && teamActionsRemaining < cost) {
hint.textContent = `Planète repérée. Actions d'équipe insuffisantes pour capturer (coût : ${cost}, disponibles : ${teamActionsRemaining}).`;
return;
}
const confirmed = await showCaptureModal(meta.planet, meta.controlledBy, cost, cell.x, cell.y);
if (!confirmed) return;
try {
const res = await apiCaptureCell(seedStr, cell.x, cell.y);
if (res.status === 410) {
hint.textContent = "Le serveur change sa base de planètes — synchronisation...";
await refreshFromServer();
await refreshFromServer(); return;
}
if (res.status === 429) {
const data = await res.json();
teamActionsRemaining = data.teamActionsRemaining ?? teamActionsRemaining;
updateTeamQuotaDisplay();
hint.textContent = `Actions d'équipe insuffisantes (coût : ${data.cost ?? cost}, disponibles : ${data.teamActionsRemaining ?? "?"}).`;
return;
}
if (!res.ok) throw new Error("reveal");
applyRevealPayload(await res.json());
startCooldown();
if (res.status === 409) {
const data = await res.json();
if (data.error === "already_owned") {
hint.textContent = "Cette planète vous appartient déjà.";
} else {
hint.textContent = "Impossible de capturer cette tuile.";
}
return;
}
if (!res.ok) { hint.textContent = "Erreur lors de la capture."; return; }
const data = await res.json();
markTileReveal(key);
cells.set(key, { ...meta, controlledBy: currentTeam });
if (data.teamActionsRemaining !== undefined && data.teamActionsRemaining !== null) {
teamActionsRemaining = data.teamActionsRemaining;
updateTeamQuotaDisplay();
}
hint.textContent = `🏴 Planète (${cell.x},${cell.y}) capturée !`;
showLocalSelection(cell.x, cell.y);
updateEconomyDisplay();
draw();
fetchAndApplyScores();
} catch (e) {
if (e?.code === "SEED_EXPIRED") { await refreshFromServer(); return; }
hint.textContent = "Could not save reveal — check server / database.";
} catch {
hint.textContent = "Erreur réseau lors de la capture.";
}
}
@@ -887,7 +939,6 @@ export async function refreshFromServer() {
hint.textContent = "Le monde a été réinitilisaté. Vous pouvez cliquer sur une tuile pour recommencer le jeu.";
}
await fetchGridForSeed(seedStr);
await fetchAndApplyScores();
await fetchAndApplyActivePlayers();
updateEconomyDisplay();
draw();

View File

@@ -4,7 +4,6 @@ import {
updateResetCountdown,
fetchConfig,
fetchGridForSeed,
fetchAndApplyScores,
fetchAndApplyActivePlayers,
updateEconomyDisplay,
loadEconScores,
@@ -55,7 +54,6 @@ const ECON_TICK_SECONDS = 5;
function scheduleScorePoll() {
clearTimeout(scorePollTimer);
scorePollTimer = window.setTimeout(async () => {
await fetchAndApplyScores();
await fetchAndApplyActivePlayers();
await loadEconScores();
await loadElementBonus();
@@ -104,7 +102,6 @@ async function boot() {
try {
await fetchConfig();
await fetchGridForSeed(seedStr);
await fetchAndApplyScores();
await fetchAndApplyActivePlayers();
await loadEconScores();
await loadVictoryPoints();

View File

@@ -352,8 +352,8 @@ body {
}
.scoreTeam--blue .scoreVP {
color: rgba(130, 230, 130, 1);
text-shadow: 0 0 16px rgba(90, 200, 130, 0.35);
color: rgba(90, 200, 255, 1);
text-shadow: 0 0 16px rgba(90, 200, 255, 0.4);
}
.scoreTeam--red .scoreValue {
@@ -362,8 +362,8 @@ body {
}
.scoreTeam--red .scoreVP {
color: rgba(230, 150, 80, 1);
text-shadow: 0 0 16px rgba(220, 130, 60, 0.35);
color: rgba(220, 75, 85, 1);
text-shadow: 0 0 16px rgba(220, 75, 85, 0.4);
}
.scoreSep {

View File

@@ -7,9 +7,10 @@ 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 {{ dailyActionQuota: number, databaseWipeoutIntervalSeconds: number, debugModeForTeams: boolean, configReloadIntervalSeconds: number, resourceWorth: object, militaryPower: object }} */
/** @type {{ dailyActionQuota: number, teamActionQuota: number, databaseWipeoutIntervalSeconds: number, debugModeForTeams: boolean, configReloadIntervalSeconds: number, resourceWorth: object, militaryPower: object }} */
let cached = {
dailyActionQuota: 100,
teamActionQuota: 100,
databaseWipeoutIntervalSeconds: 21600,
debugModeForTeams: true,
configReloadIntervalSeconds: 30,
@@ -41,6 +42,9 @@ export function loadConfigFile() {
if (typeof j.dailyActionQuota === "number" && j.dailyActionQuota >= 1) {
cached.dailyActionQuota = Math.floor(j.dailyActionQuota);
}
if (typeof j.teamActionQuota === "number" && j.teamActionQuota >= 1) {
cached.teamActionQuota = Math.floor(j.teamActionQuota);
}
if (typeof j.databaseWipeoutIntervalSeconds === "number" && j.databaseWipeoutIntervalSeconds >= 60) {
cached.databaseWipeoutIntervalSeconds = j.databaseWipeoutIntervalSeconds;
}

View File

@@ -1,6 +1,7 @@
import { pool } from "./pools.js";
import { loadConfigFile, getConfig } from "../configLoader.js";
import { computeWorldSeedState } from "../worldSeed.js";
import { nextNoonUtc, resetAllUserActions } from "./usersDb.js";
let lastSeedSlot = null;
@@ -113,6 +114,32 @@ export async function initGameSchema() {
quota_reset_at TIMESTAMPTZ NOT NULL DEFAULT '1970-01-01 00:00:00+00'
);
`);
// ── Per-team cell visibility (who has revealed what) ──────────────────────
await pool.query(`
CREATE TABLE IF NOT EXISTS team_cell_visibility (
world_seed TEXT NOT NULL,
team TEXT NOT NULL CHECK (team IN ('blue', 'red')),
x SMALLINT NOT NULL,
y SMALLINT NOT NULL,
PRIMARY KEY (world_seed, team, x, y)
);
CREATE INDEX IF NOT EXISTS idx_tcv_seed_team ON team_cell_visibility (world_seed, team);
`);
// ── Make discovered_by nullable (NULL = neutral, uncaptured) ─────────────
await pool.query(`
ALTER TABLE grid_cells ALTER COLUMN discovered_by DROP NOT NULL;
ALTER TABLE grid_cells ALTER COLUMN discovered_by DROP DEFAULT;
DO $$
BEGIN
IF EXISTS (
SELECT 1 FROM pg_constraint WHERE conname = 'grid_cells_discovered_by_check'
) THEN
ALTER TABLE grid_cells DROP CONSTRAINT grid_cells_discovered_by_check;
END IF;
END $$;
ALTER TABLE grid_cells ADD CONSTRAINT grid_cells_discovered_by_check
CHECK (discovered_by IS NULL OR discovered_by IN ('blue', 'red'));
`);
}
// ── World-seed epoch ──────────────────────────────────────────────────────────
@@ -153,6 +180,19 @@ export async function ensureSeedEpoch() {
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]);
await pool.query("DELETE FROM team_cell_visibility WHERE world_seed != $1", [worldSeed]);
// Reset both team and user quotas to server defaults (no bonuses) for the new period
const cfg = getConfig();
const nextNoon = nextNoonUtc().toISOString();
await pool.query(
`INSERT INTO team_action_quota (team, actions_remaining, quota_reset_at)
VALUES ('blue', $1, $2), ('red', $1, $2)
ON CONFLICT (team) DO UPDATE
SET actions_remaining = $1,
quota_reset_at = $2`,
[cfg.teamActionQuota, nextNoon]
);
await resetAllUserActions(cfg.dailyActionQuota, nextNoon);
console.log(`[world] Slot ${lastSeedSlot}${seedSlot}; grid wiped, old cooldowns cleared.`);
lastSeedSlot = seedSlot;
}
@@ -170,17 +210,52 @@ export async function getGridCells(worldSeed) {
return rows;
}
export async function insertCell(seed, x, y, exploitable, hasPlanet, planetJson, team) {
export async function insertCell(seed, x, y, exploitable, hasPlanet, planetJson) {
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)
VALUES ($1, $2, $3, $4, $5, $6, NULL)
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]
[seed, x, y, exploitable, hasPlanet, planetJson]
);
return rows[0] ?? null;
}
// ── Team cell visibility ──────────────────────────────────────────────────────
/** Returns true if team has already revealed this cell. */
export async function checkTeamVisibility(worldSeed, team, x, y) {
const { rows } = await pool.query(
`SELECT 1 FROM team_cell_visibility WHERE world_seed = $1 AND team = $2 AND x = $3 AND y = $4`,
[worldSeed, team, x, y]
);
return rows.length > 0;
}
/** Inserts a visibility record. Returns true if newly inserted (first reveal). */
export async function insertTeamVisibility(worldSeed, team, x, y) {
const { rowCount } = await pool.query(
`INSERT INTO team_cell_visibility (world_seed, team, x, y)
VALUES ($1, $2, $3, $4)
ON CONFLICT (world_seed, team, x, y) DO NOTHING`,
[worldSeed, team, x, y]
);
return rowCount > 0;
}
/** Returns all grid cells that are visible to a given team. */
export async function getTeamVisibleCells(worldSeed, team) {
const { rows } = await pool.query(
`SELECT gc.x, gc.y, gc.exploitable, gc.has_planet, gc.planet_json, gc.discovered_by
FROM grid_cells gc
JOIN team_cell_visibility tcv
ON tcv.world_seed = gc.world_seed AND tcv.x = gc.x AND tcv.y = gc.y
WHERE gc.world_seed = $1 AND tcv.team = $2`,
[worldSeed, team]
);
return rows;
}
export async function getExistingCell(seed, x, y) {
const { rows } = await pool.query(
`SELECT x, y, exploitable, has_planet, planet_json, discovered_by
@@ -296,7 +371,7 @@ export async function getVictoryPoints() {
export async function getScores(worldSeed) {
const { rows } = await pool.query(
`SELECT discovered_by, COUNT(*) AS cnt
FROM grid_cells WHERE world_seed = $1
FROM grid_cells WHERE world_seed = $1 AND discovered_by IS NOT NULL
GROUP BY discovered_by`,
[worldSeed]
);
@@ -398,3 +473,18 @@ export async function decrementTeamActions(team) {
);
return rows[0] ?? null;
}
/**
* Atomically decrements team actions_remaining by `amount` if sufficient.
* Returns the updated row, or null if not enough actions.
*/
export async function decrementTeamActionsBy(team, amount) {
const { rows } = await pool.query(
`UPDATE team_action_quota
SET actions_remaining = actions_remaining - $2
WHERE team = $1 AND actions_remaining >= $2
RETURNING actions_remaining`,
[team, amount]
);
return rows[0] ?? null;
}

View File

@@ -111,4 +111,19 @@ export async function decrementUserActions(userId) {
[userId]
);
return rows[0] ?? null;
}
/**
* Resets ALL users' quota to a given value (used on world-seed wipeout).
* Users who have no row yet get one inserted.
*/
export async function resetAllUserActions(actionsRemaining, quotaResetAt) {
await usersPool.query(
`INSERT INTO user_action_quota (user_id, actions_remaining, quota_reset_at)
SELECT id, $1, $2 FROM users
ON CONFLICT (user_id) DO UPDATE
SET actions_remaining = $1,
quota_reset_at = $2`,
[actionsRemaining, quotaResetAt]
);
}

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