add: animation on repair case
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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]]}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user