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, focused repair-case view, case placeholder traversal, snap-to-placeholder placement, `E`, two-fists hold input, exploded model transition, per-part scan visuals, persistent red broken-part markers, centered broken-part UI videos, 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, focused repair-case view, case placeholder traversal, snap-to-placeholder placement, broken-part deposit, `E`, two-fists hold input, exploded model transition, per-part scan visuals, persistent red broken-part markers, centered broken-part UI videos, multiple grabbable replacement choices, correct-part install validation, and mission completion
|
||||||
|
|
||||||
## Audio
|
## Audio
|
||||||
|
|
||||||
|
|||||||
@@ -18,8 +18,9 @@ The current user flow is:
|
|||||||
8. The scan visual moves across the fragmented model one part at a time and keeps a red marker plus the `cassé.webm` prompt centered on any configured broken part once it has been found.
|
8. The scan visual moves across the fragmented model one part at a time and keeps a red marker plus the `cassé.webm` prompt centered on any configured broken part once it has been found.
|
||||||
9. In `repairing`, the case opens in a larger focused view and several grabbable replacement parts appear on the case placeholders.
|
9. In `repairing`, the case opens in a larger focused view and several grabbable replacement parts appear on the case placeholders.
|
||||||
10. Move the correct replacement part close to a placeholder. When released near a placeholder, it snaps into place with a short animation.
|
10. Move the correct replacement part close to a placeholder. When released near a placeholder, it snaps into place with a short animation.
|
||||||
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. Move each scanned broken part into a compatible placeholder so the damaged parts are stored in the case.
|
||||||
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`.
|
12. 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.
|
||||||
|
13. 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,7 +34,7 @@ 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, a blue scan visual moves from part to part, and a red halo/wire marker plus the configured broken UI video stay 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 in a larger focused transform, `RepairCaseModel` traverses the case GLTF for empty nodes named `placeholder_*`, several grabbable replacement parts appear on those placeholder positions, and releasing a part near a placeholder snaps it into place with a short GSAP animation. If the current case asset has no placeholder nodes, the flow keeps using fallback focus positions. 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 plus the configured broken UI video stay 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 in a larger focused transform, `RepairCaseModel` traverses the case GLTF for empty nodes named `placeholder_*`, several grabbable replacement parts appear on those placeholder positions, and releasing a part near a placeholder snaps it into place with a short GSAP animation. Scanned broken parts are also rendered as grabbable objects and must be deposited into a compatible placeholder before the final install target validates. If `brokenParts[].placeholderName` is configured, that broken part snaps only to the matching placeholder; otherwise it can use any available placeholder. If the current case asset has no placeholder nodes, the flow keeps using fallback focus positions. The install target only validates when the configured correct replacement part is placed and all scanned broken parts have been deposited. 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
|
||||||
|
|
||||||
@@ -44,7 +45,7 @@ In `fragmented`, the repair object is rendered with `ExplodableModel`, then auto
|
|||||||
- `src/components/three/gameplay/RepairBrokenPartPrompt.tsx` centers the configured broken UI video on detected broken parts during scanning.
|
- `src/components/three/gameplay/RepairBrokenPartPrompt.tsx` centers the configured broken UI video on 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, placeholder placement markers, snap placement behavior, correct-part placement validation, and the install trigger in `repairing`.
|
- `src/components/three/gameplay/RepairRepairingStep.tsx` renders grabbable replacement choices, grabbable scanned broken parts, placeholder placement markers, snap placement behavior, correct-part and broken-part placement validation, and the install trigger in `repairing`.
|
||||||
- `src/components/three/gameplay/RepairPromptVideo.tsx` renders `.webm` prompts inside the 3D scene.
|
- `src/components/three/gameplay/RepairPromptVideo.tsx` renders `.webm` prompts inside the 3D scene.
|
||||||
- `src/components/three/gameplay/RepairScanSequence.tsx` keeps the exploded model visible and advances the scan from part to part.
|
- `src/components/three/gameplay/RepairScanSequence.tsx` keeps the exploded model visible and advances the scan from part to part.
|
||||||
- `src/components/three/gameplay/RepairScanVisual.tsx` renders the scan halo and scan line around the active part.
|
- `src/components/three/gameplay/RepairScanVisual.tsx` renders the scan halo and scan line around the active part.
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ import { RepairCompletionStep } from "@/components/three/gameplay/RepairCompleti
|
|||||||
import { RepairInspectionObject } from "@/components/three/gameplay/RepairInspectionObject";
|
import { RepairInspectionObject } from "@/components/three/gameplay/RepairInspectionObject";
|
||||||
import { RepairMissionCase } from "@/components/three/gameplay/RepairMissionCase";
|
import { RepairMissionCase } from "@/components/three/gameplay/RepairMissionCase";
|
||||||
import { RepairRepairingStep } from "@/components/three/gameplay/RepairRepairingStep";
|
import { RepairRepairingStep } from "@/components/three/gameplay/RepairRepairingStep";
|
||||||
import { RepairScanSequence } from "@/components/three/gameplay/RepairScanSequence";
|
import {
|
||||||
|
RepairScanSequence,
|
||||||
|
type RepairScannedBrokenPart,
|
||||||
|
} from "@/components/three/gameplay/RepairScanSequence";
|
||||||
import { REPAIR_FRAGMENTATION_SEQUENCE_SECONDS } from "@/data/gameplay/repairGameConfig";
|
import { REPAIR_FRAGMENTATION_SEQUENCE_SECONDS } from "@/data/gameplay/repairGameConfig";
|
||||||
import { REPAIR_MISSIONS } from "@/data/gameplay/repairMissions";
|
import { REPAIR_MISSIONS } from "@/data/gameplay/repairMissions";
|
||||||
import { useRepairFragmentationInput } from "@/hooks/gameplay/useRepairFragmentationInput";
|
import { useRepairFragmentationInput } from "@/hooks/gameplay/useRepairFragmentationInput";
|
||||||
@@ -37,6 +40,9 @@ export function RepairGame({
|
|||||||
const [casePlaceholders, setCasePlaceholders] = useState<
|
const [casePlaceholders, setCasePlaceholders] = useState<
|
||||||
readonly RepairCasePlaceholder[]
|
readonly RepairCasePlaceholder[]
|
||||||
>([]);
|
>([]);
|
||||||
|
const [scannedBrokenParts, setScannedBrokenParts] = useState<
|
||||||
|
readonly RepairScannedBrokenPart[]
|
||||||
|
>([]);
|
||||||
const parsedScale = toVector3Scale(scale);
|
const parsedScale = toVector3Scale(scale);
|
||||||
const readyForFragmentation = step === "inspected";
|
const readyForFragmentation = step === "inspected";
|
||||||
|
|
||||||
@@ -77,11 +83,15 @@ export function RepairGame({
|
|||||||
{step === "scanning" ? (
|
{step === "scanning" ? (
|
||||||
<RepairScanSequence
|
<RepairScanSequence
|
||||||
config={config}
|
config={config}
|
||||||
onComplete={() => setMissionStep(mission, "repairing")}
|
onComplete={(brokenParts) => {
|
||||||
|
setScannedBrokenParts(brokenParts);
|
||||||
|
setMissionStep(mission, "repairing");
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
{step === "repairing" ? (
|
{step === "repairing" ? (
|
||||||
<RepairRepairingStep
|
<RepairRepairingStep
|
||||||
|
brokenParts={scannedBrokenParts}
|
||||||
config={config}
|
config={config}
|
||||||
placeholders={casePlaceholders}
|
placeholders={casePlaceholders}
|
||||||
onRepair={() => setMissionStep(mission, "done")}
|
onRepair={() => setMissionStep(mission, "done")}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
import { useCallback, useState } from "react";
|
import { useState } from "react";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import type { RepairCasePlaceholder } from "@/components/three/gameplay/RepairCaseModel";
|
import type { RepairCasePlaceholder } from "@/components/three/gameplay/RepairCaseModel";
|
||||||
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 type { RepairScannedBrokenPart } from "@/components/three/gameplay/RepairScanSequence";
|
||||||
import { GrabbableObject } from "@/components/three/interaction/GrabbableObject";
|
import { GrabbableObject } from "@/components/three/interaction/GrabbableObject";
|
||||||
import { TriggerObject } from "@/components/three/interaction/TriggerObject";
|
import { TriggerObject } from "@/components/three/interaction/TriggerObject";
|
||||||
import {
|
import {
|
||||||
@@ -23,15 +24,22 @@ const REPLACEMENT_START_OFFSETS: Vector3Tuple[] = [
|
|||||||
[0, 1.05, 0.45],
|
[0, 1.05, 0.45],
|
||||||
[1.15, 1, 0.25],
|
[1.15, 1, 0.25],
|
||||||
];
|
];
|
||||||
|
const BROKEN_PART_START_OFFSETS: Vector3Tuple[] = [
|
||||||
|
[-1.35, 0.55, -0.85],
|
||||||
|
[0, 0.6, -1],
|
||||||
|
[1.35, 0.55, -0.85],
|
||||||
|
];
|
||||||
const REPAIR_INSTALL_RADIUS = 1.1;
|
const REPAIR_INSTALL_RADIUS = 1.1;
|
||||||
|
|
||||||
interface RepairRepairingStepProps {
|
interface RepairRepairingStepProps {
|
||||||
|
brokenParts: readonly RepairScannedBrokenPart[];
|
||||||
config: RepairMissionConfig;
|
config: RepairMissionConfig;
|
||||||
placeholders: readonly RepairCasePlaceholder[];
|
placeholders: readonly RepairCasePlaceholder[];
|
||||||
onRepair: () => void;
|
onRepair: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RepairRepairingStep({
|
export function RepairRepairingStep({
|
||||||
|
brokenParts,
|
||||||
config,
|
config,
|
||||||
placeholders,
|
placeholders,
|
||||||
onRepair,
|
onRepair,
|
||||||
@@ -39,50 +47,82 @@ export function RepairRepairingStep({
|
|||||||
const [placedPartIds, setPlacedPartIds] = useState<Record<string, boolean>>(
|
const [placedPartIds, setPlacedPartIds] = useState<Record<string, boolean>>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
|
const [depositedBrokenPartIds, setDepositedBrokenPartIds] = useState<
|
||||||
|
Record<string, boolean>
|
||||||
|
>({});
|
||||||
const replacementParts = getReplacementParts(config);
|
const replacementParts = getReplacementParts(config);
|
||||||
|
const brokenPartsToDeposit = getBrokenPartsToDeposit(config, brokenParts);
|
||||||
const requiredReplacementPart = replacementParts.find(
|
const requiredReplacementPart = replacementParts.find(
|
||||||
(part) => part.id === config.requiredReplacementPartId,
|
(part) => part.id === config.requiredReplacementPartId,
|
||||||
);
|
);
|
||||||
const requiredReplacementLabel =
|
const requiredReplacementLabel =
|
||||||
requiredReplacementPart?.label ?? config.label;
|
requiredReplacementPart?.label ?? config.label;
|
||||||
const placeholderPositions = getPlaceholderPositions(placeholders);
|
const placeholderTargets = getPlaceholderTargets(placeholders);
|
||||||
|
const placeholderPositions = placeholderTargets.map(
|
||||||
|
(target) => target.position,
|
||||||
|
);
|
||||||
const hasCorrectPartPlaced = Boolean(
|
const hasCorrectPartPlaced = Boolean(
|
||||||
placedPartIds[config.requiredReplacementPartId],
|
placedPartIds[config.requiredReplacementPartId],
|
||||||
);
|
);
|
||||||
|
const hasDepositedBrokenParts = brokenPartsToDeposit.every(
|
||||||
|
(part) => depositedBrokenPartIds[part.id],
|
||||||
|
);
|
||||||
const hasWrongPartPlaced = replacementParts.some(
|
const hasWrongPartPlaced = replacementParts.some(
|
||||||
(part) =>
|
(part) =>
|
||||||
part.id !== config.requiredReplacementPartId && placedPartIds[part.id],
|
part.id !== config.requiredReplacementPartId && placedPartIds[part.id],
|
||||||
);
|
);
|
||||||
const installColor = hasCorrectPartPlaced
|
const isReadyToInstall = hasCorrectPartPlaced && hasDepositedBrokenParts;
|
||||||
|
const installColor = isReadyToInstall
|
||||||
? "#22c55e"
|
? "#22c55e"
|
||||||
: hasWrongPartPlaced
|
: hasWrongPartPlaced
|
||||||
? "#ef4444"
|
? "#ef4444"
|
||||||
: "#f97316";
|
: "#f97316";
|
||||||
const installFillColor = hasCorrectPartPlaced
|
const installFillColor = isReadyToInstall
|
||||||
? "#86efac"
|
? "#86efac"
|
||||||
: hasWrongPartPlaced
|
: hasWrongPartPlaced
|
||||||
? "#fecaca"
|
? "#fecaca"
|
||||||
: "#fed7aa";
|
: "#fed7aa";
|
||||||
|
|
||||||
const handleReplacementPosition = useCallback(
|
function handleReplacementPosition(
|
||||||
(partId: string, position: THREE.Vector3) => {
|
partId: string,
|
||||||
const isPlaced = isNearPlaceholder(position, placeholderPositions);
|
position: THREE.Vector3,
|
||||||
setPlacedPartIds((current) => {
|
): void {
|
||||||
if (!current[partId] || isPlaced) return current;
|
const isPlaced = isNearPlaceholder(position, placeholderPositions);
|
||||||
|
setPlacedPartIds((current) => {
|
||||||
|
if (!current[partId] || isPlaced) return current;
|
||||||
|
|
||||||
return { ...current, [partId]: false };
|
return { ...current, [partId]: false };
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
[placeholderPositions],
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleReplacementSnap = useCallback((partId: string) => {
|
function handleReplacementSnap(partId: string): void {
|
||||||
setPlacedPartIds((current) => {
|
setPlacedPartIds((current) => {
|
||||||
if (current[partId]) return current;
|
if (current[partId]) return current;
|
||||||
|
|
||||||
return { ...current, [partId]: true };
|
return { ...current, [partId]: true };
|
||||||
});
|
});
|
||||||
}, []);
|
}
|
||||||
|
|
||||||
|
function handleBrokenPartPosition(
|
||||||
|
partId: string,
|
||||||
|
position: THREE.Vector3,
|
||||||
|
targets: readonly Vector3Tuple[],
|
||||||
|
): void {
|
||||||
|
const isDeposited = isNearPlaceholder(position, targets);
|
||||||
|
setDepositedBrokenPartIds((current) => {
|
||||||
|
if (!current[partId] || isDeposited) return current;
|
||||||
|
|
||||||
|
return { ...current, [partId]: false };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleBrokenPartSnap(partId: string): void {
|
||||||
|
setDepositedBrokenPartIds((current) => {
|
||||||
|
if (current[partId]) return current;
|
||||||
|
|
||||||
|
return { ...current, [partId]: true };
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
@@ -90,14 +130,16 @@ export function RepairRepairingStep({
|
|||||||
position={INSTALL_TARGET_POSITION}
|
position={INSTALL_TARGET_POSITION}
|
||||||
colliders="ball"
|
colliders="ball"
|
||||||
label={
|
label={
|
||||||
hasCorrectPartPlaced
|
isReadyToInstall
|
||||||
? `Installer ${requiredReplacementLabel}`
|
? `Installer ${requiredReplacementLabel}`
|
||||||
: hasWrongPartPlaced
|
: hasWrongPartPlaced
|
||||||
? `Mauvaise piece`
|
? `Mauvaise piece`
|
||||||
: `Approcher ${requiredReplacementLabel}`
|
: hasCorrectPartPlaced
|
||||||
|
? `Ranger piece cassee`
|
||||||
|
: `Approcher ${requiredReplacementLabel}`
|
||||||
}
|
}
|
||||||
onTrigger={() => {
|
onTrigger={() => {
|
||||||
if (!hasCorrectPartPlaced) return;
|
if (!isReadyToInstall) return;
|
||||||
|
|
||||||
onRepair();
|
onRepair();
|
||||||
}}
|
}}
|
||||||
@@ -158,27 +200,93 @@ export function RepairRepairingStep({
|
|||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
|
||||||
|
{brokenPartsToDeposit.map((part, index) => {
|
||||||
|
const startOffset =
|
||||||
|
BROKEN_PART_START_OFFSETS[index % BROKEN_PART_START_OFFSETS.length] ??
|
||||||
|
BROKEN_PART_START_OFFSETS[0]!;
|
||||||
|
const startPosition: Vector3Tuple = [
|
||||||
|
REPAIR_CASE_FOCUS_POSITION[0] + startOffset[0],
|
||||||
|
REPAIR_CASE_FOCUS_POSITION[1] + startOffset[1],
|
||||||
|
REPAIR_CASE_FOCUS_POSITION[2] + startOffset[2],
|
||||||
|
];
|
||||||
|
const targetPositions = getBrokenPartTargetPositions(
|
||||||
|
part,
|
||||||
|
placeholderTargets,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<GrabbableObject
|
||||||
|
key={part.id}
|
||||||
|
position={startPosition}
|
||||||
|
colliders="ball"
|
||||||
|
handControlled
|
||||||
|
label={`Ranger ${part.label}`}
|
||||||
|
onPositionChange={(position) => {
|
||||||
|
handleBrokenPartPosition(part.id, position, targetPositions);
|
||||||
|
}}
|
||||||
|
onSnap={() => {
|
||||||
|
handleBrokenPartSnap(part.id);
|
||||||
|
}}
|
||||||
|
snapDuration={REPAIR_CASE_PLACEHOLDER_SNAP_DURATION}
|
||||||
|
snapRadius={REPAIR_CASE_PLACEHOLDER_SNAP_RADIUS}
|
||||||
|
snapTargets={targetPositions}
|
||||||
|
>
|
||||||
|
<group>
|
||||||
|
<RepairObjectModel
|
||||||
|
label={part.label}
|
||||||
|
modelPath={part.modelPath}
|
||||||
|
scale={0.24}
|
||||||
|
/>
|
||||||
|
<mesh position={[0, 0.42, 0]}>
|
||||||
|
<sphereGeometry args={[0.11, 16, 16]} />
|
||||||
|
<meshBasicMaterial color="#ef4444" transparent opacity={0.85} />
|
||||||
|
</mesh>
|
||||||
|
</group>
|
||||||
|
</GrabbableObject>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
<RepairPromptVideo src={config.interactUiPath} position={[0, 2.3, 0]} />
|
<RepairPromptVideo src={config.interactUiPath} position={[0, 2.3, 0]} />
|
||||||
</group>
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getPlaceholderPositions(
|
function getPlaceholderTargets(
|
||||||
placeholders: readonly RepairCasePlaceholder[],
|
placeholders: readonly RepairCasePlaceholder[],
|
||||||
): readonly Vector3Tuple[] {
|
): readonly RepairCasePlaceholder[] {
|
||||||
if (placeholders.length > 0) {
|
if (placeholders.length > 0) {
|
||||||
return placeholders.map((placeholder) => placeholder.position);
|
return placeholders;
|
||||||
}
|
}
|
||||||
|
|
||||||
return REPLACEMENT_START_OFFSETS.map(
|
return REPLACEMENT_START_OFFSETS.map(
|
||||||
(offset): Vector3Tuple => [
|
(offset, index): RepairCasePlaceholder => ({
|
||||||
REPAIR_CASE_FOCUS_POSITION[0] + offset[0],
|
name: `fallback_${index + 1}`,
|
||||||
REPAIR_CASE_FOCUS_POSITION[1] + offset[1],
|
position: [
|
||||||
REPAIR_CASE_FOCUS_POSITION[2] + offset[2],
|
REPAIR_CASE_FOCUS_POSITION[0] + offset[0],
|
||||||
],
|
REPAIR_CASE_FOCUS_POSITION[1] + offset[1],
|
||||||
|
REPAIR_CASE_FOCUS_POSITION[2] + offset[2],
|
||||||
|
],
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBrokenPartTargetPositions(
|
||||||
|
part: RepairScannedBrokenPart,
|
||||||
|
placeholderTargets: readonly RepairCasePlaceholder[],
|
||||||
|
): readonly Vector3Tuple[] {
|
||||||
|
if (!part.placeholderName) {
|
||||||
|
return placeholderTargets.map((placeholder) => placeholder.position);
|
||||||
|
}
|
||||||
|
|
||||||
|
const matchingPlaceholder = placeholderTargets.find(
|
||||||
|
(placeholder) => placeholder.name === part.placeholderName,
|
||||||
|
);
|
||||||
|
|
||||||
|
return matchingPlaceholder
|
||||||
|
? [matchingPlaceholder.position]
|
||||||
|
: placeholderTargets.map((placeholder) => placeholder.position);
|
||||||
|
}
|
||||||
|
|
||||||
function isNearPlaceholder(
|
function isNearPlaceholder(
|
||||||
position: THREE.Vector3,
|
position: THREE.Vector3,
|
||||||
placeholderPositions: readonly Vector3Tuple[],
|
placeholderPositions: readonly Vector3Tuple[],
|
||||||
@@ -203,3 +311,17 @@ function getReplacementParts(
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getBrokenPartsToDeposit(
|
||||||
|
config: RepairMissionConfig,
|
||||||
|
brokenParts: readonly RepairScannedBrokenPart[],
|
||||||
|
): readonly RepairScannedBrokenPart[] {
|
||||||
|
if (brokenParts.length > 0) return brokenParts;
|
||||||
|
|
||||||
|
return config.brokenParts.map((part) => ({
|
||||||
|
id: part.id,
|
||||||
|
label: part.label,
|
||||||
|
modelPath: part.modelPath ?? config.modelPath,
|
||||||
|
...(part.placeholderName ? { placeholderName: part.placeholderName } : {}),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,7 +13,14 @@ import type { ExplodedPart } from "@/utils/three/ExplodedModel";
|
|||||||
|
|
||||||
interface RepairScanSequenceProps {
|
interface RepairScanSequenceProps {
|
||||||
config: RepairMissionConfig;
|
config: RepairMissionConfig;
|
||||||
onComplete: () => void;
|
onComplete: (brokenParts: readonly RepairScannedBrokenPart[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RepairScannedBrokenPart {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
modelPath: string;
|
||||||
|
placeholderName?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function RepairScanSequence({
|
export function RepairScanSequence({
|
||||||
@@ -35,7 +42,7 @@ export function RepairScanSequence({
|
|||||||
setActivePartIndex((currentIndex) => {
|
setActivePartIndex((currentIndex) => {
|
||||||
const nextIndex = currentIndex + 1;
|
const nextIndex = currentIndex + 1;
|
||||||
if (nextIndex >= parts.length) {
|
if (nextIndex >= parts.length) {
|
||||||
onComplete();
|
onComplete(getScannedBrokenParts(parts, config));
|
||||||
return currentIndex;
|
return currentIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,7 +53,7 @@ export function RepairScanSequence({
|
|||||||
return () => {
|
return () => {
|
||||||
window.clearTimeout(timeoutId);
|
window.clearTimeout(timeoutId);
|
||||||
};
|
};
|
||||||
}, [activePartIndex, onComplete, parts.length]);
|
}, [activePartIndex, config, onComplete, parts]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group>
|
||||||
@@ -74,6 +81,26 @@ export function RepairScanSequence({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getScannedBrokenParts(
|
||||||
|
parts: readonly ExplodedPart[],
|
||||||
|
config: RepairMissionConfig,
|
||||||
|
): readonly RepairScannedBrokenPart[] {
|
||||||
|
const brokenPartIndexes = getBrokenPartIndexes(parts, config.brokenParts);
|
||||||
|
|
||||||
|
return brokenPartIndexes.map((_, index) => {
|
||||||
|
const configuredPart = config.brokenParts[index] ?? config.brokenParts[0];
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: configuredPart?.id ?? `${config.id}-broken-part-${index}`,
|
||||||
|
label: configuredPart?.label ?? `${config.label} broken part`,
|
||||||
|
modelPath: configuredPart?.modelPath ?? config.modelPath,
|
||||||
|
...(configuredPart?.placeholderName
|
||||||
|
? { placeholderName: configuredPart.placeholderName }
|
||||||
|
: {}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function getBrokenPartIndexes(
|
function getBrokenPartIndexes(
|
||||||
parts: readonly ExplodedPart[],
|
parts: readonly ExplodedPart[],
|
||||||
brokenParts: readonly RepairMissionPartConfig[],
|
brokenParts: readonly RepairMissionPartConfig[],
|
||||||
|
|||||||
@@ -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, vue focalisée de la mallette, traverse des placeholders de mallette, placement avec snap vers placeholder, 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 -> 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
|
||||||
|
|
||||||
## Audio
|
## Audio
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ export interface RepairMissionPartConfig {
|
|||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
nodeName?: string;
|
nodeName?: string;
|
||||||
|
placeholderName?: string;
|
||||||
modelPath?: string;
|
modelPath?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user