Initial commit
**Motivations:** - Initialisation du versionning git pour le projet **Root causes:** - N/A (Nouveau projet) **Correctifs:** - N/A **Evolutions:** - Structure initiale du projet - Ajout du .gitignore **Pages affectées:** - Tous les fichiers
This commit is contained in:
193
web/js/main.js
Normal file
193
web/js/main.js
Normal file
@@ -0,0 +1,193 @@
|
||||
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 () => {
|
||||
let base = getApiBase();
|
||||
if (!base) {
|
||||
root.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();
|
||||
});
|
||||
});
|
||||
root.innerHTML = "";
|
||||
base = getApiBase();
|
||||
}
|
||||
if (base) {
|
||||
root.innerHTML = "<div class=\"boot-panel\"><p>Chargement…</p></div>";
|
||||
while (true) {
|
||||
try {
|
||||
state = await bootstrapFromApi(setMyZooId, root);
|
||||
break;
|
||||
} catch (e) {
|
||||
console.error("bootstrapFromApi failed", e);
|
||||
root.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 = root.querySelector(".boot-err");
|
||||
if (errP && e && e.message) errP.textContent = e.message;
|
||||
await new Promise((resolve) => {
|
||||
document.getElementById("boot-retry").addEventListener("click", () => resolve());
|
||||
});
|
||||
}
|
||||
}
|
||||
root.innerHTML = "";
|
||||
}
|
||||
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);
|
||||
}
|
||||
})();
|
||||
Reference in New Issue
Block a user