fix(ui): scope hand-tracking activation + clean MissionNotification video branch

- HandTrackingProvider: drop the physics-mode auto-activation that turned
  the camera/MediaPipe pipeline on whenever any interactable was nearby
  (e.g. walking near the ebike to mount it). Hand tracking is now gated
  *only* by the active repair-mission step (inspected, repairing,
  reassembling, done). When testing in TestMap, set
  mainState=ebike + currentStep=inspected via the GameStateDebugPanel.
- MissionNotification: video branch no longer inherits the CRT-style
  enter/scan/flicker/sepia animations applied to the PNG branch via
  index.css. The webm assets already animate themselves, so the wrapping
  container is rendered with inline styles only (clip-path silhouette
  preserved, but no .__image-wrap::before scan line, no .__image flicker
  filter, no parent enter animation, no drop-shadow).
This commit is contained in:
Tom Boullay
2026-06-03 03:44:04 +02:00
parent 7bcbba4eb1
commit 5ad2e27a89
2 changed files with 43 additions and 15 deletions
+33 -5
View File
@@ -6,6 +6,12 @@ import type { RepairMissionId } from "@/types/gameplay/repairMission";
// <video> element renders at the wrong dimensions and shifts the layout. // <video> element renders at the wrong dimensions and shifts the layout.
const NOTIFICATION_ASPECT_RATIO = "589 / 211"; const NOTIFICATION_ASPECT_RATIO = "589 / 211";
// Same clip-path as `.mission-notification__image-wrap` in index.css. Inlined
// here so the video branch can re-use the silhouette without inheriting the
// scan-line `::before` and CRT animations applied to the PNG branch.
const NOTIFICATION_CLIP_PATH =
"polygon(0 0, 100% 0, 100% 69%, 88% 100%, 0 100%)";
interface MissionNotificationProps { interface MissionNotificationProps {
mission?: RepairMissionId; mission?: RepairMissionId;
imagePath?: string; imagePath?: string;
@@ -24,14 +30,34 @@ export function MissionNotification({
return ( return (
<div <div
className={`mission-notification${visible ? "" : " mission-notification--hidden"}`} className={`mission-notification${visible ? "" : " mission-notification--hidden"}`}
// Webm assets already animate themselves; suppress the CRT entrance
// flicker + drop-shadow that index.css applies to all .mission-notification
// nodes so the video plays in a clean container.
style={
isVideo
? {
animation: "none",
filter: "none",
}
: undefined
}
aria-live="polite" aria-live="polite"
> >
<div className="mission-notification__glow" /> {isVideo ? null : <div className="mission-notification__glow" />}
<span className="mission-notification__image-wrap">
{isVideo ? ( {isVideo ? (
<video <span
className="mission-notification__image"
style={{ style={{
position: "relative",
display: "block",
overflow: "hidden",
clipPath: NOTIFICATION_CLIP_PATH,
}}
>
<video
style={{
display: "block",
width: "100%",
height: "auto",
aspectRatio: NOTIFICATION_ASPECT_RATIO, aspectRatio: NOTIFICATION_ASPECT_RATIO,
objectFit: "cover", objectFit: "cover",
}} }}
@@ -43,14 +69,16 @@ export function MissionNotification({
playsInline playsInline
preload="auto" preload="auto"
/> />
</span>
) : ( ) : (
<span className="mission-notification__image-wrap">
<img <img
className="mission-notification__image" className="mission-notification__image"
src={src} src={src}
alt="Nouvel objectif de mission" alt="Nouvel objectif de mission"
/> />
)}
</span> </span>
)}
</div> </div>
); );
} }
@@ -1,9 +1,7 @@
import type { ReactNode } from "react"; import type { ReactNode } from "react";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { HAND_TRACKING_LINGER_MS } from "@/data/handTrackingConfig"; import { HAND_TRACKING_LINGER_MS } from "@/data/handTrackingConfig";
import { useSceneMode } from "@/hooks/debug/useSceneMode";
import { useDebugStore } from "@/hooks/debug/useDebugStore"; import { useDebugStore } from "@/hooks/debug/useDebugStore";
import { useInteraction } from "@/hooks/interaction/useInteraction";
import { import {
HAND_TRACKING_IDLE_SNAPSHOT, HAND_TRACKING_IDLE_SNAPSHOT,
HandTrackingContext, HandTrackingContext,
@@ -25,8 +23,14 @@ export function HandTrackingProvider({
}: { }: {
children: ReactNode; children: ReactNode;
}): React.JSX.Element { }): React.JSX.Element {
const sceneMode = useSceneMode(); // Hand tracking is gated *only* by the active repair-mission step. We
const repairNeedsHands = useGameStore((state) => { // intentionally do NOT activate it from generic interactable proximity
// (e.g. standing next to the ebike to mount it) — that previously caused
// hand tracking to spin up around any interactable in the physics
// (TestMap) scene mode, even though the player wasn't in a step that
// actually uses hands. Use the GameStateDebugPanel to set
// mainState=ebike + currentStep=inspected when testing in TestMap.
const requested = useGameStore((state) => {
switch (state.mainState) { switch (state.mainState) {
case "ebike": case "ebike":
return REPAIR_HAND_TRACKING_STEPS.has(state.ebike.currentStep); return REPAIR_HAND_TRACKING_STEPS.has(state.ebike.currentStep);
@@ -39,10 +43,6 @@ export function HandTrackingProvider({
return false; return false;
} }
}); });
const { nearby, holding, handHolding } = useInteraction();
const requested =
repairNeedsHands ||
(sceneMode === "physics" && (nearby || holding || handHolding));
// Keep the runtime active a little after `requested` turns off so // Keep the runtime active a little after `requested` turns off so
// MediaPipe has time to initialize the webcam + model + first frame // MediaPipe has time to initialize the webcam + model + first frame