Files
La-Fabrik/src/components/ui/tutorial/HandTrackingTutorial.tsx
T
Tom Boullay 7bcbba4eb1
🔍 Lint / 🪄 Check lint (push) Has been cancelled
🔍 Lint / 🎨 Check format (push) Has been cancelled
🔍 Lint / 🔎 Typecheck (push) Has been cancelled
📊 Quality / 🔒 Security Audit (push) Has been cancelled
📊 Quality / 📋 Dependency Freshness (push) Has been cancelled
📊 Quality / 📦 Bundle Size (push) Has been cancelled
🔍 Lint / 🏗 Build (push) Has been cancelled
fix(ui): polish demo-flow overlays
- OutroVideoOverlay: stagger reveal so 'Next step :' appears immediately and
  'La ferme' fades in 500ms later, instead of both showing at once.
- MissionNotification: enforce 589/211 aspect-ratio + objectFit cover on the
  <video> branch so webm assets (square 2000x2000) render at the same place
  as the legacy PNG notifications instead of shifting the layout.
- HandTrackingTutorial: add a 5000ms fallback timeout that auto-dismisses
  the overlay if MediaPipe never reports a hand (camera blocked, mouse-only
  player), so the screen never stays stuck.
2026-06-03 03:27:18 +02:00

76 lines
2.8 KiB
TypeScript

import { useEffect, useState } from "react";
import { Hand } from "lucide-react";
import { useGameStore } from "@/managers/stores/useGameStore";
import { useHandTrackingSnapshot } from "@/hooks/handTracking/useHandTrackingSnapshot";
import type { MissionStep } from "@/types/gameplay/repairMission";
import { TutorialOverlay } from "@/components/ui/tutorial/TutorialOverlay";
// Repair steps where the hand-tracking tutorial is allowed to display. Covers
// the no-hand-tracking phase (fragmented, scanning) and the first hand-driven
// step (inspected) — beyond that the player has presumably learned.
const HAND_TUTORIAL_STEPS: ReadonlySet<MissionStep> = new Set([
"fragmented",
"scanning",
"inspected",
]);
// Fallback: if hand detection never fires (camera blocked, MediaPipe failure,
// player using mouse), the tutorial auto-dismisses after this delay so it
// never blocks the screen indefinitely.
const HAND_TUTORIAL_FALLBACK_TIMEOUT_MS = 5000;
/**
* First-time hand-tracking tutorial. Visible during the early ebike repair
* steps until MediaPipe actually detects a hand on screen. Once dismissed it
* stays dismissed for the session.
*/
export function HandTrackingTutorial(): React.JSX.Element | null {
const mainState = useGameStore((state) => state.mainState);
const ebikeStep = useGameStore((state) => state.ebike.currentStep);
const { hands, status } = useHandTrackingSnapshot();
const [dismissed, setDismissed] = useState(false);
const isInShowWindow =
mainState === "ebike" && HAND_TUTORIAL_STEPS.has(ebikeStep);
const handsDetected = status !== "idle" && hands.length > 0;
useEffect(() => {
if (handsDetected && !dismissed) {
// Sync the persistent dismissal flag with an external signal (the
// hand-tracking snapshot). Same shape as the resync pattern used
// elsewhere in the repo (e.g. PylonDownedPylon).
// eslint-disable-next-line react-hooks/set-state-in-effect
setDismissed(true);
}
}, [handsDetected, dismissed]);
// Fallback timeout: dismiss the tutorial even if no hand is ever detected,
// so the overlay never gets stuck on screen.
useEffect(() => {
if (!isInShowWindow || dismissed) return undefined;
const timer = window.setTimeout(
() => setDismissed(true),
HAND_TUTORIAL_FALLBACK_TIMEOUT_MS,
);
return () => window.clearTimeout(timer);
}, [isInShowWindow, dismissed]);
if (!isInShowWindow || dismissed) return null;
return (
<TutorialOverlay
icon={
<div className="tutorial-overlay__hands">
<Hand size={96} strokeWidth={1.5} />
<Hand
size={96}
strokeWidth={1.5}
style={{ transform: "scaleX(-1)" }}
/>
</div>
}
text="Placez vos mains devant la caméra pour attraper les pièces. Sinon, utilisez la souris."
/>
);
}