Spaces:
Paused
Paused
Download src/web/js/app.js from deepondas/laya-snake: direct link, hf CLI and curl.
- Browser
- Download file 6.25 kB
-
https://huggingface.co/spaces/deepondas/laya-snake/resolve/main/src/web/js/app.js
- Command line
-
hf download hf://spaces/deepondas/laya-snake/src/web/js/app.js
-
curl -L -o app.js https://huggingface.co/spaces/deepondas/laya-snake/resolve/main/src/web/js/app.js
6.25 kB
| import { game, applyState, subscribe } from "./gameState.js"; | |
| import { trackMotion } from "./snakeMotion.js"; | |
| import { renderDashboard, drawCharts, setText } from "./dashboard.js"; | |
| import { CanvasRenderer } from "./renderer2d.js"; | |
| const $ = (id) => document.getElementById(id); | |
| const VIEWS = ["2d", "3d", "cinematic"]; | |
| /* ---------- viewer preferences (this browser only) ---------- */ | |
| const prefs = { view: "3d", tod: "golden", grid: true, follow: false }; | |
| try { Object.assign(prefs, JSON.parse(localStorage.getItem("laya-snake-view") || "{}")); } catch (_) {} | |
| if (!VIEWS.includes(prefs.view)) prefs.view = "3d"; | |
| const debug = new URLSearchParams(location.search).has("debug"); | |
| // Saves the viewer's scene preferences and applies them. | |
| function savePrefs() { | |
| try { localStorage.setItem("laya-snake-view", JSON.stringify(prefs)); } catch (_) {} | |
| showPrefs(); | |
| selectRenderer(); | |
| } | |
| // Reflects the local scene preferences in the settings panel. | |
| function showPrefs() { | |
| [...$("view-seg").children].forEach((b) => b.classList.toggle("on", b.dataset.v === prefs.view)); | |
| $("tod-sel").value = prefs.tod; | |
| $("sw-grid").classList.toggle("on", prefs.grid); | |
| $("sw-follow").classList.toggle("on", prefs.follow); | |
| setText("hdr-view", { "2d": "2D view", "3d": "3D view", cinematic: "Realistic view" }[prefs.view]); | |
| } | |
| /* ---------- commands (unchanged server API) ---------- */ | |
| // Sends one control command to the game server and applies the state it returns. | |
| async function cmd(name) { | |
| if (game.state && game.state.view_only) return; | |
| try { | |
| const r = await fetch("/api/cmd/" + name, { method: "POST" }); | |
| if (r.ok) applyState(await r.json()); | |
| } catch (_) {} | |
| } | |
| $("btn-pause").onclick = () => cmd("pause"); | |
| $("btn-reset").onclick = () => cmd("reset"); | |
| $("sw-shield").onclick = () => cmd("shield"); | |
| $("sw-auto").onclick = () => cmd("auto"); | |
| $("sw-obst").onclick = () => cmd("obstacles"); | |
| $("food-dec").onclick = () => game.state && cmd("foods/" + (game.state.rules.foods - 1)); | |
| $("food-inc").onclick = () => game.state && cmd("foods/" + (game.state.rules.foods + 1)); | |
| $("speed").oninput = (e) => cmd("speed/" + e.target.value); | |
| $("size-sel").onchange = (e) => cmd("size/" + e.target.value); | |
| $("tod-sel").onchange = (e) => { prefs.tod = e.target.value; savePrefs(); }; | |
| $("sw-grid").onclick = () => { prefs.grid = !prefs.grid; savePrefs(); }; | |
| $("sw-follow").onclick = () => { prefs.follow = !prefs.follow; savePrefs(); }; | |
| [...$("view-seg").children].forEach((b) => b.onclick = () => { prefs.view = b.dataset.v; savePrefs(); }); | |
| document.addEventListener("keydown", (e) => { | |
| if (e.metaKey || e.ctrlKey || e.altKey || /SELECT|INPUT/.test(e.target.tagName)) return; | |
| const k = e.key.toLowerCase(); | |
| const local = { | |
| v: () => { prefs.view = VIEWS[(VIEWS.indexOf(prefs.view) + 1) % VIEWS.length]; }, | |
| g: () => { prefs.grid = !prefs.grid; }, | |
| f: () => { prefs.follow = !prefs.follow; }, | |
| }; | |
| if (local[k]) { e.preventDefault(); local[k](); savePrefs(); return; } | |
| const map = { " ": "pause", r: "reset", s: "shield", a: "auto", o: "obstacles", | |
| arrowup: "faster", arrowdown: "slower", "+": "grow", "=": "grow", "-": "shrink", "_": "shrink" }; | |
| if (map[k]) { e.preventDefault(); cmd(map[k]); } | |
| }); | |
| const navLinks = [...document.querySelectorAll(".nav a")]; | |
| const spy = new IntersectionObserver((entries) => { | |
| entries.forEach((en) => { | |
| if (en.isIntersecting) navLinks.forEach((a) => a.classList.toggle("on", a.getAttribute("href") === "#" + en.target.id)); | |
| }); | |
| }, { rootMargin: "-30% 0px -60% 0px" }); | |
| ["game", "stats", "model", "charts", "history"].forEach((id) => spy.observe($(id))); | |
| /* ---------- renderers ---------- */ | |
| const canvas2d = new CanvasRenderer($("stage"), prefs); | |
| let three = null, threeState = "idle"; | |
| let current = null; | |
| // Loads the Three.js world the first time a 3D view is chosen; afterwards it is reused as is. | |
| function ensureThree() { | |
| if (threeState !== "idle") return; | |
| threeState = "loading"; | |
| $("load3d").classList.add("show"); | |
| import("./renderer3d.js") | |
| .then(async (m) => { | |
| three = new m.ThreeRenderer($("stage3d"), prefs); | |
| if (debug) window.__laya3d = three; | |
| if (game.state) three.update(game.state); | |
| await three.init(); | |
| if (debug) { $("debug3d").classList.add("show"); await three.enableDebug($("debug3d")); } | |
| threeState = "ready"; | |
| }) | |
| .catch((err) => { | |
| console.error("[3d] could not start the 3D view:", err); | |
| threeState = "failed"; | |
| $("load3d").textContent = "3D view unavailable (WebGL). Showing 2D."; | |
| setTimeout(() => $("load3d").classList.remove("show"), 4000); | |
| }) | |
| .finally(() => { if (threeState === "ready") $("load3d").classList.remove("show"); selectRenderer(); }); | |
| } | |
| // Picks the renderer for the current view; the game itself is never touched by a switch. | |
| function selectRenderer() { | |
| const wants3d = prefs.view !== "2d"; | |
| if (wants3d) ensureThree(); | |
| const next = wants3d && threeState === "ready" ? three : canvas2d; | |
| if (next === three) three.setMode(prefs.view); | |
| $("stage-box").dataset.mode = next === three ? prefs.view : "2d"; | |
| if (next !== current) { | |
| if (current) current.deactivate(); | |
| current = next; | |
| current.resize(); | |
| } | |
| } | |
| new ResizeObserver(() => { canvas2d.resize(); if (three) three.resize(); }).observe($("stage-box")); | |
| window.addEventListener("resize", () => { canvas2d.resize(); drawCharts(); }); | |
| /* ---------- state flow: server update -> state -> dashboard + both renderers ---------- */ | |
| subscribe((s, prev) => { | |
| trackMotion(prev, s); | |
| canvas2d.update(s); | |
| if (three) three.update(s); | |
| renderDashboard(s); | |
| }); | |
| // One animation loop for the whole page; only the visible renderer draws. | |
| function loop(now) { | |
| requestAnimationFrame(loop); | |
| if (game.state && current) current.render(now); | |
| } | |
| // Subscribes to the server's state stream (Server-Sent Events, unchanged). | |
| function connect() { | |
| const es = new EventSource("/api/events"); | |
| es.onmessage = (e) => applyState(JSON.parse(e.data)); | |
| es.onerror = () => { | |
| $("status").className = "pill down"; | |
| setText("status-text", "DISCONNECTED"); | |
| }; | |
| } | |
| showPrefs(); | |
| selectRenderer(); | |
| fetch("/api/state").then((r) => r.json()).then(applyState).catch(() => {}); | |
| connect(); | |
| requestAnimationFrame(loop); | |