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 = "

Construis un zoo

" + "

Connectez-vous à un serveur pour jouer (compte et sauvegarde en base).

" + "
" + "" + "

"; 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 = "

Chargement…

"; while (true) { try { state = await bootstrapFromApi(setMyZooId, rootEl); break; } catch (e) { console.error("bootstrapFromApi failed", e); rootEl.innerHTML = "

Construis un zoo

Erreur de connexion au serveur.

"; 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); } })();