|
|
|
|
@@ -1,6 +1,6 @@
|
|
|
|
|
import { fnv1a32, hash2u32, mulberry32 } from "./rng.js";
|
|
|
|
|
import { formatPlanet, generatePlanet } from "./planetGeneration.js";
|
|
|
|
|
import { apiFetchConfig, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints, apiFetchActivePlayers, apiFetchMilitaryDeductions, apiMilitaryAttack, apiCaptureCell } from "./api.js";
|
|
|
|
|
import { apiFetchConfig, apiFetchGrid, apiRevealCell, apiFetchEconScores, apiTickEconScores, apiFetchElementBonus, apiTickElementBonus, apiFetchDbInfo, apiFetchVictoryPoints, apiFetchActivePlayers, apiFetchActivePlayerNames, apiFetchMilitaryDeductions, apiMilitaryAttack, apiCaptureCell } from "./api.js";
|
|
|
|
|
import { computeTeamIncome, computeTeamElementBonus, computeTeamElementBonusDetailed, renderResourceTable, renderElementBonusTable, setEconSort, getEconSort, setElemSort, getElemSort, computeTeamMilitaryDetailed, renderMilitaryTable, setMilSort, getMilSort } from "./economy.js";
|
|
|
|
|
|
|
|
|
|
// ── Constants ─────────────────────────────────────────────────────────────────
|
|
|
|
|
@@ -234,6 +234,9 @@ const captureModalBodyEl = document.getElementById("captureModalBody");
|
|
|
|
|
const captureModalYesEl = document.getElementById("captureModalYes");
|
|
|
|
|
const captureModalNoEl = document.getElementById("captureModalNo");
|
|
|
|
|
const teamQuotaEl = document.getElementById("teamActionsRemaining");
|
|
|
|
|
const captorInfoEl = document.getElementById("captorInfo");
|
|
|
|
|
const playerListPopupEl = document.getElementById("playerListPopup");
|
|
|
|
|
const playerListContentEl = document.getElementById("playerListContent");
|
|
|
|
|
// ── Cell helpers ──────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
export function cellKey(x, y) { return `${x},${y}`; }
|
|
|
|
|
@@ -325,6 +328,64 @@ export async function fetchAndApplyActivePlayers() {
|
|
|
|
|
} catch { /* ignore */ }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Player list popup (click on joueur count) ─────────────────────────────────
|
|
|
|
|
|
|
|
|
|
function closePlayerListPopup() {
|
|
|
|
|
if (playerListPopupEl) playerListPopupEl.classList.add("hidden");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function openPlayerListPopup(anchorEl, team) {
|
|
|
|
|
if (!playerListPopupEl || !playerListContentEl) return;
|
|
|
|
|
try {
|
|
|
|
|
const names = await apiFetchActivePlayerNames();
|
|
|
|
|
const list = names[team] ?? [];
|
|
|
|
|
if (!list.length) {
|
|
|
|
|
playerListContentEl.innerHTML = `<span class="playerListEmpty">Aucun joueur actif</span>`;
|
|
|
|
|
} else {
|
|
|
|
|
const teamClass = `playerListName--${team}`;
|
|
|
|
|
playerListContentEl.innerHTML =
|
|
|
|
|
list.map(u => `<div class="${escHtml(teamClass)}">${escHtml(u)}</div>`).join("");
|
|
|
|
|
}
|
|
|
|
|
// Position popup below the anchor
|
|
|
|
|
const rect = anchorEl.getBoundingClientRect();
|
|
|
|
|
const parentRect = anchorEl.closest(".infoColumn, aside")?.getBoundingClientRect() ?? { left: 0, top: 0 };
|
|
|
|
|
playerListPopupEl.style.left = `${rect.left - parentRect.left}px`;
|
|
|
|
|
playerListPopupEl.style.top = `${rect.bottom - parentRect.top + 4}px`;
|
|
|
|
|
playerListPopupEl.classList.remove("hidden");
|
|
|
|
|
playerListPopupEl.dataset.team = team;
|
|
|
|
|
} catch { /* ignore */ }
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (activeCountBlueEl) {
|
|
|
|
|
activeCountBlueEl.style.cursor = "pointer";
|
|
|
|
|
activeCountBlueEl.addEventListener("click", (ev) => {
|
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
if (playerListPopupEl && !playerListPopupEl.classList.contains("hidden") && playerListPopupEl.dataset.team === "blue") {
|
|
|
|
|
closePlayerListPopup();
|
|
|
|
|
} else {
|
|
|
|
|
openPlayerListPopup(activeCountBlueEl, "blue");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (activeCountRedEl) {
|
|
|
|
|
activeCountRedEl.style.cursor = "pointer";
|
|
|
|
|
activeCountRedEl.addEventListener("click", (ev) => {
|
|
|
|
|
ev.stopPropagation();
|
|
|
|
|
if (playerListPopupEl && !playerListPopupEl.classList.contains("hidden") && playerListPopupEl.dataset.team === "red") {
|
|
|
|
|
closePlayerListPopup();
|
|
|
|
|
} else {
|
|
|
|
|
openPlayerListPopup(activeCountRedEl, "red");
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
document.addEventListener("click", (ev) => {
|
|
|
|
|
if (playerListPopupEl && !playerListPopupEl.contains(ev.target)) {
|
|
|
|
|
closePlayerListPopup();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ── Element bonus ─────────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
let elemBonusBlue = 0;
|
|
|
|
|
@@ -534,6 +595,7 @@ export async function fetchGridForSeed(seed, depth = 0) {
|
|
|
|
|
controlledBy: row.discovered_by ?? row.discoveredBy ?? null,
|
|
|
|
|
hasPlanet: Boolean(row.has_planet),
|
|
|
|
|
planet: row.planet_json ?? null,
|
|
|
|
|
capturedBy: row.captured_by ?? row.capturedBy ?? null,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
@@ -696,6 +758,20 @@ function refreshCursorFromLast() {
|
|
|
|
|
|
|
|
|
|
// ── Selection display ─────────────────────────────────────────────────────────
|
|
|
|
|
|
|
|
|
|
/** Escapes a string for safe HTML insertion. */
|
|
|
|
|
function escHtml(str) {
|
|
|
|
|
return String(str).replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/** Shows or hides the captor info banner below planet stats. */
|
|
|
|
|
function showCaptorInfo(capturedBy, team) {
|
|
|
|
|
if (!captorInfoEl) return;
|
|
|
|
|
if (!capturedBy) { captorInfoEl.className = "captorInfo hidden"; return; }
|
|
|
|
|
const teamClass = team === "blue" ? "captorInfo--blue" : team === "red" ? "captorInfo--red" : "";
|
|
|
|
|
captorInfoEl.className = `captorInfo ${teamClass}`;
|
|
|
|
|
captorInfoEl.innerHTML = `Capturée par <strong>${escHtml(capturedBy)}</strong>`;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function applyRevealPayload(cell) {
|
|
|
|
|
const _revealKey = cellKey(cell.x, cell.y);
|
|
|
|
|
markTileReveal(_revealKey);
|
|
|
|
|
@@ -703,16 +779,19 @@ function applyRevealPayload(cell) {
|
|
|
|
|
controlledBy: cell.discoveredBy ?? null,
|
|
|
|
|
hasPlanet: Boolean(cell.hasPlanet),
|
|
|
|
|
planet: cell.planet ?? null,
|
|
|
|
|
capturedBy: cell.capturedBy ?? null,
|
|
|
|
|
});
|
|
|
|
|
details.classList.remove("details--hidden");
|
|
|
|
|
if (!cell.exploitable) {
|
|
|
|
|
hint.textContent = `(${cell.x},${cell.y}) Inexploitable`;
|
|
|
|
|
details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : Inexploitable`;
|
|
|
|
|
showCaptorInfo(null, null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!cell.hasPlanet) {
|
|
|
|
|
hint.textContent = `(${cell.x},${cell.y}) Vide`;
|
|
|
|
|
details.textContent = `Tuile (${cell.x},${cell.y})\n\nStatus : Vide`;
|
|
|
|
|
showCaptorInfo(null, null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const controlStatus = cell.discoveredBy === null
|
|
|
|
|
@@ -722,6 +801,7 @@ function applyRevealPayload(cell) {
|
|
|
|
|
: `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 : ${controlStatus}\n\n${formatPlanet(cell.planet)}`;
|
|
|
|
|
showCaptorInfo(cell.capturedBy ?? null, cell.discoveredBy);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function showLocalSelection(x, y) {
|
|
|
|
|
@@ -732,11 +812,13 @@ function showLocalSelection(x, y) {
|
|
|
|
|
if (!isExploitable(x, y)) {
|
|
|
|
|
hint.textContent = `(${x},${y}) Inexploitable`;
|
|
|
|
|
details.textContent = `Tuile (${x},${y})\n\nStatus : Inexploitable`;
|
|
|
|
|
showCaptorInfo(null, null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
if (!meta.hasPlanet) {
|
|
|
|
|
hint.textContent = `(${x},${y}) Vide`;
|
|
|
|
|
details.textContent = `Tuile (${x},${y})\n\nStatus : Vide`;
|
|
|
|
|
showCaptorInfo(null, null);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
let planet = meta.planet;
|
|
|
|
|
@@ -751,6 +833,7 @@ function showLocalSelection(x, y) {
|
|
|
|
|
: `Contrôlée par l'adversaire`;
|
|
|
|
|
hint.textContent = `(${x},${y}) Planète présente`;
|
|
|
|
|
details.textContent = `Tuile (${x},${y})\n\nStatus : ${controlStatus}\n\n${formatPlanet(planet)}`;
|
|
|
|
|
showCaptorInfo(meta.capturedBy ?? null, meta.controlledBy);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ── Military attack modal ────────────────────────────────────────────────────
|
|
|
|
|
@@ -896,7 +979,7 @@ async function onCanvasClick(ev) {
|
|
|
|
|
if (!res.ok) { hint.textContent = "Erreur lors de la capture."; return; }
|
|
|
|
|
const data = await res.json();
|
|
|
|
|
markTileReveal(key);
|
|
|
|
|
cells.set(key, { ...meta, controlledBy: currentTeam });
|
|
|
|
|
cells.set(key, { ...meta, controlledBy: currentTeam, capturedBy: data.cell?.capturedBy ?? null });
|
|
|
|
|
if (data.teamActionsRemaining !== undefined && data.teamActionsRemaining !== null) {
|
|
|
|
|
teamActionsRemaining = data.teamActionsRemaining;
|
|
|
|
|
updateTeamQuotaDisplay();
|
|
|
|
|
|