add: animate repair reassembly

This commit is contained in:
Tom Boullay
2026-05-08 02:40:31 +01:00
parent 1589298b09
commit 41a3846205
12 changed files with 126 additions and 13 deletions
@@ -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 { RepairMissionCase } from "@/components/three/gameplay/RepairMissionCase";
import { RepairRepairingStep } from "@/components/three/gameplay/RepairRepairingStep";
import { RepairReassemblyStep } from "@/components/three/gameplay/RepairReassemblyStep";
import {
RepairScanSequence,
type RepairScannedBrokenPart,
@@ -94,7 +95,13 @@ export function RepairGame({
brokenParts={scannedBrokenParts}
config={config}
placeholders={casePlaceholders}
onRepair={() => setMissionStep(mission, "done")}
onRepair={() => setMissionStep(mission, "reassembling")}
/>
) : null}
{step === "reassembling" ? (
<RepairReassemblyStep
config={config}
onComplete={() => setMissionStep(mission, "done")}
/>
) : null}
{step === "done" ? (
@@ -103,7 +110,7 @@ export function RepairGame({
onComplete={() => completeMission(mission)}
/>
) : null}
{step !== "waiting" && step !== "done" ? (
{step !== "waiting" && step !== "done" && step !== "reassembling" ? (
<RepairMissionCase
config={config}
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",
"scanning",
"repairing",
"reassembling",
"done",
];
+3 -3
View File
@@ -300,7 +300,7 @@ Le store expose :
Les étapes de mission utilisent actuellement cette séquence :
\`\`\`ts
"locked" | "waiting" | "inspected" | "fragmented" | "scanning" | "repairing" | "done"
"locked" | "waiting" | "inspected" | "fragmented" | "scanning" | "repairing" | "reassembling" | "done"
\`\`\`
## 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\` 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 :
@@ -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, 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
+1
View File
@@ -1,3 +1,4 @@
export const REPAIR_FRAGMENTATION_FIST_HOLD_SECONDS = 1;
export const REPAIR_FRAGMENTATION_SEQUENCE_SECONDS = 4;
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"
| "scanning"
| "repairing"
| "reassembling"
| "done";
interface IntroState {
@@ -81,6 +82,8 @@ function getNextMissionStep(step: MissionStep): MissionStep {
case "scanning":
return "repairing";
case "repairing":
return "reassembling";
case "reassembling":
case "done":
return "done";
}
@@ -99,8 +102,10 @@ function getPreviousMissionStep(step: MissionStep): MissionStep {
return "fragmented";
case "repairing":
return "scanning";
case "done":
case "reassembling":
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>([
"inspected",
"repairing",
"reassembling",
"done",
]);