feat(tutorial): add movement and hand-tracking onboarding overlays
Mount two first-time tutorial overlays driven by the game state machine: - MovementTutorial: visible during the intro reveal and the free-walk step before the ebike mount, dismissed on the first Z/Q/S/D keydown. - HandTrackingTutorial: visible during the early ebike repair steps (fragmented, scanning, inspected), dismissed when MediaPipe detects any hand on screen. Both share a generic TutorialOverlay shell (transparent panel, dark blue border, lucide-react Hand / inline ZQSD keycap icons, centered text). The overlay sits at z-index 14, behind Subtitles (15) and the talkie overlay (16), so dialogue/subtitle UI stays in front. Dismissals stay persistent for the session: keyboard-triggered uses event-handler setState; hand-detection uses a guarded effect-driven setState (same pattern as PylonDownedPylon resync). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { useGameStore } from "@/managers/stores/useGameStore";
|
||||
import type { GameStep } from "@/types/game";
|
||||
import { TutorialOverlay } from "@/components/ui/tutorial/TutorialOverlay";
|
||||
|
||||
const MOVEMENT_KEYS = new Set(["z", "q", "s", "d"]);
|
||||
// Intro steps where the movement tutorial is allowed to display. From the
|
||||
// reveal fade through the free-walk window before the ebike mount.
|
||||
const MOVEMENT_TUTORIAL_STEPS: ReadonlySet<GameStep> = new Set([
|
||||
"reveal",
|
||||
"await-ebike-mount",
|
||||
]);
|
||||
|
||||
function KeyCap({ label }: { label: string }): React.JSX.Element {
|
||||
return <span className="tutorial-overlay__keycap">{label}</span>;
|
||||
}
|
||||
|
||||
/**
|
||||
* First-time movement tutorial. Visible during the intro reveal and the
|
||||
* walk-around step before the ebike mount, until the player presses any
|
||||
* of Z, Q, S, D. Once dismissed it stays dismissed for the session.
|
||||
*/
|
||||
export function MovementTutorial(): React.JSX.Element | null {
|
||||
const introStep = useGameStore((state) => state.intro.currentStep);
|
||||
const [dismissed, setDismissed] = useState(false);
|
||||
|
||||
const isInShowWindow = MOVEMENT_TUTORIAL_STEPS.has(introStep);
|
||||
|
||||
useEffect(() => {
|
||||
if (dismissed) return;
|
||||
function onKeyDown(event: KeyboardEvent): void {
|
||||
if (MOVEMENT_KEYS.has(event.key.toLowerCase())) {
|
||||
setDismissed(true);
|
||||
}
|
||||
}
|
||||
window.addEventListener("keydown", onKeyDown);
|
||||
return () => window.removeEventListener("keydown", onKeyDown);
|
||||
}, [dismissed]);
|
||||
|
||||
if (!isInShowWindow || dismissed) return null;
|
||||
|
||||
return (
|
||||
<TutorialOverlay
|
||||
icon={
|
||||
<div className="tutorial-overlay__keyboard">
|
||||
<span aria-hidden="true" />
|
||||
<KeyCap label="Z" />
|
||||
<span aria-hidden="true" />
|
||||
<KeyCap label="Q" />
|
||||
<KeyCap label="S" />
|
||||
<KeyCap label="D" />
|
||||
</div>
|
||||
}
|
||||
text="Utilisez le clavier et la souris pour vous déplacer."
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user