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.
This commit is contained in:
Tom Boullay
2026-06-02 18:37:12 +02:00
parent d2ce990165
commit d1bf438465
4 changed files with 75 additions and 14 deletions
+8 -1
View File
@@ -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<RepairCasePartAnchors>({});
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" ? (
<RepairRepairingStep
anchors={caseAnchors}
brokenParts={scannedBrokenParts}
config={config}
placeholders={casePlaceholders}
@@ -159,6 +165,7 @@ export function RepairGame({
<RepairMissionCase
config={config}
onPlaceholdersChange={setCasePlaceholders}
onAnchorsChange={setCaseAnchors}
open={step === "repairing"}
zoomed={step === "repairing"}
showFragmentationPrompt={readyForFragmentation}
@@ -1,5 +1,6 @@
import {
RepairCaseModel,
type RepairCasePartAnchors,
type RepairCasePlaceholder,
} from "@/components/three/gameplay/RepairCaseModel";
import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo";
@@ -19,6 +20,7 @@ interface RepairMissionCaseProps {
onPlaceholdersChange?:
| ((placeholders: readonly RepairCasePlaceholder[]) => 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}
@@ -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({
<RepairPlaceholderMarkers positions={placeholderPositions} />
{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]);