/** * @file PetSprite.tsx * @description The mascot wearing a custom pet (`/api/pets`). A Codex pet is * a 1536×1872 spritesheet — 8 columns × 9 rows of 192×208 frames, one row per * animation state (idle, run right, run left, waving, jumping, failed, * waiting, running, review) — played one row at a time by Sagi's mood; an * uploaded image is shown still and bobs with the same `data-mood` CSS as * Sagi. Reduced motion holds the row's first frame. A sprite that fails to * load (deleted from another browser) calls `onBroken`, so the mascot falls * back to Sagi instead of an empty box. * @author Nguyễn Ngọc Trí Vĩ */ import { useEffect, useState } from "react"; import { api } from "../../lib/api"; import type { Mood } from "./brain"; const CELL = { w: 192, h: 208 }; const SHEET = { cols: 8, rows: 9 }; /** Frames in each row of the Codex atlas. */ const FRAMES = [6, 8, 8, 4, 5, 8, 6, 6, 6]; /** Which row plays for each mood. */ const ROW: Record = { idle: 0, sleeping: 0, thinking: 7, // running — an agent is working watching: 8, // review happy: 4, // jumping stuck: 6, // waiting — for the user worried: 5, // failed disconnected: 5, }; export function PetSprite({ id, kind, mood, size, reducedMotion, onBroken, }: { id: string; kind: "codex" | "image"; mood: Mood; size: number; reducedMotion: boolean; onBroken: () => void; }) { const url = api.pets.spriteUrl(id); const row = ROW[mood] ?? 0; const [frame, setFrame] = useState(0); useEffect(() => { setFrame(0); if (kind !== "codex" || reducedMotion) return; const timer = window.setInterval( () => setFrame((f) => (f + 1) % (FRAMES[row] ?? 1)), mood === "sleeping" ? 400 : 125 ); return () => window.clearInterval(timer); }, [kind, row, mood, reducedMotion]); // A background image reports no error of its own: probe it once. useEffect(() => { if (kind !== "codex") return; const probe = new Image(); probe.onerror = onBroken; probe.src = url; return () => { probe.onerror = null; }; // eslint-disable-next-line react-hooks/exhaustive-deps }, [url, kind]); if (kind === "image") { return ( ); } const scale = size / CELL.h; return (
); }