add: animate repair reassembly

This commit is contained in:
Tom Boullay
2026-05-08 02:40:31 +01:00
parent 19a83982a9
commit ead3634aab
12 changed files with 126 additions and 13 deletions
+1 -1
View File
@@ -33,7 +33,7 @@ This keeps hand tracking active while the player is inside an interaction zone,
The production repair activation conditions are: The production repair activation conditions are:
- active `mainState` is `bike`, `pylone`, or `ferme` - active `mainState` is `bike`, `pylone`, or `ferme`
- the active mission step is `inspected`, `repairing`, or `done` - the active mission step is `inspected`, `repairing`, `reassembling`, or `done`
This keeps the webcam off during `waiting`, `fragmented`, and `scanning`, then enables hand input only when the repair flow is expected to use hands. This keeps the webcam off during `waiting`, `fragmented`, and `scanning`, then enables hand input only when the repair flow is expected to use hands.
+2 -1
View File
@@ -75,6 +75,7 @@ The mission steps currently use this sequence:
"fragmented" | "fragmented" |
"scanning" | "scanning" |
"repairing" | "repairing" |
"reassembling" |
"done"; "done";
``` ```
@@ -136,7 +137,7 @@ For repair missions, it mounts the reusable `RepairGame` component with a missio
<RepairGame mission="bike" position={[8, 0, -6]} /> <RepairGame mission="bike" position={[8, 0, -6]} />
``` ```
`RepairGame` reads the active mission step from the store and writes transitions through generic actions such as `setMissionStep` and `completeMission`. This keeps the scene component small and avoids mission-specific branching inside the repair flow. The production repair flow currently supports `waiting -> inspected -> fragmented -> scanning -> repairing -> done -> next mission` state transitions. `RepairGame` reads the active mission step from the store and writes transitions through generic actions such as `setMissionStep` and `completeMission`. This keeps the scene component small and avoids mission-specific branching inside the repair flow. The production repair flow currently supports `waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission` state transitions.
That means the scene can progressively move toward this pattern: That means the scene can progressively move toward this pattern:
+1 -1
View File
@@ -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 - Reusable production `RepairGame` mounted for `bike`, `pylone`, and `ferme` mission states
- Repair mission config shared through `src/data/gameplay/repairMissions.ts` - 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, focused repair-case view, case placeholder traversal, snap-to-placeholder placement, broken-part deposit, `E`, two-fists hold input, exploded model transition, per-part scan visuals, persistent red broken-part markers, centered broken-part UI videos, multiple grabbable replacement choices, correct-part install validation, and mission completion - Repair-game flow supports `waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission` with `.webm` prompts, repair case spawn/opening/exit, focused repair-case view, case placeholder traversal, snap-to-placeholder placement, broken-part deposit, `E`, two-fists hold input, exploded and inverse reassembly transitions, completion particles, per-part scan visuals, persistent red broken-part markers, centered broken-part UI videos, multiple grabbable replacement choices, correct-part install validation, and mission completion
## Audio ## Audio
+7 -4
View File
@@ -19,8 +19,9 @@ The current user flow is:
9. In `repairing`, the case opens in a larger focused view and several grabbable replacement parts appear on the case placeholders. 9. In `repairing`, the case opens in a larger focused view and several grabbable replacement parts appear on the case placeholders.
10. Move the correct replacement part close to a placeholder. When released near a placeholder, it snaps into place with a short animation. 10. Move the correct replacement part close to a placeholder. When released near a placeholder, it snaps into place with a short animation.
11. Move each scanned broken part into a compatible placeholder so the damaged parts are stored in the case. 11. Move each scanned broken part into a compatible placeholder so the damaged parts are stored in the case.
12. 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. 12. Press `E` on the green install target to move to `reassembling`. Wrong parts turn the target red and cannot finish the repair.
13. Press `E` on the completion target. The repair case closes, returns to the ground, disappears, then `completeMission` moves to the next mission or to `outro` after `ferme`. 13. The exploded object animates back into its assembled form with completion particles, then moves to `done`.
14. Press `E` on the completion target. The repair case closes, returns to the ground, disappears, then `completeMission` moves to the next mission or to `outro` after `ferme`.
## Why It Matters ## Why It Matters
@@ -34,7 +35,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 `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`, the exploded model remains visible, a blue scan visual moves from part to part, and a red halo/wire marker plus the configured broken UI video stay attached to configured broken parts after the scanner reaches them. The scan can match a specific `nodeName` when mission data provides one, otherwise it falls back to the first scanned parts as placeholder broken parts. In `repairing`, the case opens in a larger focused transform, `RepairCaseModel` traverses the case GLTF for empty nodes named `placeholder_*`, several grabbable replacement parts appear on those placeholder positions, and releasing a part near a placeholder snaps it into place with a short GSAP animation. Scanned broken parts are also rendered as grabbable objects and must be deposited into a compatible placeholder before the final install target validates. If `brokenParts[].placeholderName` is configured, that broken part snaps only to the matching placeholder; otherwise it can use any available placeholder. If the current case asset has no placeholder nodes, the flow keeps using fallback focus positions. The install target only validates when the configured correct replacement part is placed and all scanned broken parts have been deposited. 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, a blue scan visual moves from part to part, and a red halo/wire marker plus the configured broken UI video stay attached to configured broken parts after the scanner reaches them. The scan can match a specific `nodeName` when mission data provides one, otherwise it falls back to the first scanned parts as placeholder broken parts. In `repairing`, the case opens in a larger focused transform, `RepairCaseModel` traverses the case GLTF for empty nodes named `placeholder_*`, several grabbable replacement parts appear on those placeholder positions, and releasing a part near a placeholder snaps it into place with a short GSAP animation. Scanned broken parts are also rendered as grabbable objects and must be deposited into a compatible placeholder before the final install target validates. If `brokenParts[].placeholderName` is configured, that broken part snaps only to the matching placeholder; otherwise it can use any available placeholder. If the current case asset has no placeholder nodes, the flow keeps using fallback focus positions. The install target only validates when the configured correct replacement part is placed and all scanned broken parts have been deposited. In `reassembling`, the exploded model animates back into its assembled position with green completion particles before the flow moves to `done`. 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 ## Key Files
@@ -46,6 +47,8 @@ In `fragmented`, the repair object is rendered with `ExplodableModel`, then auto
- `src/components/three/gameplay/RepairInspectionObject.tsx` handles the `waiting` inspection interaction. - `src/components/three/gameplay/RepairInspectionObject.tsx` handles the `waiting` inspection interaction.
- `src/components/three/gameplay/RepairMissionCase.tsx` renders the mission repair case after inspection. - `src/components/three/gameplay/RepairMissionCase.tsx` renders the mission repair case after inspection.
- `src/components/three/gameplay/RepairRepairingStep.tsx` renders grabbable replacement choices, grabbable scanned broken parts, placeholder placement markers, snap placement behavior, correct-part and broken-part placement validation, and the install trigger in `repairing`. - `src/components/three/gameplay/RepairRepairingStep.tsx` renders grabbable replacement choices, grabbable scanned broken parts, placeholder placement markers, snap placement behavior, correct-part and broken-part placement validation, and the install trigger in `repairing`.
- `src/components/three/gameplay/RepairReassemblyStep.tsx` renders the inverse fragmentation animation before the final completion step.
- `src/components/three/gameplay/RepairCompletionParticles.tsx` renders the green completion particles during reassembly.
- `src/components/three/gameplay/RepairPromptVideo.tsx` renders `.webm` prompts inside the 3D scene. - `src/components/three/gameplay/RepairPromptVideo.tsx` renders `.webm` prompts inside the 3D scene.
- `src/components/three/gameplay/RepairScanSequence.tsx` keeps the exploded model visible and advances the scan from part to part. - `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/components/three/gameplay/RepairScanVisual.tsx` renders the scan halo and scan line around the active part.
@@ -93,7 +96,7 @@ python -m backend.main
## Current Limitations ## Current Limitations
- The reusable production `RepairGame` currently covers `waiting -> inspected -> fragmented -> scanning -> repairing -> done -> next mission`. - The reusable production `RepairGame` currently covers `waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission`.
- Mission progression is wired through Zustand using `completeMission` at the end of each repair. - Mission progression is wired through Zustand using `completeMission` at the end of each repair.
- There is no central `GameManager` in this branch. - There is no central `GameManager` in this branch.
- Hand tracking is available for the two-fists input and grabbable replacement parts; final installation still uses the shared `E` trigger path. - Hand tracking is available for the two-fists input and grabbable replacement parts; final installation still uses the shared `E` trigger path.
@@ -0,0 +1,52 @@
import { useRef } from "react";
import { useFrame } from "@react-three/fiber";
import * as THREE from "three";
const PARTICLES = Array.from({ length: 24 }, (_, index) => {
const angle = (index / 24) * Math.PI * 2;
const ring = index % 3;
return {
angle,
radius: 0.45 + ring * 0.28,
y: 0.35 + (index % 5) * 0.16,
speed: 0.8 + (index % 4) * 0.18,
};
});
export function RepairCompletionParticles(): React.JSX.Element {
const groupRef = useRef<THREE.Group>(null);
useFrame(({ clock }) => {
const group = groupRef.current;
if (!group) return;
group.rotation.y = clock.elapsedTime * 0.9;
group.children.forEach((child, index) => {
const particle = PARTICLES[index];
if (!particle) return;
const pulse = 1 + Math.sin(clock.elapsedTime * 5 + index) * 0.35;
child.position.y =
particle.y + Math.sin(clock.elapsedTime * particle.speed) * 0.08;
child.scale.setScalar(pulse);
});
});
return (
<group ref={groupRef}>
{PARTICLES.map((particle, index) => (
<mesh
key={index}
position={[
Math.cos(particle.angle) * particle.radius,
particle.y,
Math.sin(particle.angle) * particle.radius,
]}
>
<sphereGeometry args={[0.045, 12, 12]} />
<meshBasicMaterial color="#86efac" transparent opacity={0.85} />
</mesh>
))}
</group>
);
}
+9 -2
View File
@@ -5,6 +5,7 @@ import { RepairCompletionStep } from "@/components/three/gameplay/RepairCompleti
import { RepairInspectionObject } from "@/components/three/gameplay/RepairInspectionObject"; import { RepairInspectionObject } from "@/components/three/gameplay/RepairInspectionObject";
import { RepairMissionCase } from "@/components/three/gameplay/RepairMissionCase"; import { RepairMissionCase } from "@/components/three/gameplay/RepairMissionCase";
import { RepairRepairingStep } from "@/components/three/gameplay/RepairRepairingStep"; import { RepairRepairingStep } from "@/components/three/gameplay/RepairRepairingStep";
import { RepairReassemblyStep } from "@/components/three/gameplay/RepairReassemblyStep";
import { import {
RepairScanSequence, RepairScanSequence,
type RepairScannedBrokenPart, type RepairScannedBrokenPart,
@@ -94,7 +95,13 @@ export function RepairGame({
brokenParts={scannedBrokenParts} brokenParts={scannedBrokenParts}
config={config} config={config}
placeholders={casePlaceholders} placeholders={casePlaceholders}
onRepair={() => setMissionStep(mission, "done")} onRepair={() => setMissionStep(mission, "reassembling")}
/>
) : null}
{step === "reassembling" ? (
<RepairReassemblyStep
config={config}
onComplete={() => setMissionStep(mission, "done")}
/> />
) : null} ) : null}
{step === "done" ? ( {step === "done" ? (
@@ -103,7 +110,7 @@ export function RepairGame({
onComplete={() => completeMission(mission)} onComplete={() => completeMission(mission)}
/> />
) : null} ) : null}
{step !== "waiting" && step !== "done" ? ( {step !== "waiting" && step !== "done" && step !== "reassembling" ? (
<RepairMissionCase <RepairMissionCase
config={config} config={config}
onPlaceholdersChange={setCasePlaceholders} onPlaceholdersChange={setCasePlaceholders}
@@ -0,0 +1,42 @@
import { useEffect, useState } from "react";
import { RepairCompletionParticles } from "@/components/three/gameplay/RepairCompletionParticles";
import { ExplodableModel } from "@/components/three/models/ExplodableModel";
import { REPAIR_REASSEMBLY_SECONDS } from "@/data/gameplay/repairGameConfig";
import type { RepairMissionConfig } from "@/data/gameplay/repairMissions";
interface RepairReassemblyStepProps {
config: RepairMissionConfig;
onComplete: () => void;
}
export function RepairReassemblyStep({
config,
onComplete,
}: RepairReassemblyStepProps): React.JSX.Element {
const [split, setSplit] = useState(true);
useEffect(() => {
const closeTimeoutId = window.setTimeout(() => {
setSplit(false);
}, 50);
const completeTimeoutId = window.setTimeout(() => {
onComplete();
}, REPAIR_REASSEMBLY_SECONDS * 1000);
return () => {
window.clearTimeout(closeTimeoutId);
window.clearTimeout(completeTimeoutId);
};
}, [onComplete]);
return (
<group>
<ExplodableModel
modelPath={config.modelPath}
split={split}
splitDistance={1.2}
/>
<RepairCompletionParticles />
</group>
);
}
@@ -20,6 +20,7 @@ const MISSION_STEPS: MissionStep[] = [
"fragmented", "fragmented",
"scanning", "scanning",
"repairing", "repairing",
"reassembling",
"done", "done",
]; ];
+3 -3
View File
@@ -300,7 +300,7 @@ Le store expose :
Les étapes de mission utilisent actuellement cette séquence : Les étapes de mission utilisent actuellement cette séquence :
\`\`\`ts \`\`\`ts
"locked" | "waiting" | "inspected" | "fragmented" | "scanning" | "repairing" | "done" "locked" | "waiting" | "inspected" | "fragmented" | "scanning" | "repairing" | "reassembling" | "done"
\`\`\` \`\`\`
## Lire le state dans un composant ## Lire le state dans un composant
@@ -361,7 +361,7 @@ Pour les missions de réparation, il monte le composant réutilisable \`RepairGa
<RepairGame mission="bike" position={[8, 0, -6]} /> <RepairGame mission="bike" position={[8, 0, -6]} />
\`\`\` \`\`\`
\`RepairGame\` lit l'étape de mission active depuis le store et écrit les transitions via des actions génériques comme \`setMissionStep\` et \`completeMission\`. Cela garde le composant de scène petit et évite les branches spécifiques à chaque mission dans le flow de réparation. Le flow de réparation de production supporte actuellement les transitions \`waiting -> inspected -> fragmented -> scanning -> repairing -> done -> next mission\`. \`RepairGame\` lit l'étape de mission active depuis le store et écrit les transitions via des actions génériques comme \`setMissionStep\` et \`completeMission\`. Cela garde le composant de scène petit et évite les branches spécifiques à chaque mission dans le flow de réparation. Le flow de réparation de production supporte actuellement les transitions \`waiting -> inspected -> fragmented -> scanning -> repairing -> reassembling -> done -> next mission\`.
La scène peut donc évoluer progressivement vers ce pattern : La scène peut donc évoluer progressivement vers ce pattern :
@@ -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\` - \`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\` - 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, vue focalisée de la mallette, traverse des placeholders de mallette, placement avec snap vers placeholder, dépôt des pièces cassées, touche \`E\`, hold deux poings, transition de modèle explosé, scan visuel par pièce, marqueur rouge persistant et vidéo UI centrée sur les pièces cassées, 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 -> reassembling -> done -> next mission\`, prompts \`.webm\`, apparition/ouverture/sortie de la mallette, vue focalisée de la mallette, traverse des placeholders de mallette, placement avec snap vers placeholder, dépôt des pièces cassées, touche \`E\`, hold deux poings, transition de modèle explosé, réassemblage inverse avec particules, scan visuel par pièce, marqueur rouge persistant et vidéo UI centrée sur les pièces cassées, plusieurs choix de pièces grabbables, validation de la bonne pièce et complétion de mission
## Audio ## Audio
+1
View File
@@ -1,3 +1,4 @@
export const REPAIR_FRAGMENTATION_FIST_HOLD_SECONDS = 1; export const REPAIR_FRAGMENTATION_FIST_HOLD_SECONDS = 1;
export const REPAIR_FRAGMENTATION_SEQUENCE_SECONDS = 4; export const REPAIR_FRAGMENTATION_SEQUENCE_SECONDS = 4;
export const REPAIR_SCAN_PART_SECONDS = 1.2; export const REPAIR_SCAN_PART_SECONDS = 1.2;
export const REPAIR_REASSEMBLY_SECONDS = 1.4;
+6 -1
View File
@@ -9,6 +9,7 @@ export type MissionStep =
| "fragmented" | "fragmented"
| "scanning" | "scanning"
| "repairing" | "repairing"
| "reassembling"
| "done"; | "done";
interface IntroState { interface IntroState {
@@ -81,6 +82,8 @@ function getNextMissionStep(step: MissionStep): MissionStep {
case "scanning": case "scanning":
return "repairing"; return "repairing";
case "repairing": case "repairing":
return "reassembling";
case "reassembling":
case "done": case "done":
return "done"; return "done";
} }
@@ -99,8 +102,10 @@ function getPreviousMissionStep(step: MissionStep): MissionStep {
return "fragmented"; return "fragmented";
case "repairing": case "repairing":
return "scanning"; return "scanning";
case "done": case "reassembling":
return "repairing"; return "repairing";
case "done":
return "reassembling";
} }
} }
@@ -14,6 +14,7 @@ import type { MissionStep } from "@/managers/stores/useGameStore";
const REPAIR_HAND_TRACKING_STEPS = new Set<MissionStep>([ const REPAIR_HAND_TRACKING_STEPS = new Set<MissionStep>([
"inspected", "inspected",
"repairing", "repairing",
"reassembling",
"done", "done",
]); ]);