diff --git a/docs/user/features.md b/docs/user/features.md index 4cb4329..eedbd88 100644 --- a/docs/user/features.md +++ b/docs/user/features.md @@ -31,7 +31,7 @@ This document lists features that are implemented in the current codebase. - Reusable production `RepairGame` mounted for `bike`, `pylone`, and `ferme` mission states - Repair mission config shared through `src/data/gameplay/repairMissions.ts` -- Repair-game flow supports `waiting -> inspected -> fragmented -> scanning -> repairing -> done -> next mission` with `.webm` prompts, repair case spawn/opening/exit, `E`, two-fists hold input, exploded model transition, scan visuals, multiple grabbable replacement choices, correct-part install validation, and mission completion +- Repair-game flow supports `waiting -> inspected -> fragmented -> scanning -> repairing -> done -> next mission` with `.webm` prompts, repair case spawn/opening/exit, `E`, two-fists hold input, exploded model transition, per-part scan visuals, multiple grabbable replacement choices, correct-part install validation, and mission completion ## Audio diff --git a/docs/user/main-feature.md b/docs/user/main-feature.md index 7ca2ea5..dcd6d76 100644 --- a/docs/user/main-feature.md +++ b/docs/user/main-feature.md @@ -15,7 +15,7 @@ The current user flow is: 5. The repair case appears near the mission object and can float when the player approaches it. 6. Press `E` or hold both fists closed for one second to move from `inspected` to `fragmented`. 7. The mission object uses an exploded-model transition, then moves to `scanning`. -8. The scan visual highlights the broken area and shows the `cassé.webm` prompt. +8. The scan visual moves across the fragmented model one part at a time. 9. In `repairing`, the case opens and several grabbable replacement parts appear near the case. 10. Move the correct replacement part close to the install target. 11. Press `E` on the green install target to move to `done` and show the reassembled object. Wrong parts turn the target red and cannot finish the repair. @@ -33,7 +33,7 @@ When the player inspects the object, `RepairGame` writes `inspected` through the In `inspected`, `RepairGame` can also move to `fragmented`. The player can use the interaction key or hold both fists closed for one second. The hand-tracking path is state-based, so it does not depend on being inside a local object interaction radius. -In `fragmented`, the repair object is rendered with `ExplodableModel`, then automatically advances to `scanning`. In `scanning`, a blue scan visual and the `cassé.webm` prompt are shown before the flow advances to `repairing`. In `repairing`, the case opens, several grabbable replacement parts appear, and the install target only validates the configured correct part for the active mission. In `done`, the repaired object remains visible with a completion target that plays the case exit animation before advancing the global mission progression. +In `fragmented`, the repair object is rendered with `ExplodableModel`, then automatically advances to `scanning`. In `scanning`, the exploded model remains visible and a blue scan visual moves from part to part before the flow advances to `repairing`. In `repairing`, the case opens, several grabbable replacement parts appear, and the install target only validates the configured correct part for the active mission. In `done`, the repaired object remains visible with a completion target that plays the case exit animation before advancing the global mission progression. ## Key Files @@ -44,7 +44,8 @@ In `fragmented`, the repair object is rendered with `ExplodableModel`, then auto - `src/components/three/gameplay/RepairMissionCase.tsx` renders the mission repair case after inspection. - `src/components/three/gameplay/RepairRepairingStep.tsx` renders grabbable replacement choices, correct-part placement validation, and the install trigger in `repairing`. - `src/components/three/gameplay/RepairPromptVideo.tsx` renders `.webm` prompts inside the 3D scene. -- `src/components/three/gameplay/RepairScanVisual.tsx` renders the scan halo, scan line, and broken prompt. +- `src/components/three/gameplay/RepairScanSequence.tsx` keeps the exploded model visible and advances the scan from part to part. +- `src/components/three/gameplay/RepairScanVisual.tsx` renders the scan halo and scan line around the active part. - `src/hooks/gameplay/useRepairFragmentationInput.ts` handles the `inspected -> fragmented` keyboard and hand-tracking input. - `src/hooks/gameplay/useRepairMissionStep.ts` reads the active mission step from the game store. - `src/hooks/handTracking/useBothFistsHold.ts` detects the reusable two-fists hold gesture. diff --git a/src/components/three/gameplay/RepairGame.tsx b/src/components/three/gameplay/RepairGame.tsx index c046df9..fcd1304 100644 --- a/src/components/three/gameplay/RepairGame.tsx +++ b/src/components/three/gameplay/RepairGame.tsx @@ -4,11 +4,8 @@ import { RepairCompletionStep } from "@/components/three/gameplay/RepairCompleti import { RepairInspectionObject } from "@/components/three/gameplay/RepairInspectionObject"; import { RepairMissionCase } from "@/components/three/gameplay/RepairMissionCase"; import { RepairRepairingStep } from "@/components/three/gameplay/RepairRepairingStep"; -import { RepairScanVisual } from "@/components/three/gameplay/RepairScanVisual"; -import { - REPAIR_FRAGMENTATION_SEQUENCE_SECONDS, - REPAIR_SCAN_SEQUENCE_SECONDS, -} from "@/data/gameplay/repairGameConfig"; +import { RepairScanSequence } from "@/components/three/gameplay/RepairScanSequence"; +import { REPAIR_FRAGMENTATION_SEQUENCE_SECONDS } from "@/data/gameplay/repairGameConfig"; import { REPAIR_MISSIONS } from "@/data/gameplay/repairMissions"; import { useRepairFragmentationInput } from "@/hooks/gameplay/useRepairFragmentationInput"; import { useRepairMissionStep } from "@/hooks/gameplay/useRepairMissionStep"; @@ -47,17 +44,11 @@ export function RepairGame({ useEffect(() => { if (mainState !== mission) return undefined; - if (step !== "fragmented" && step !== "scanning") return undefined; - - const nextStep = step === "fragmented" ? "scanning" : "repairing"; - const sequenceSeconds = - step === "fragmented" - ? REPAIR_FRAGMENTATION_SEQUENCE_SECONDS - : REPAIR_SCAN_SEQUENCE_SECONDS; + if (step !== "fragmented") return undefined; const timeoutId = window.setTimeout(() => { - setMissionStep(mission, nextStep); - }, sequenceSeconds * 1000); + setMissionStep(mission, "scanning"); + }, REPAIR_FRAGMENTATION_SEQUENCE_SECONDS * 1000); return () => { window.clearTimeout(timeoutId); @@ -79,7 +70,12 @@ export function RepairGame({ {step === "fragmented" ? ( ) : null} - {step === "scanning" ? : null} + {step === "scanning" ? ( + setMissionStep(mission, "repairing")} + /> + ) : null} {step === "repairing" ? ( void; +} + +export function RepairScanSequence({ + config, + onComplete, +}: RepairScanSequenceProps): React.JSX.Element { + const [parts, setParts] = useState([]); + const [activePartIndex, setActivePartIndex] = useState(0); + const activePart = parts[activePartIndex]; + + useEffect(() => { + if (parts.length === 0) return undefined; + + const timeoutId = window.setTimeout(() => { + setActivePartIndex((currentIndex) => { + const nextIndex = currentIndex + 1; + if (nextIndex >= parts.length) { + onComplete(); + return currentIndex; + } + + return nextIndex; + }); + }, REPAIR_SCAN_PART_SECONDS * 1000); + + return () => { + window.clearTimeout(timeoutId); + }; + }, [activePartIndex, onComplete, parts.length]); + + return ( + + + + + ); +} diff --git a/src/components/three/gameplay/RepairScanVisual.tsx b/src/components/three/gameplay/RepairScanVisual.tsx index 6da2f13..b4703b1 100644 --- a/src/components/three/gameplay/RepairScanVisual.tsx +++ b/src/components/three/gameplay/RepairScanVisual.tsx @@ -1,27 +1,36 @@ import { useRef } from "react"; import { useFrame } from "@react-three/fiber"; import * as THREE from "three"; -import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo"; -import type { RepairMissionConfig } from "@/data/gameplay/repairMissions"; interface RepairScanVisualProps { - config: RepairMissionConfig; + target?: THREE.Object3D | null | undefined; } export function RepairScanVisual({ - config, + target = null, }: RepairScanVisualProps): React.JSX.Element { + const groupRef = useRef(null); const scanLineRef = useRef(null); + const worldPosition = useRef(new THREE.Vector3()); + const localPosition = useRef(new THREE.Vector3()); useFrame(({ clock }) => { + const group = groupRef.current; const scanLine = scanLineRef.current; - if (!scanLine) return; + if (!group || !scanLine) return; + + if (target) { + target.getWorldPosition(worldPosition.current); + localPosition.current.copy(worldPosition.current); + group.parent?.worldToLocal(localPosition.current); + group.position.copy(localPosition.current); + } scanLine.position.y = 0.35 + Math.sin(clock.elapsedTime * 4) * 0.7; }); return ( - + @@ -39,7 +48,6 @@ export function RepairScanVisual({ - ); } diff --git a/src/components/three/models/ExplodableModel.tsx b/src/components/three/models/ExplodableModel.tsx index c8542c7..b23b176 100644 --- a/src/components/three/models/ExplodableModel.tsx +++ b/src/components/three/models/ExplodableModel.tsx @@ -4,6 +4,7 @@ import { useFrame } from "@react-three/fiber"; import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF"; import { useClonedObject } from "@/hooks/three/useClonedObject"; import { ExplodedModel } from "@/utils/three/ExplodedModel"; +import type { ExplodedPart } from "@/utils/three/ExplodedModel"; import type { ModelTransformProps, Vector3Tuple } from "@/types/three/three"; import { logModelLoadError } from "@/utils/three/modelLoadLogger"; import { toVector3Scale } from "@/utils/three/scale"; @@ -55,6 +56,7 @@ interface ExplodableModelInnerProps extends ModelTransformProps { modelPath: string; split: boolean; splitDistance?: number; + onPartsReady?: (parts: readonly ExplodedPart[]) => void; } export function ExplodableModel( @@ -78,6 +80,7 @@ function ExplodableModelInner({ rotation = [0, 0, 0], scale = 1, splitDistance = 1.2, + onPartsReady, }: ExplodableModelInnerProps): React.JSX.Element { const { scene } = useLoggedGLTF(modelPath, { scope: "ExplodableModel", @@ -96,6 +99,10 @@ function ExplodableModelInner({ explodedModel.setSplit(split); }, [explodedModel, split]); + useEffect(() => { + onPartsReady?.(explodedModel.getParts()); + }, [explodedModel, onPartsReady]); + useFrame((_, delta) => { explodedModel.update(delta); }); diff --git a/src/data/docs/docsTranslations.ts b/src/data/docs/docsTranslations.ts index cfa2aae..25db1d8 100644 --- a/src/data/docs/docsTranslations.ts +++ b/src/data/docs/docsTranslations.ts @@ -442,7 +442,7 @@ Ce document liste les fonctionnalités présentes dans le code actuel. - \`RepairGame\` de production réutilisable monté pour les états de mission \`bike\`, \`pylone\` et \`ferme\` - Configuration de mission partagée via \`src/data/gameplay/repairMissions.ts\` -- Flow repair-game avec \`waiting -> inspected -> fragmented -> scanning -> repairing -> done -> next mission\`, prompts \`.webm\`, apparition/ouverture/sortie de la mallette, touche \`E\`, hold deux poings, transition de modèle explosé, visuels de scan, plusieurs choix de pièces grabbables, validation de la bonne pièce et complétion de mission +- Flow repair-game avec \`waiting -> inspected -> fragmented -> scanning -> repairing -> done -> next mission\`, prompts \`.webm\`, apparition/ouverture/sortie de la mallette, touche \`E\`, hold deux poings, transition de modèle explosé, scan visuel par pièce, plusieurs choix de pièces grabbables, validation de la bonne pièce et complétion de mission ## Audio diff --git a/src/data/gameplay/repairGameConfig.ts b/src/data/gameplay/repairGameConfig.ts index a646331..0886ec5 100644 --- a/src/data/gameplay/repairGameConfig.ts +++ b/src/data/gameplay/repairGameConfig.ts @@ -1,3 +1,3 @@ export const REPAIR_FRAGMENTATION_FIST_HOLD_SECONDS = 1; export const REPAIR_FRAGMENTATION_SEQUENCE_SECONDS = 4; -export const REPAIR_SCAN_SEQUENCE_SECONDS = 4; +export const REPAIR_SCAN_PART_SECONDS = 1.2; diff --git a/src/utils/three/ExplodedModel.ts b/src/utils/three/ExplodedModel.ts index 1b62428..e646ebd 100644 --- a/src/utils/three/ExplodedModel.ts +++ b/src/utils/three/ExplodedModel.ts @@ -1,6 +1,6 @@ import * as THREE from "three"; -interface ExplodedPart { +export interface ExplodedPart { object: THREE.Object3D; originalPosition: THREE.Vector3; targetPosition: THREE.Vector3; @@ -31,6 +31,10 @@ export class ExplodedModel { this.targetProgress = split ? 1 : 0; } + getParts(): readonly ExplodedPart[] { + return this.parts; + } + update(delta: number): void { const diff = this.targetProgress - this.progress; if (Math.abs(diff) < 0.001) {