From d1bf438465257ddfd0a478ff28078c89785e50c3 Mon Sep 17 00:00:00 2001 From: Tom Boullay Date: Tue, 2 Jun 2026 18:37:12 +0200 Subject: [PATCH] feat(repair): inject ebike + pylon parts at packderelance anchors - Ebike replacement parts: cooling core (correct, anchored at refroidisseur) + four distractors anchored at cabledroit/cablegauche/pucehaut/pucebas. Removes the ad-hoc gant_l/talkie distractors in favor of consistent case-anchored visuals. - Pylon replacement parts: cable1 + cable2 (alternative correct, both with caseLockGroup 'pylon-cable' for upcoming soft-lock) + refroidisseur and two puce distractors anchored to packderelance. - Farm replacement parts kept as-is (caseAnchor undefined falls back to placeholder slot positions for backward compatibility). - RepairGame threads anchors from RepairCaseModel through RepairMissionCase to RepairRepairingStep; replacement-part initial position now resolves to the anchor world position when caseAnchor is set, falling back to the legacy slot index otherwise. --- src/components/three/gameplay/RepairGame.tsx | 9 ++- .../three/gameplay/RepairMissionCase.tsx | 5 ++ .../three/gameplay/RepairRepairingStep.tsx | 11 +++- src/data/gameplay/repairMissions.ts | 64 +++++++++++++++---- 4 files changed, 75 insertions(+), 14 deletions(-) diff --git a/src/components/three/gameplay/RepairGame.tsx b/src/components/three/gameplay/RepairGame.tsx index c6e0a8b..febbaca 100644 --- a/src/components/three/gameplay/RepairGame.tsx +++ b/src/components/three/gameplay/RepairGame.tsx @@ -1,7 +1,10 @@ import { Suspense, useEffect, useMemo, useState } from "react"; import { useGLTF } from "@react-three/drei"; import { ExplodableModel } from "@/components/three/models/ExplodableModel"; -import type { RepairCasePlaceholder } from "@/components/three/gameplay/RepairCaseModel"; +import type { + RepairCasePartAnchors, + RepairCasePlaceholder, +} from "@/components/three/gameplay/RepairCaseModel"; import { RepairCompletionStep } from "@/components/three/gameplay/RepairCompletionStep"; import { RepairInspectionObject } from "@/components/three/gameplay/RepairInspectionObject"; import { RepairMissionCase } from "@/components/three/gameplay/RepairMissionCase"; @@ -63,6 +66,7 @@ export function RepairGame({ const [casePlaceholders, setCasePlaceholders] = useState< readonly RepairCasePlaceholder[] >([]); + const [caseAnchors, setCaseAnchors] = useState({}); const [scannedBrokenParts, setScannedBrokenParts] = useState< readonly RepairScannedBrokenPart[] >([]); @@ -81,6 +85,7 @@ export function RepairGame({ const timeoutId = window.setTimeout(() => { setCasePlaceholders([]); + setCaseAnchors({}); setScannedBrokenParts([]); }, 0); @@ -137,6 +142,7 @@ export function RepairGame({ ) : null} {step === "repairing" ? ( void) | undefined; + onAnchorsChange?: ((anchors: RepairCasePartAnchors) => void) | undefined; onExitComplete?: (() => void) | undefined; open?: boolean; zoomed?: boolean; @@ -30,6 +32,7 @@ export function RepairMissionCase({ config, exiting = false, onPlaceholdersChange, + onAnchorsChange, onExitComplete, open = false, zoomed = false, @@ -57,6 +60,7 @@ export function RepairMissionCase({ exiting={exiting} onExitComplete={onExitComplete} onPlaceholdersChange={onPlaceholdersChange} + onAnchorsChange={onAnchorsChange} open={open} floating={!zoomed} position={modelPosition} @@ -70,6 +74,7 @@ export function RepairMissionCase({ exiting={exiting} onExitComplete={onExitComplete} onPlaceholdersChange={onPlaceholdersChange} + onAnchorsChange={onAnchorsChange} open={open} floating={!zoomed} position={modelPosition} diff --git a/src/components/three/gameplay/RepairRepairingStep.tsx b/src/components/three/gameplay/RepairRepairingStep.tsx index 8b30064..27d20de 100644 --- a/src/components/three/gameplay/RepairRepairingStep.tsx +++ b/src/components/three/gameplay/RepairRepairingStep.tsx @@ -1,6 +1,9 @@ import { useEffect, useRef, useState } from "react"; import * as THREE from "three"; -import type { RepairCasePlaceholder } from "@/components/three/gameplay/RepairCaseModel"; +import type { + RepairCasePartAnchors, + RepairCasePlaceholder, +} from "@/components/three/gameplay/RepairCaseModel"; import { RepairObjectModel } from "@/components/three/gameplay/RepairObjectModel"; import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo"; import { GrabbableObject } from "@/components/three/interaction/GrabbableObject"; @@ -38,6 +41,7 @@ const STORED_BROKEN_PART_COLOR = "#38bdf8"; let hasWarnedMissingPlaceholders = false; interface RepairRepairingStepProps { + anchors?: RepairCasePartAnchors; brokenParts: readonly RepairScannedBrokenPart[]; config: RepairMissionConfig; placeholders: readonly RepairCasePlaceholder[]; @@ -63,6 +67,7 @@ interface RepairPartPlacementFeedbackProps { } export function RepairRepairingStep({ + anchors = {}, brokenParts, config, placeholders, @@ -193,7 +198,11 @@ export function RepairRepairingStep({ {replacementParts.map((part, index) => { + const anchorPosition = part.caseAnchor + ? anchors[part.caseAnchor] + : undefined; const placeholderPosition = + anchorPosition ?? placeholderPositions[index % placeholderPositions.length] ?? placeholderPositions[0]!; const isPlaced = Boolean(placedPartIds[part.id]); diff --git a/src/data/gameplay/repairMissions.ts b/src/data/gameplay/repairMissions.ts index 261d29a..01b4e22 100644 --- a/src/data/gameplay/repairMissions.ts +++ b/src/data/gameplay/repairMissions.ts @@ -38,13 +38,33 @@ export const REPAIR_MISSIONS: Record = { replacementParts: [ { id: "ebike-cooling-core-replacement", - label: "Replacement cooling core", + label: "Refroidisseur", modelPath: "/models/refroidisseur/model.gltf", + caseAnchor: "refroidisseur", }, { - id: "ebike-glove-distractor", - label: "Insulation glove", - modelPath: "/models/gant_l/model.gltf", + id: "ebike-cable-right-distractor", + label: "Câble droit", + modelPath: "/models/cable1/model.gltf", + caseAnchor: "cabledroit", + }, + { + id: "ebike-cable-left-distractor", + label: "Câble gauche", + modelPath: "/models/cable2/model.gltf", + caseAnchor: "cablegauche", + }, + { + id: "ebike-puce-haut-distractor", + label: "Puce haute", + modelPath: "/models/puce/model.gltf", + caseAnchor: "pucehaut", + }, + { + id: "ebike-puce-bas-distractor", + label: "Puce basse", + modelPath: "/models/puce/model.gltf", + caseAnchor: "pucebas", }, ], }, @@ -59,7 +79,10 @@ export const REPAIR_MISSIONS: Record = { brokenUiPath: REPAIR_BROKEN_UI_PATH, case: DEFAULT_REPAIR_CASE, reassemblySeconds: 1.8, - requiredReplacementPartIds: ["pylon-grid-relay-replacement"], + requiredReplacementPartIds: [ + "pylon-cable-right-replacement", + "pylon-cable-left-replacement", + ], scanPartSeconds: 1.4, brokenParts: [ { @@ -77,19 +100,36 @@ export const REPAIR_MISSIONS: Record = { ], replacementParts: [ { - id: "pylon-grid-relay-replacement", - label: "Replacement grid relay", - modelPath: "/models/pylone/model.gltf", + id: "pylon-cable-right-replacement", + label: "Câble droit", + modelPath: "/models/cable1/model.gltf", + caseAnchor: "cabledroit", + caseLockGroup: "pylon-cable", }, { - id: "pylon-stone-distractor", - label: "Stone counterweight", - modelPath: "/models/galet/model.gltf", + id: "pylon-cable-left-replacement", + label: "Câble gauche", + modelPath: "/models/cable2/model.gltf", + caseAnchor: "cablegauche", + caseLockGroup: "pylon-cable", }, { id: "pylon-cooling-distractor", - label: "Cooling core", + label: "Refroidisseur", modelPath: "/models/refroidisseur/model.gltf", + caseAnchor: "refroidisseur", + }, + { + id: "pylon-puce-haut-distractor", + label: "Puce haute", + modelPath: "/models/puce/model.gltf", + caseAnchor: "pucehaut", + }, + { + id: "pylon-puce-bas-distractor", + label: "Puce basse", + modelPath: "/models/puce/model.gltf", + caseAnchor: "pucebas", }, ], },