// App shell: layers in z order - scene canvas (1), halftone (2), CRT (5),
// HUD + touch overlay (9-12), BIOS readout (20), title menu (30). Also owns
// the pre-game device detection (gamepad, touch) and the title -> enter
// flow. The title menu carries its own loading gate (single central
// spinner), so no separate preboot veil.
import { useEffect } from "react";
import GameCanvas from "./scene/GameCanvas.jsx";
import { Halftone, CrtOverlay } from "./ui/Overlays.jsx";
import Hud from "./ui/Hud.jsx";
import TouchOverlay from "./ui/TouchOverlay.jsx";
import BiosOverlay from "./ui/BiosOverlay.jsx";
import TitleMenu from "./ui/TitleMenu.jsx";
import ReachyControls from "./ui/ReachyControls.jsx";
import MicroduckControls from "./ui/MicroduckControls.jsx";
import EmotionBar from "./ui/EmotionBar.jsx";
import { useGame } from "./store.js";
export default function App() {
useEffect(() => {
const params = new URLSearchParams(location.search);
// Gamepad presence for the tutorial variant, before the game controller
// takes over polling (the game loop keeps mirroring it afterwards).
const pollPad = () => {
const gp = [...(navigator.getGamepads?.() ?? [])].find((p) => p && p.connected);
useGame.setState({ padConnected: !!gp });
};
window.addEventListener("gamepadconnected", pollPad);
window.addEventListener("gamepaddisconnected", pollPad);
pollPad();
// Touch detection, latching: (pointer: coarse) OR any real touch proves
// the device (DevTools device modes and hybrid laptops often fail the
// media query). __microduckTouched hands the proof to controls/touch.js
// which boots later. ?touch=1 forces the thumbs on any device.
const coarse = matchMedia("(pointer: coarse)");
const armTouch = () => {
window.__microduckTouched = true;
useGame.setState({ touchMode: true });
};
const onCoarse = () => coarse.matches && armTouch();
if (coarse.matches) armTouch();
else coarse.addEventListener("change", onCoarse);
window.addEventListener("touchstart", armTouch, { once: true, passive: true });
if (params.get("touch") === "1") armTouch();
// The title menu opens right away and holds its own loading gate
// (central spinner) until logo, fonts, duck stage and game boot are
// in. ?boot=1 skips the title entirely - testing hook.
if (params.get("boot") === "1") {
useGame.setState({ prebootDone: true, entered: true, menuOpen: false });
} else {
useGame.setState({ prebootDone: true, menuOpen: true });
}
return () => {
window.removeEventListener("gamepadconnected", pollPad);
window.removeEventListener("gamepaddisconnected", pollPad);
coarse.removeEventListener("change", onCoarse);
window.removeEventListener("touchstart", armTouch);
};
}, []);
return (
<>
>
);
}