// Title / pause overlay: a full-page ink screen printed in the landing's // comic DA - big Anton title with the ink-drop + cyan/magenta aberration // treatment, a comic-block CTA on an acid plate, and an orange halftone // ramp pooling in the top-left corner. Colour and locomotion live in the // in-game HUD quickbar, so the intro's only action is Waddle in. // // The first "Waddle in" cues the BIOS; later Esc / the in-game Back button // reopen the overlay as pause ("Resume"). The ?boot=1 bypass lives in // App.jsx and never mounts this component. import { useEffect, useRef, useState } from "react"; import Box from "@mui/material/Box"; import Typography from "@mui/material/Typography"; import { keyframes, styled } from "@mui/material/styles"; import { useGame } from "../store.js"; import { signed } from "../game/signed.js"; import { INK, ORANGE, MONO } from "../theme.js"; import { ComicButton, ComicTitle, HalftoneRamp, ANTON } from "./comic.jsx"; const rowIn = keyframes` from { transform: translateY(12px); opacity: 0; } to { transform: none; opacity: 1; } `; const brandIn = keyframes` from { transform: translateY(10px) scale(0.94); opacity: 0; } to { transform: none; opacity: 1; } `; // One soft ease, small travel, ~80 ms between rows - staged, not showy. const row = (delay, name = rowIn) => ({ animation: `${name} 0.55s cubic-bezier(0.22, 1, 0.36, 1) both`, animationDelay: `${delay}s`, "@media (prefers-reduced-motion: reduce)": { animation: "none" }, }); const Kbd = styled("kbd")(({ round }) => ({ display: "inline-flex", alignItems: "center", justifyContent: "center", minWidth: round ? "1.9rem" : "1.65rem", height: "1.65rem", padding: "0 0.45rem", font: "inherit", fontSize: "0.68rem", fontWeight: 600, color: "#fff", background: "#14141c", border: `2px solid ${INK}`, borderRadius: round ? "50%" : 8, boxShadow: "0 0 0 2px rgba(255, 255, 255, 0.82)", })); const TILES = { kb: [ { caps: "cluster-arrows", name: "Move", hint: "arrows or ZQSD" }, { caps: ["A", "E"], name: "Kick", hint: "left / right" }, { caps: ["R"], name: "Sit", hint: "tap again to stand" }, { caps: ["G"], name: "Pick up", hint: "beak to the ground" }, { caps: ["C"], name: "Camera", hint: "toggle chase" }, { caps: ["Space"], name: "Reset", hint: "fresh start" }, ], pad: [ { caps: ["LS"], name: "Move", hint: "left stick" }, { caps: ["LB", "RB"], name: "Kick", hint: "left / right" }, { caps: ["Y"], name: "Head", hint: "sticks move the head" }, { caps: ["RS"], name: "Camera", hint: "orbit" }, { caps: ["X", "\u2193"], name: "Sit", hint: "tap again to stand" }, ], touch: [ { caps: ["Stick"], name: "Move", hint: "left thumb" }, { caps: ["A"], round: true, name: "Kick", hint: "tap it" }, { caps: ["B"], round: true, name: "Quack", hint: "hold for beak" }, ], }; const HINTS = { kb: "drag to orbit \u00b7 scroll to zoom", pad: "A ground pick \u00b7 RT quack \u00b7 hold LT wheee \u00b7 R3 chase", touch: "drag to orbit \u00b7 pinch to zoom", }; function closeMenu() { useGame.setState({ menuOpen: false }); if (!useGame.getState().entered) useGame.setState({ entered: true }); } export default function TitleMenu() { const menuOpen = useGame((s) => s.menuOpen); const entered = useGame((s) => s.entered); const padConnected = useGame((s) => s.padConnected); const touchMode = useGame((s) => s.touchMode); const [closing, setClosing] = useState(false); const prevOpen = useRef(menuOpen); // Keep the overlay mounted through the 0.35 s closing fade. useEffect(() => { const was = prevOpen.current; prevOpen.current = menuOpen; if (was && !menuOpen) { setClosing(true); const t = setTimeout(() => setClosing(false), 380); return () => clearTimeout(t); } }, [menuOpen]); // Enter enters / resumes; Esc toggles the pause menu (never over the // BIOS readout). useEffect(() => { const onKey = (e) => { const s = useGame.getState(); if (e.code === "Enter" && s.menuOpen) { if (e.target instanceof HTMLButtonElement && e.target.dataset.cta !== "1") return; e.preventDefault(); closeMenu(); return; } if (e.code !== "Escape") return; if (s.biosVisible) return; if (s.menuOpen) { if (s.entered) closeMenu(); return; } if (s.entered) useGame.setState({ menuOpen: true }); }; window.addEventListener("keydown", onKey); return () => window.removeEventListener("keydown", onKey); }, []); if (!menuOpen && !closing) return null; // A plugged-in gamepad wins over the touch tutorial. const tutorialVariant = padConnected ? "pad" : touchMode ? "touch" : "kb"; const tiles = TILES[tutorialVariant]; const ctaLabel = entered ? "Resume" : "Waddle in"; return ( {/* Theme-orange halftone pooling in the top-left corner, dots growing toward it - the landing's screen-tone on ink ground. */} {/* Vitrine brand lockup: drawn duck head + name. Hovering swaps to the open-beak frame, same hard sprite swap as the vitrine. */} {/* The open frame's canvas (460x333) is a hair wider/taller than the closed one (454x269); offsets pin the head while the jaw hangs below. */} {/* The landing hero's mistracked-VHS treatment: orange fill with ink drop + chroma ghosts, hollow echo line below. */} The exact same trained policies that drive the real robot, live in your browser. {tiles.map((t) => ( {t.caps === "cluster-arrows" ? ( {"\u2191"} {"\u2190"} {"\u2193"} {"\u2192"} ) : ( {t.caps.map((c) => ( {c} ))} )} {t.name} {t.hint} ))} {HINTS[tutorialVariant]} {ctaLabel} ); }