update: debug overlay layout controls
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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 />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user