add: animation on repair case

This commit is contained in:
Tom Boullay
2026-05-08 02:10:19 +01:00
parent eee69825c6
commit 95d9bd4f3e
7 changed files with 77 additions and 23 deletions
+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, `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 ## Audio
+3 -3
View File
@@ -19,7 +19,7 @@ The current user flow is:
9. In `repairing`, the case opens and several grabbable replacement parts appear near the case. 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. 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. 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 ## 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 `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 ## Key Files
- `src/world/GameStageContent.tsx` mounts production `RepairGame` instances for `bike`, `pylone`, and `ferme`. - `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/RepairGame.tsx` composes the reusable production repair flow.
- `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.
@@ -8,6 +8,8 @@ import {
REPAIR_CASE_FLOAT_ACTIVATION_DISTANCE, REPAIR_CASE_FLOAT_ACTIVATION_DISTANCE,
REPAIR_CASE_FLOAT_DOWN_SPEED, REPAIR_CASE_FLOAT_DOWN_SPEED,
REPAIR_CASE_FLOAT_HEIGHT, REPAIR_CASE_FLOAT_HEIGHT,
REPAIR_CASE_EXIT_DURATION,
REPAIR_CASE_EXIT_Y_OFFSET,
REPAIR_CASE_FLOAT_UP_SPEED, REPAIR_CASE_FLOAT_UP_SPEED,
REPAIR_CASE_LID_NODE_NAME, REPAIR_CASE_LID_NODE_NAME,
REPAIR_CASE_OPEN_ROTATION_OFFSET_DEGREES, REPAIR_CASE_OPEN_ROTATION_OFFSET_DEGREES,
@@ -24,6 +26,8 @@ import { toVector3Scale } from "@/utils/three/scale";
interface RepairCaseModelProps extends ModelTransformProps { interface RepairCaseModelProps extends ModelTransformProps {
modelPath: string; modelPath: string;
open: boolean; open: boolean;
exiting?: boolean;
onExitComplete?: (() => void) | undefined;
} }
const CASE_CLOSED_ROTATION_OFFSET_Z = THREE.MathUtils.degToRad( const CASE_CLOSED_ROTATION_OFFSET_Z = THREE.MathUtils.degToRad(
@@ -39,6 +43,8 @@ const ROTATION_AMPLITUDE = THREE.MathUtils.degToRad(
export function RepairCaseModel({ export function RepairCaseModel({
modelPath, modelPath,
open, open,
exiting = false,
onExitComplete,
position = [0, 0, 0], position = [0, 0, 0],
rotation = [0, 0, 0], rotation = [0, 0, 0],
scale = 1, scale = 1,
@@ -58,10 +64,15 @@ export function RepairCaseModel({
const animationActiveRef = useRef(false); const animationActiveRef = useRef(false);
const phase = useRef({ x: 0, y: 0, z: 0 }); const phase = useRef({ x: 0, y: 0, z: 0 });
const pop = useRef({ scale: 0.001, yOffset: REPAIR_CASE_POP_Y_OFFSET }); const pop = useRef({ scale: 0.001, yOffset: REPAIR_CASE_POP_Y_OFFSET });
const onExitCompleteRef = useRef(onExitComplete);
const initialOpen = useRef(open); const initialOpen = useRef(open);
const openedRotationZ = useRef(0); const openedRotationZ = useRef(0);
const parsedScale = toVector3Scale(scale); const parsedScale = toVector3Scale(scale);
useEffect(() => {
onExitCompleteRef.current = onExitComplete;
}, [onExitComplete]);
useEffect(() => { useEffect(() => {
const popAnimation = pop.current; 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(() => { useEffect(() => {
const lid = model.getObjectByName(REPAIR_CASE_LID_NODE_NAME); const lid = model.getObjectByName(REPAIR_CASE_LID_NODE_NAME);
lidRef.current = lid ?? null; lidRef.current = lid ?? null;
@@ -122,6 +153,7 @@ export function RepairCaseModel({
group.getWorldPosition(worldPosition.current); group.getWorldPosition(worldPosition.current);
const isNear = const isNear =
!exiting &&
worldPosition.current.distanceTo(camera.position) <= worldPosition.current.distanceTo(camera.position) <=
REPAIR_CASE_FLOAT_ACTIVATION_DISTANCE; REPAIR_CASE_FLOAT_ACTIVATION_DISTANCE;
const targetHeight = isNear ? REPAIR_CASE_FLOAT_HEIGHT : 0; const targetHeight = isNear ? REPAIR_CASE_FLOAT_HEIGHT : 0;
@@ -1,5 +1,7 @@
import { useState } from "react";
import { RepairObjectModel } from "@/components/three/gameplay/RepairObjectModel"; import { RepairObjectModel } from "@/components/three/gameplay/RepairObjectModel";
import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo"; import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo";
import { RepairMissionCase } from "@/components/three/gameplay/RepairMissionCase";
import { TriggerObject } from "@/components/three/interaction/TriggerObject"; import { TriggerObject } from "@/components/three/interaction/TriggerObject";
import type { RepairMissionConfig } from "@/data/gameplay/repairMissions"; import type { RepairMissionConfig } from "@/data/gameplay/repairMissions";
@@ -12,19 +14,28 @@ export function RepairCompletionStep({
config, config,
onComplete, onComplete,
}: RepairCompletionStepProps): React.JSX.Element { }: RepairCompletionStepProps): React.JSX.Element {
const [isCompleting, setIsCompleting] = useState(false);
return ( return (
<group> <group>
<RepairMissionCase
config={config}
exiting={isCompleting}
onExitComplete={onComplete}
/>
<RepairObjectModel <RepairObjectModel
label={config.label} label={config.label}
modelPath={config.modelPath} modelPath={config.modelPath}
scale={1} scale={1}
/> />
{!isCompleting ? (
<TriggerObject <TriggerObject
position={[0, 1.1, 0]} position={[0, 1.1, 0]}
colliders="ball" colliders="ball"
label={`Valider ${config.label}`} label={`Valider ${config.label}`}
onTrigger={onComplete} onTrigger={() => setIsCompleting(true)}
> >
<mesh> <mesh>
<torusGeometry args={[1.35, 0.045, 12, 96]} /> <torusGeometry args={[1.35, 0.045, 12, 96]} />
@@ -35,8 +46,11 @@ export function RepairCompletionStep({
<meshBasicMaterial color="#bbf7d0" transparent opacity={0.3} /> <meshBasicMaterial color="#bbf7d0" transparent opacity={0.3} />
</mesh> </mesh>
</TriggerObject> </TriggerObject>
) : null}
{!isCompleting ? (
<RepairPromptVideo src={config.stageUiPath} position={[0, 2.55, 0]} /> <RepairPromptVideo src={config.stageUiPath} position={[0, 2.55, 0]} />
) : null}
</group> </group>
); );
} }
@@ -5,12 +5,16 @@ import type { RepairMissionConfig } from "@/data/gameplay/repairMissions";
interface RepairMissionCaseProps { interface RepairMissionCaseProps {
config: RepairMissionConfig; config: RepairMissionConfig;
exiting?: boolean;
onExitComplete?: (() => void) | undefined;
open?: boolean; open?: boolean;
showFragmentationPrompt?: boolean; showFragmentationPrompt?: boolean;
} }
export function RepairMissionCase({ export function RepairMissionCase({
config, config,
exiting = false,
onExitComplete,
open = false, open = false,
showFragmentationPrompt = false, showFragmentationPrompt = false,
}: RepairMissionCaseProps): React.JSX.Element { }: RepairMissionCaseProps): React.JSX.Element {
@@ -18,12 +22,14 @@ export function RepairMissionCase({
<group> <group>
<RepairCaseModel <RepairCaseModel
modelPath={REPAIR_CASE_MODEL_PATH} modelPath={REPAIR_CASE_MODEL_PATH}
exiting={exiting}
onExitComplete={onExitComplete}
open={open} open={open}
position={config.case.position} position={config.case.position}
rotation={config.case.rotation} rotation={config.case.rotation}
scale={config.case.scale} scale={config.case.scale}
/> />
{showFragmentationPrompt ? ( {showFragmentationPrompt && !exiting ? (
<RepairPromptVideo <RepairPromptVideo
src={config.interactUiPath} src={config.interactUiPath}
position={[config.case.position[0], 2.4, config.case.position[2]]} position={[config.case.position[0], 2.4, config.case.position[2]]}
+1 -1
View File
@@ -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 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 ## Audio
+2
View File
@@ -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_ANIMATION_DURATION = 0.8;
export const REPAIR_CASE_POP_DURATION = 0.45; export const REPAIR_CASE_POP_DURATION = 0.45;
export const REPAIR_CASE_POP_Y_OFFSET = -0.25; 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_ACTIVATION_DISTANCE = 5;
export const REPAIR_CASE_FLOAT_HEIGHT = 1; export const REPAIR_CASE_FLOAT_HEIGHT = 1;