From 80bc74c3a8c5b97356ff1b265e93ce5e804920ee Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Fri, 8 May 2026 02:10:19 +0100 Subject: [PATCH] add: animation on repair case --- docs/user/features.md | 2 +- docs/user/main-feature.md | 6 +-- .../three/gameplay/RepairCaseModel.tsx | 34 +++++++++++++- .../three/gameplay/RepairCompletionStep.tsx | 46 ++++++++++++------- .../three/gameplay/RepairMissionCase.tsx | 8 +++- src/data/docs/docsTranslations.ts | 2 +- src/data/gameplay/repairCaseConfig.ts | 2 + 7 files changed, 77 insertions(+), 23 deletions(-) diff --git a/docs/user/features.md b/docs/user/features.md index 1df61ee..4cb4329 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, `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, 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 53b7c29..7ca2ea5 100644 --- a/docs/user/main-feature.md +++ b/docs/user/main-feature.md @@ -19,7 +19,7 @@ The current user flow is: 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. -12. Press `E` on the completion target to call `completeMission` and move to the next mission, or to `outro` after `ferme`. +12. 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 @@ -33,12 +33,12 @@ 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 advances the global mission progression. +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. ## Key Files - `src/world/GameStageContent.tsx` mounts production `RepairGame` instances for `bike`, `pylone`, and `ferme`. -- `src/components/three/gameplay/RepairCompletionStep.tsx` renders the final repaired object, completion target, and mission UI prompt. +- `src/components/three/gameplay/RepairCompletionStep.tsx` renders the final repaired object, completion target, case exit animation, and mission UI prompt. - `src/components/three/gameplay/RepairGame.tsx` composes the reusable production repair flow. - `src/components/three/gameplay/RepairInspectionObject.tsx` handles the `waiting` inspection interaction. - `src/components/three/gameplay/RepairMissionCase.tsx` renders the mission repair case after inspection. diff --git a/src/components/three/gameplay/RepairCaseModel.tsx b/src/components/three/gameplay/RepairCaseModel.tsx index 49cd9f1..ab5085f 100644 --- a/src/components/three/gameplay/RepairCaseModel.tsx +++ b/src/components/three/gameplay/RepairCaseModel.tsx @@ -8,6 +8,8 @@ import { REPAIR_CASE_FLOAT_ACTIVATION_DISTANCE, REPAIR_CASE_FLOAT_DOWN_SPEED, REPAIR_CASE_FLOAT_HEIGHT, + REPAIR_CASE_EXIT_DURATION, + REPAIR_CASE_EXIT_Y_OFFSET, REPAIR_CASE_FLOAT_UP_SPEED, REPAIR_CASE_LID_NODE_NAME, REPAIR_CASE_OPEN_ROTATION_OFFSET_DEGREES, @@ -24,6 +26,8 @@ import { toVector3Scale } from "@/utils/three/scale"; interface RepairCaseModelProps extends ModelTransformProps { modelPath: string; open: boolean; + exiting?: boolean; + onExitComplete?: (() => void) | undefined; } const CASE_CLOSED_ROTATION_OFFSET_Z = THREE.MathUtils.degToRad( @@ -39,6 +43,8 @@ const ROTATION_AMPLITUDE = THREE.MathUtils.degToRad( export function RepairCaseModel({ modelPath, open, + exiting = false, + onExitComplete, position = [0, 0, 0], rotation = [0, 0, 0], scale = 1, @@ -58,10 +64,15 @@ export function RepairCaseModel({ const animationActiveRef = useRef(false); const phase = useRef({ x: 0, y: 0, z: 0 }); const pop = useRef({ scale: 0.001, yOffset: REPAIR_CASE_POP_Y_OFFSET }); + const onExitCompleteRef = useRef(onExitComplete); const initialOpen = useRef(open); const openedRotationZ = useRef(0); const parsedScale = toVector3Scale(scale); + useEffect(() => { + onExitCompleteRef.current = onExitComplete; + }, [onExitComplete]); + useEffect(() => { const popAnimation = pop.current; @@ -83,6 +94,26 @@ export function RepairCaseModel({ }; }, []); + useEffect(() => { + if (!exiting) return undefined; + + const popAnimation = pop.current; + gsap.to(popAnimation, { + scale: 0.001, + yOffset: REPAIR_CASE_EXIT_Y_OFFSET, + duration: REPAIR_CASE_EXIT_DURATION, + ease: "back.in(1.4)", + overwrite: true, + onComplete: () => { + onExitCompleteRef.current?.(); + }, + }); + + return () => { + gsap.killTweensOf(popAnimation); + }; + }, [exiting]); + useEffect(() => { const lid = model.getObjectByName(REPAIR_CASE_LID_NODE_NAME); lidRef.current = lid ?? null; @@ -122,8 +153,9 @@ export function RepairCaseModel({ group.getWorldPosition(worldPosition.current); const isNear = + !exiting && worldPosition.current.distanceTo(camera.position) <= - REPAIR_CASE_FLOAT_ACTIVATION_DISTANCE; + REPAIR_CASE_FLOAT_ACTIVATION_DISTANCE; const targetHeight = isNear ? REPAIR_CASE_FLOAT_HEIGHT : 0; const floatSpeed = isNear ? REPAIR_CASE_FLOAT_UP_SPEED diff --git a/src/components/three/gameplay/RepairCompletionStep.tsx b/src/components/three/gameplay/RepairCompletionStep.tsx index 621fd03..21ba652 100644 --- a/src/components/three/gameplay/RepairCompletionStep.tsx +++ b/src/components/three/gameplay/RepairCompletionStep.tsx @@ -1,5 +1,7 @@ +import { useState } from "react"; import { RepairObjectModel } from "@/components/three/gameplay/RepairObjectModel"; import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo"; +import { RepairMissionCase } from "@/components/three/gameplay/RepairMissionCase"; import { TriggerObject } from "@/components/three/interaction/TriggerObject"; import type { RepairMissionConfig } from "@/data/gameplay/repairMissions"; @@ -12,31 +14,43 @@ export function RepairCompletionStep({ config, onComplete, }: RepairCompletionStepProps): React.JSX.Element { + const [isCompleting, setIsCompleting] = useState(false); + return ( + + - - - - - - - - - - + {!isCompleting ? ( + setIsCompleting(true)} + > + + + + + + + + + + ) : null} - + {!isCompleting ? ( + + ) : null} ); } diff --git a/src/components/three/gameplay/RepairMissionCase.tsx b/src/components/three/gameplay/RepairMissionCase.tsx index 46bb6a4..7fd6234 100644 --- a/src/components/three/gameplay/RepairMissionCase.tsx +++ b/src/components/three/gameplay/RepairMissionCase.tsx @@ -5,12 +5,16 @@ import type { RepairMissionConfig } from "@/data/gameplay/repairMissions"; interface RepairMissionCaseProps { config: RepairMissionConfig; + exiting?: boolean; + onExitComplete?: (() => void) | undefined; open?: boolean; showFragmentationPrompt?: boolean; } export function RepairMissionCase({ config, + exiting = false, + onExitComplete, open = false, showFragmentationPrompt = false, }: RepairMissionCaseProps): React.JSX.Element { @@ -18,12 +22,14 @@ export function RepairMissionCase({ - {showFragmentationPrompt ? ( + {showFragmentationPrompt && !exiting ? ( inspected -> fragmented -> scanning -> repairing -> done -> next mission\`, prompts \`.webm\`, apparition/ouverture 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é, visuels de scan, plusieurs choix de pièces grabbables, validation de la bonne pièce et complétion de mission ## Audio diff --git a/src/data/gameplay/repairCaseConfig.ts b/src/data/gameplay/repairCaseConfig.ts index 1dc48d1..df05773 100644 --- a/src/data/gameplay/repairCaseConfig.ts +++ b/src/data/gameplay/repairCaseConfig.ts @@ -8,6 +8,8 @@ export const REPAIR_CASE_OPEN_ROTATION_OFFSET_DEGREES = 115; export const REPAIR_CASE_ANIMATION_DURATION = 0.8; export const REPAIR_CASE_POP_DURATION = 0.45; export const REPAIR_CASE_POP_Y_OFFSET = -0.25; +export const REPAIR_CASE_EXIT_DURATION = 0.5; +export const REPAIR_CASE_EXIT_Y_OFFSET = -0.35; export const REPAIR_CASE_FLOAT_ACTIVATION_DISTANCE = 5; export const REPAIR_CASE_FLOAT_HEIGHT = 1;