update: debug overlay layout controls

This commit is contained in:
Tom Boullay
2026-05-01 23:39:04 +02:00
parent 1a783f1867
commit eef39ab53d
20 changed files with 581 additions and 209 deletions
-75
View File
@@ -1,75 +0,0 @@
import { Debug } from "@/utils/debug/Debug";
import {
type MainGameState,
useGameStore,
} from "@/managers/stores/useGameStore";
const MAIN_STATES: MainGameState[] = [
"intro",
"bike",
"pylone",
"ferme",
"outro",
];
export function GameStateHUD(): React.JSX.Element | null {
const debug = Debug.getInstance();
const mainState = useGameStore((state) => state.mainState);
const detail = useGameStore((state) => {
switch (state.mainState) {
case "intro":
return state.intro.hasCompleted ? "completed" : "waiting";
case "bike":
return state.bike.currentStep;
case "pylone":
return state.pylone.currentStep;
case "ferme":
return state.ferme.currentStep;
case "outro":
return state.outro.hasStarted ? "started" : "waiting";
}
});
const setMainState = useGameStore((state) => state.setMainState);
const advanceGameState = useGameStore((state) => state.advanceGameState);
const resetGame = useGameStore((state) => state.resetGame);
if (!debug.active) return null;
return (
<aside className="game-state-hud" aria-label="Game state debug panel">
<div className="game-state-hud__header">
<span>Game State</span>
<strong>{mainState}</strong>
</div>
<p className="game-state-hud__detail">Sub state: {detail}</p>
<div
className="game-state-hud__states"
aria-label="Main states"
role="group"
>
{MAIN_STATES.map((state) => (
<button
key={state}
aria-pressed={state === mainState}
className={state === mainState ? "is-active" : undefined}
type="button"
onClick={() => setMainState(state)}
>
{state}
</button>
))}
</div>
<div className="game-state-hud__actions">
<button type="button" onClick={advanceGameState}>
Next step
</button>
<button type="button" onClick={resetGame}>
Reset
</button>
</div>
</aside>
);
}
+2 -4
View File
@@ -1,17 +1,15 @@
import { Crosshair } from "@/components/ui/Crosshair";
import { GameStateHUD } from "@/components/ui/GameStateHUD";
import { HandTrackingOverlay } from "@/components/ui/HandTrackingOverlay";
import { DebugOverlayLayout } from "@/components/ui/debug/DebugOverlayLayout";
import { HandTrackingVisualizer } from "@/components/ui/HandTrackingVisualizer";
import { InteractPrompt } from "@/components/ui/InteractPrompt";
export function GameUI(): React.JSX.Element {
return (
<>
<GameStateHUD />
<DebugOverlayLayout />
<Crosshair />
<InteractPrompt />
<HandTrackingVisualizer />
<HandTrackingOverlay />
</>
);
}
-38
View File
@@ -1,38 +0,0 @@
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
import type { HandTrackingStatus } from "@/types/handTracking/handTracking";
const STATUS_LABELS: Record<HandTrackingStatus, string> = {
idle: "Idle",
requesting_camera: "Requesting camera",
starting_camera: "Starting camera",
connecting_server: "Connecting server",
connecting: "Connecting",
connected: "Connected",
disconnected: "Disconnected",
error: "Error",
};
export function HandTrackingOverlay(): React.JSX.Element | null {
const { hands, status, usageStatus, serverStatus, error } =
useHandTrackingSnapshot();
if (status === "idle") {
return null;
}
const fist = hands.some((hand) => hand.isFist);
return (
<aside className="hand-tracking-overlay" aria-label="Hand tracking status">
<strong>Hand tracking</strong>
<span>Status: {STATUS_LABELS[status]}</span>
<span>Usage: {usageStatus}</span>
{serverStatus ? <span>Server: {serverStatus}</span> : null}
<span>Hands: {hands.length}</span>
<span>Fist: {fist ? "yes" : "no"}</span>
{error ? (
<span className="hand-tracking-overlay__error">{error}</span>
) : null}
</aside>
);
}
@@ -0,0 +1,22 @@
import { GameStateDebugPanel } from "@/components/ui/debug/GameStateDebugPanel";
import { HandTrackingDebugPanel } from "@/components/ui/debug/HandTrackingDebugPanel";
import { useShowDebugOverlay } from "@/hooks/debug/useShowDebugOverlay";
export function DebugOverlayLayout(): React.JSX.Element | null {
const showDebugOverlay = useShowDebugOverlay();
if (!showDebugOverlay) return null;
return (
<aside className="debug-overlay-layout" aria-label="Debug overlay panels">
<header className="debug-overlay-layout__header">
<span className="debug-overlay-layout__kicker">Debug overlay</span>
</header>
<div className="debug-overlay-layout__sections">
<HandTrackingDebugPanel />
<GameStateDebugPanel />
</div>
</aside>
);
}
@@ -0,0 +1,164 @@
import { RotateCcw, StepBack, StepForward } from "lucide-react";
import {
type MainGameState,
type MissionStep,
useGameStore,
} from "@/managers/stores/useGameStore";
const MAIN_STATES: MainGameState[] = [
"intro",
"bike",
"pylone",
"ferme",
"outro",
];
const MISSION_STEPS: MissionStep[] = [
"locked",
"waiting",
"inspected",
"fragmented",
"scanning",
"repairing",
"done",
];
function toPascalCase(value: string): string {
return value
.split(/[-_\s]+/)
.filter(Boolean)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join("");
}
export function GameStateDebugPanel(): React.JSX.Element {
const mainState = useGameStore((state) => state.mainState);
const detail = useGameStore((state) => {
switch (state.mainState) {
case "intro":
return state.intro.hasCompleted ? "completed" : "waiting";
case "bike":
return state.bike.currentStep;
case "pylone":
return state.pylone.currentStep;
case "ferme":
return state.ferme.currentStep;
case "outro":
return state.outro.hasStarted ? "started" : "waiting";
}
});
const setMainState = useGameStore((state) => state.setMainState);
const setIntroState = useGameStore((state) => state.setIntroState);
const setBikeState = useGameStore((state) => state.setBikeState);
const setPyloneState = useGameStore((state) => state.setPyloneState);
const setFermeState = useGameStore((state) => state.setFermeState);
const setOutroState = useGameStore((state) => state.setOutroState);
const advanceGameState = useGameStore((state) => state.advanceGameState);
const rewindGameState = useGameStore((state) => state.rewindGameState);
const resetGame = useGameStore((state) => state.resetGame);
const subStateOptions =
mainState === "intro"
? ["waiting", "completed"]
: mainState === "outro"
? ["waiting", "started"]
: MISSION_STEPS;
function setSubState(nextSubState: string): void {
if (mainState === "intro") {
setIntroState({ hasCompleted: nextSubState === "completed" });
return;
}
if (mainState === "bike") {
setBikeState({ currentStep: nextSubState as MissionStep });
return;
}
if (mainState === "pylone") {
setPyloneState({ currentStep: nextSubState as MissionStep });
return;
}
if (mainState === "ferme") {
setFermeState({ currentStep: nextSubState as MissionStep });
return;
}
setOutroState({ hasStarted: nextSubState === "started" });
}
return (
<section
className="game-state-debug-panel debug-overlay-section"
aria-label="Game state debug panel"
>
<div className="game-state-debug-panel__header">
<h3>Game State</h3>
</div>
<div className="game-state-debug-panel__switch-group">
<div className="game-state-debug-panel__switch-heading">
<span>Main state</span>
<strong>{toPascalCase(mainState)}</strong>
</div>
<div
className="game-state-debug-panel__states"
aria-label="Main states"
role="group"
>
{MAIN_STATES.map((state) => (
<button
key={state}
aria-pressed={state === mainState}
className={state === mainState ? "is-active" : undefined}
type="button"
onClick={() => setMainState(state)}
>
{toPascalCase(state)}
</button>
))}
</div>
</div>
<div className="game-state-debug-panel__switch-group">
<div className="game-state-debug-panel__switch-heading">
<span>Sub state</span>
<strong>{toPascalCase(detail)}</strong>
</div>
<div
className="game-state-debug-panel__states"
aria-label="Sub states"
role="group"
>
{subStateOptions.map((subState) => (
<button
key={subState}
aria-pressed={subState === detail}
className={subState === detail ? "is-active" : undefined}
type="button"
onClick={() => setSubState(subState)}
>
{toPascalCase(subState)}
</button>
))}
</div>
</div>
<div className="game-state-debug-panel__actions">
<button type="button" onClick={rewindGameState}>
<StepBack aria-hidden="true" size={14} />
Previous step
</button>
<button type="button" onClick={advanceGameState}>
<StepForward aria-hidden="true" size={14} />
Next step
</button>
<button type="button" onClick={resetGame}>
<RotateCcw aria-hidden="true" size={14} />
Reset
</button>
</div>
</section>
);
}
@@ -0,0 +1,65 @@
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
import type { HandTrackingStatus } from "@/types/handTracking/handTracking";
const STATUS_LABELS: Record<HandTrackingStatus, string> = {
idle: "Idle",
requesting_camera: "Requesting camera",
starting_camera: "Starting camera",
connecting_server: "Connecting server",
connecting: "Connecting",
connected: "Connected",
disconnected: "Disconnected",
error: "Error",
};
export function HandTrackingDebugPanel(): React.JSX.Element | null {
const { hands, status, usageStatus, serverStatus, error } =
useHandTrackingSnapshot();
if (status === "idle") {
return null;
}
const fist = hands.some((hand) => hand.isFist);
return (
<section
className="hand-tracking-debug-panel debug-overlay-section"
aria-label="Hand tracking status"
>
<div className="debug-overlay-section__heading">
<h3>Hand tracking</h3>
<span>{STATUS_LABELS[status]}</span>
</div>
<dl className="debug-overlay-metrics">
<div>
<dt>Usage</dt>
<dd>{usageStatus}</dd>
</div>
<div>
<dt>Model loaded</dt>
<dd>none</dd>
</div>
{serverStatus ? (
<div>
<dt>Server</dt>
<dd>{serverStatus}</dd>
</div>
) : null}
<div>
<dt>Hands</dt>
<dd>{hands.length}</dd>
</div>
<div>
<dt>Fist</dt>
<dd>{fist ? "yes" : "no"}</dd>
</div>
</dl>
{error ? (
<span className="hand-tracking-debug-panel__error">{error}</span>
) : null}
</section>
);
}