**Motivations:** - Ensure lint config is not degraded and fix all lint errors for pousse workflow. **Root causes:** - Unused variables kept with _ prefix instead of removed (_row, _questReward, _i). - getAnimalBlockOrigin had 5 parameters (max 4). - use of continue statement (no-continue rule). **Correctifs:** - ESLint config verified; no eslint-disable in codebase. - Removed unused variable _row (biome-rules); removed dead function _questReward (quests); removed unused map param _i (state.js). - getAnimalBlockOrigin refactored to 4 params (pos object instead of x, y). - Replaced continue with if (cell) block in normalizeLoadedCells (state.js). - JSDoc param names aligned with _height, _y (biome-rules). **Evolutions:** - (none) **Pages affectées:** - web/js/biome-rules.js - web/js/quests.js - web/js/state.js - web/js/placement.js
202 lines
6.7 KiB
JavaScript
202 lines
6.7 KiB
JavaScript
import { defaultState, saveState } from "./state.js";
|
|
import { refreshOffers } from "./conveyor.js";
|
|
import { render } from "./ui.js";
|
|
import { startGameLoop } from "./game-loop.js";
|
|
import { playSound, setMusicEnabled, setMusicGetState } from "./audio.js";
|
|
import { resolveIncident, INCIDENT_EMOJI } from "./visitor-incidents.js";
|
|
import { getAttractionCenter, getVisitorPosition } from "./visitor-attraction.js";
|
|
import { incidentLabel, incidentBubbleAria } from "./texts-fr.js";
|
|
import { getApiBase, loadZoos, saveMyZoo, setApiBaseUrl } from "./api-client.js";
|
|
import { bootstrapFromApi, applyWorldZoos } from "./main-bootstrap.js";
|
|
|
|
const root = document.getElementById("root");
|
|
if (!root) throw new Error("Missing #root");
|
|
|
|
let state = null;
|
|
let myZooId = null;
|
|
|
|
function setMyZooId(id) {
|
|
myZooId = id;
|
|
}
|
|
|
|
async function runBootNoBase(rootEl) {
|
|
rootEl.innerHTML = "<div class=\"boot-panel\"><h1>Construis un zoo</h1>" +
|
|
"<p>Connectez-vous à un serveur pour jouer (compte et sauvegarde en base).</p>" +
|
|
"<div style=\"margin-top: 1rem;\"><label for=\"boot-api-url\">URL du serveur</label><input id=\"boot-api-url\" type=\"text\" placeholder=\"https://...\" style=\"display:block;margin-top:4px;width:100%;\" /></div>" +
|
|
"<button id=\"boot-connect\" type=\"button\" style=\"margin-top: 8px;\">Se connecter</button>" +
|
|
"<p id=\"boot-err\" class=\"boot-err\"></p></div>";
|
|
const urlInput = document.getElementById("boot-api-url");
|
|
try {
|
|
const stored = localStorage.getItem("builazoo_api_url");
|
|
if (stored) urlInput.value = stored;
|
|
} catch (_) {
|
|
// ignore localStorage
|
|
}
|
|
const errEl = document.getElementById("boot-err");
|
|
await new Promise((resolve) => {
|
|
document.getElementById("boot-connect").addEventListener("click", () => {
|
|
const url = urlInput.value.trim();
|
|
if (!url) {
|
|
errEl.textContent = "Indiquez l'URL du serveur.";
|
|
return;
|
|
}
|
|
setApiBaseUrl(url);
|
|
resolve();
|
|
});
|
|
});
|
|
rootEl.innerHTML = "";
|
|
}
|
|
|
|
async function runBootWithBase(rootEl) {
|
|
rootEl.innerHTML = "<div class=\"boot-panel\"><p>Chargement…</p></div>";
|
|
while (true) {
|
|
try {
|
|
state = await bootstrapFromApi(setMyZooId, rootEl);
|
|
break;
|
|
} catch (e) {
|
|
console.error("bootstrapFromApi failed", e);
|
|
rootEl.innerHTML = "<div class=\"boot-panel\"><h1>Construis un zoo</h1><p class=\"boot-err\">Erreur de connexion au serveur.</p><button id=\"boot-retry\" type=\"button\">Réessayer</button></div>";
|
|
const errP = rootEl.querySelector(".boot-err");
|
|
if (errP && e && e.message) errP.textContent = e.message;
|
|
await new Promise((resolve) => {
|
|
document.getElementById("boot-retry").addEventListener("click", () => resolve());
|
|
});
|
|
}
|
|
}
|
|
rootEl.innerHTML = "";
|
|
}
|
|
|
|
(async () => {
|
|
let base = getApiBase();
|
|
if (!base) {
|
|
await runBootNoBase(root);
|
|
base = getApiBase();
|
|
}
|
|
if (base) {
|
|
await runBootWithBase(root);
|
|
}
|
|
if (state) {
|
|
|
|
try {
|
|
if (localStorage.getItem("builazoo_music") === "1") setMusicEnabled(true);
|
|
} catch (_) {
|
|
// ignore localStorage
|
|
}
|
|
|
|
let lastHatched = [];
|
|
let fullRender = () => {};
|
|
function getState() {
|
|
return state;
|
|
}
|
|
function doRestart() {
|
|
state = defaultState();
|
|
const nowUnix = Math.floor(Date.now() / 1000);
|
|
refreshOffers(state, nowUnix);
|
|
saveState(state);
|
|
fullRender();
|
|
}
|
|
fullRender = render(root, {
|
|
state: getState(),
|
|
setState: () => fullRender(),
|
|
getLastHatched: () => lastHatched,
|
|
onRestart: doRestart,
|
|
updateState: (partial) => {
|
|
Object.assign(state, partial);
|
|
fullRender();
|
|
},
|
|
});
|
|
|
|
let lastApiSaveAt = 0;
|
|
const MIN_API_SAVE_INTERVAL_MS = 5000;
|
|
function saveStateFn(s) {
|
|
saveState(s);
|
|
if (getApiBase()) {
|
|
const now = Date.now();
|
|
if (now - lastApiSaveAt >= MIN_API_SAVE_INTERVAL_MS) {
|
|
lastApiSaveAt = now;
|
|
saveMyZoo(s).catch((e) => console.warn("saveMyZoo failed", e));
|
|
}
|
|
}
|
|
}
|
|
|
|
setMusicGetState(getState);
|
|
startGameLoop(getState, (s, payload) => {
|
|
if (payload?.lastHatched?.length) {
|
|
lastHatched = payload.lastHatched;
|
|
playSound("hatch");
|
|
}
|
|
fullRender();
|
|
setTimeout(() => { lastHatched = []; fullRender(); }, 1800);
|
|
}, saveStateFn);
|
|
|
|
const ZOOS_REFETCH_INTERVAL_MS = 30 * 1000;
|
|
setInterval(() => {
|
|
loadZoos().then((zoosData) => {
|
|
applyWorldZoos(state, { zoosData, playerZooId: state.myZooId ?? myZooId, playerName: state.playerName ?? "Mon zoo", playerX: state.playerX ?? 25, playerY: state.playerY ?? 50 });
|
|
fullRender();
|
|
}).catch(() => {});
|
|
}, ZOOS_REFETCH_INTERVAL_MS);
|
|
|
|
let visitorAnimTime = 0;
|
|
function syncVisitorBubble(el, visitor, index) {
|
|
const incidentType = visitor && (visitor.incidentType === null || visitor.incidentType === undefined)
|
|
? null
|
|
: (visitor && visitor.incidentType);
|
|
let bubble = el.querySelector(".visitor-incident-bubble");
|
|
if (incidentType) {
|
|
if (!bubble) {
|
|
bubble = document.createElement("span");
|
|
bubble.className = "visitor-incident-bubble";
|
|
bubble.setAttribute("role", "button");
|
|
bubble.setAttribute("tabindex", "0");
|
|
bubble.setAttribute("aria-label", incidentBubbleAria);
|
|
bubble.addEventListener("click", () => {
|
|
if (resolveIncident(getState(), index)) fullRender();
|
|
});
|
|
el.appendChild(bubble);
|
|
}
|
|
bubble.textContent = INCIDENT_EMOJI[incidentType] ?? "❓";
|
|
bubble.title = incidentLabel[incidentType] ?? incidentType;
|
|
} else if (bubble) {
|
|
bubble.remove();
|
|
}
|
|
}
|
|
function updateVisitors() {
|
|
const layer = root.querySelector(".visitors-layer");
|
|
if (!layer) {
|
|
requestAnimationFrame(updateVisitors);
|
|
return;
|
|
}
|
|
const currentState = getState();
|
|
const arrivals = currentState.visitorArrivals ?? [];
|
|
const n = arrivals.length;
|
|
const w = currentState.grid.width;
|
|
const h = currentState.grid.height;
|
|
while (layer.children.length < n) {
|
|
const el = document.createElement("div");
|
|
el.className = "visitor-sprite";
|
|
el.setAttribute("aria-hidden", "true");
|
|
el.textContent = "👤";
|
|
layer.appendChild(el);
|
|
}
|
|
while (layer.children.length > n) {
|
|
const last = layer.lastChild;
|
|
if (last) last.remove();
|
|
}
|
|
const { centerX, centerY } = getAttractionCenter(currentState, w, h);
|
|
for (let i = 0; i < n; i++) {
|
|
const el = layer.children[i];
|
|
const { px, py } = getVisitorPosition({
|
|
i, n, t: visitorAnimTime, centerX, centerY, gridWidth: w, gridHeight: h,
|
|
});
|
|
el.style.left = `${px}px`;
|
|
el.style.top = `${py}px`;
|
|
syncVisitorBubble(el, arrivals[i], i);
|
|
}
|
|
visitorAnimTime += 0.016;
|
|
requestAnimationFrame(updateVisitors);
|
|
}
|
|
requestAnimationFrame(updateVisitors);
|
|
}
|
|
})();
|