Feat/repair game #2
@@ -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, `E`, two-fists hold input, exploded model transition, per-part 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, per-part scan visuals, persistent red broken-part markers, multiple grabbable replacement choices, correct-part install validation, and mission completion
|
||||||
|
|
||||||
## Audio
|
## Audio
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ The current user flow is:
|
|||||||
5. The repair case appears near the mission object and can float when the player approaches it.
|
5. The repair case appears near the mission object and can float when the player approaches it.
|
||||||
6. Press `E` or hold both fists closed for one second to move from `inspected` to `fragmented`.
|
6. Press `E` or hold both fists closed for one second to move from `inspected` to `fragmented`.
|
||||||
7. The mission object uses an exploded-model transition, then moves to `scanning`.
|
7. The mission object uses an exploded-model transition, then moves to `scanning`.
|
||||||
8. The scan visual moves across the fragmented model one part at a time.
|
8. The scan visual moves across the fragmented model one part at a time and keeps a red marker on any configured broken part once it has been found.
|
||||||
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.
|
||||||
@@ -33,13 +33,14 @@ 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 and a blue scan visual moves from part to part 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.
|
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 stays 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, 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, case exit animation, 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/RepairBrokenPartHighlight.tsx` renders the red halo and wire marker around detected broken parts during scanning.
|
||||||
- `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, correct-part placement validation, and the install trigger in `repairing`.
|
- `src/components/three/gameplay/RepairRepairingStep.tsx` renders grabbable replacement choices, correct-part placement validation, and the install trigger in `repairing`.
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import { useRef } from "react";
|
||||||
|
import { useFrame } from "@react-three/fiber";
|
||||||
|
import * as THREE from "three";
|
||||||
|
|
||||||
|
interface RepairBrokenPartHighlightProps {
|
||||||
|
target: THREE.Object3D;
|
||||||
|
}
|
||||||
|
|
||||||
|
const _box = new THREE.Box3();
|
||||||
|
const _sphere = new THREE.Sphere();
|
||||||
|
const _worldPosition = new THREE.Vector3();
|
||||||
|
const _localPosition = new THREE.Vector3();
|
||||||
|
|
||||||
|
export function RepairBrokenPartHighlight({
|
||||||
|
target,
|
||||||
|
}: RepairBrokenPartHighlightProps): React.JSX.Element {
|
||||||
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
|
||||||
|
useFrame(({ clock }) => {
|
||||||
|
const group = groupRef.current;
|
||||||
|
if (!group) return;
|
||||||
|
|
||||||
|
_box.setFromObject(target).getBoundingSphere(_sphere);
|
||||||
|
|
||||||
|
_worldPosition.copy(_sphere.center);
|
||||||
|
_localPosition.copy(_worldPosition);
|
||||||
|
group.parent?.worldToLocal(_localPosition);
|
||||||
|
group.position.copy(_localPosition);
|
||||||
|
|
||||||
|
const pulse = 1 + Math.sin(clock.elapsedTime * 5) * 0.08;
|
||||||
|
const radius = Math.max(_sphere.radius, 0.35) * pulse;
|
||||||
|
group.scale.setScalar(radius);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<group ref={groupRef}>
|
||||||
|
<mesh>
|
||||||
|
<sphereGeometry args={[1, 32, 16]} />
|
||||||
|
<meshBasicMaterial color="#ef4444" transparent opacity={0.14} />
|
||||||
|
</mesh>
|
||||||
|
<mesh>
|
||||||
|
<sphereGeometry args={[1.06, 32, 16]} />
|
||||||
|
<meshBasicMaterial
|
||||||
|
color="#ef4444"
|
||||||
|
wireframe
|
||||||
|
transparent
|
||||||
|
opacity={0.65}
|
||||||
|
/>
|
||||||
|
</mesh>
|
||||||
|
<mesh rotation={[Math.PI / 2, 0, 0]}>
|
||||||
|
<torusGeometry args={[1.12, 0.025, 8, 96]} />
|
||||||
|
<meshBasicMaterial color="#dc2626" transparent opacity={0.9} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -1,8 +1,13 @@
|
|||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
|
import * as THREE from "three";
|
||||||
|
import { RepairBrokenPartHighlight } from "@/components/three/gameplay/RepairBrokenPartHighlight";
|
||||||
import { ExplodableModel } from "@/components/three/models/ExplodableModel";
|
import { ExplodableModel } from "@/components/three/models/ExplodableModel";
|
||||||
import { RepairScanVisual } from "@/components/three/gameplay/RepairScanVisual";
|
import { RepairScanVisual } from "@/components/three/gameplay/RepairScanVisual";
|
||||||
import { REPAIR_SCAN_PART_SECONDS } from "@/data/gameplay/repairGameConfig";
|
import { REPAIR_SCAN_PART_SECONDS } from "@/data/gameplay/repairGameConfig";
|
||||||
import type { RepairMissionConfig } from "@/data/gameplay/repairMissions";
|
import type {
|
||||||
|
RepairMissionConfig,
|
||||||
|
RepairMissionPartConfig,
|
||||||
|
} from "@/data/gameplay/repairMissions";
|
||||||
import type { ExplodedPart } from "@/utils/three/ExplodedModel";
|
import type { ExplodedPart } from "@/utils/three/ExplodedModel";
|
||||||
|
|
||||||
interface RepairScanSequenceProps {
|
interface RepairScanSequenceProps {
|
||||||
@@ -17,6 +22,10 @@ export function RepairScanSequence({
|
|||||||
const [parts, setParts] = useState<readonly ExplodedPart[]>([]);
|
const [parts, setParts] = useState<readonly ExplodedPart[]>([]);
|
||||||
const [activePartIndex, setActivePartIndex] = useState(0);
|
const [activePartIndex, setActivePartIndex] = useState(0);
|
||||||
const activePart = parts[activePartIndex];
|
const activePart = parts[activePartIndex];
|
||||||
|
const brokenPartIndexes = getBrokenPartIndexes(parts, config.brokenParts);
|
||||||
|
const visibleBrokenPartIndexes = brokenPartIndexes.filter(
|
||||||
|
(partIndex) => partIndex <= activePartIndex,
|
||||||
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (parts.length === 0) return undefined;
|
if (parts.length === 0) return undefined;
|
||||||
@@ -46,6 +55,55 @@ export function RepairScanSequence({
|
|||||||
onPartsReady={setParts}
|
onPartsReady={setParts}
|
||||||
/>
|
/>
|
||||||
<RepairScanVisual target={activePart?.object} />
|
<RepairScanVisual target={activePart?.object} />
|
||||||
|
{visibleBrokenPartIndexes.map((partIndex) => {
|
||||||
|
const part = parts[partIndex];
|
||||||
|
if (!part) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<RepairBrokenPartHighlight
|
||||||
|
key={part.object.uuid}
|
||||||
|
target={part.object}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBrokenPartIndexes(
|
||||||
|
parts: readonly ExplodedPart[],
|
||||||
|
brokenParts: readonly RepairMissionPartConfig[],
|
||||||
|
): number[] {
|
||||||
|
if (parts.length === 0 || brokenParts.length === 0) return [];
|
||||||
|
|
||||||
|
const matchedIndexes = brokenParts.flatMap((brokenPart) => {
|
||||||
|
const { nodeName } = brokenPart;
|
||||||
|
if (!nodeName) return [];
|
||||||
|
|
||||||
|
const index = parts.findIndex((part) =>
|
||||||
|
objectContainsNodeName(part.object, nodeName),
|
||||||
|
);
|
||||||
|
|
||||||
|
return index >= 0 ? [index] : [];
|
||||||
|
});
|
||||||
|
|
||||||
|
if (matchedIndexes.length > 0) return [...new Set(matchedIndexes)];
|
||||||
|
|
||||||
|
return parts.slice(0, brokenParts.length).map((_, index) => index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function objectContainsNodeName(
|
||||||
|
object: THREE.Object3D,
|
||||||
|
nodeName: string,
|
||||||
|
): boolean {
|
||||||
|
if (object.name === nodeName) return true;
|
||||||
|
|
||||||
|
let found = false;
|
||||||
|
object.traverse((child) => {
|
||||||
|
if (child.name === nodeName) {
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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, touche \`E\`, hold deux poings, transition de modèle explosé, scan visuel par pièce, 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é, scan visuel par pièce, marqueur rouge persistant 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
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user