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:
@@ -1,7 +1,10 @@
|
|||||||
import { Suspense, useEffect, useMemo, useState } from "react";
|
import { Suspense, useEffect, useMemo, useState } from "react";
|
||||||
import { useGLTF } from "@react-three/drei";
|
import { useGLTF } from "@react-three/drei";
|
||||||
import { ExplodableModel } from "@/components/three/models/ExplodableModel";
|
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 { RepairCompletionStep } from "@/components/three/gameplay/RepairCompletionStep";
|
||||||
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";
|
||||||
@@ -63,6 +66,7 @@ export function RepairGame({
|
|||||||
const [casePlaceholders, setCasePlaceholders] = useState<
|
const [casePlaceholders, setCasePlaceholders] = useState<
|
||||||
readonly RepairCasePlaceholder[]
|
readonly RepairCasePlaceholder[]
|
||||||
>([]);
|
>([]);
|
||||||
|
const [caseAnchors, setCaseAnchors] = useState<RepairCasePartAnchors>({});
|
||||||
const [scannedBrokenParts, setScannedBrokenParts] = useState<
|
const [scannedBrokenParts, setScannedBrokenParts] = useState<
|
||||||
readonly RepairScannedBrokenPart[]
|
readonly RepairScannedBrokenPart[]
|
||||||
>([]);
|
>([]);
|
||||||
@@ -81,6 +85,7 @@ export function RepairGame({
|
|||||||
|
|
||||||
const timeoutId = window.setTimeout(() => {
|
const timeoutId = window.setTimeout(() => {
|
||||||
setCasePlaceholders([]);
|
setCasePlaceholders([]);
|
||||||
|
setCaseAnchors({});
|
||||||
setScannedBrokenParts([]);
|
setScannedBrokenParts([]);
|
||||||
}, 0);
|
}, 0);
|
||||||
|
|
||||||
@@ -137,6 +142,7 @@ export function RepairGame({
|
|||||||
) : null}
|
) : null}
|
||||||
{step === "repairing" ? (
|
{step === "repairing" ? (
|
||||||
<RepairRepairingStep
|
<RepairRepairingStep
|
||||||
|
anchors={caseAnchors}
|
||||||
brokenParts={scannedBrokenParts}
|
brokenParts={scannedBrokenParts}
|
||||||
config={config}
|
config={config}
|
||||||
placeholders={casePlaceholders}
|
placeholders={casePlaceholders}
|
||||||
@@ -159,6 +165,7 @@ export function RepairGame({
|
|||||||
<RepairMissionCase
|
<RepairMissionCase
|
||||||
config={config}
|
config={config}
|
||||||
onPlaceholdersChange={setCasePlaceholders}
|
onPlaceholdersChange={setCasePlaceholders}
|
||||||
|
onAnchorsChange={setCaseAnchors}
|
||||||
open={step === "repairing"}
|
open={step === "repairing"}
|
||||||
zoomed={step === "repairing"}
|
zoomed={step === "repairing"}
|
||||||
showFragmentationPrompt={readyForFragmentation}
|
showFragmentationPrompt={readyForFragmentation}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
RepairCaseModel,
|
RepairCaseModel,
|
||||||
|
type RepairCasePartAnchors,
|
||||||
type RepairCasePlaceholder,
|
type RepairCasePlaceholder,
|
||||||
} from "@/components/three/gameplay/RepairCaseModel";
|
} from "@/components/three/gameplay/RepairCaseModel";
|
||||||
import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo";
|
import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo";
|
||||||
@@ -19,6 +20,7 @@ interface RepairMissionCaseProps {
|
|||||||
onPlaceholdersChange?:
|
onPlaceholdersChange?:
|
||||||
| ((placeholders: readonly RepairCasePlaceholder[]) => void)
|
| ((placeholders: readonly RepairCasePlaceholder[]) => void)
|
||||||
| undefined;
|
| undefined;
|
||||||
|
onAnchorsChange?: ((anchors: RepairCasePartAnchors) => void) | undefined;
|
||||||
onExitComplete?: (() => void) | undefined;
|
onExitComplete?: (() => void) | undefined;
|
||||||
open?: boolean;
|
open?: boolean;
|
||||||
zoomed?: boolean;
|
zoomed?: boolean;
|
||||||
@@ -30,6 +32,7 @@ export function RepairMissionCase({
|
|||||||
config,
|
config,
|
||||||
exiting = false,
|
exiting = false,
|
||||||
onPlaceholdersChange,
|
onPlaceholdersChange,
|
||||||
|
onAnchorsChange,
|
||||||
onExitComplete,
|
onExitComplete,
|
||||||
open = false,
|
open = false,
|
||||||
zoomed = false,
|
zoomed = false,
|
||||||
@@ -57,6 +60,7 @@ export function RepairMissionCase({
|
|||||||
exiting={exiting}
|
exiting={exiting}
|
||||||
onExitComplete={onExitComplete}
|
onExitComplete={onExitComplete}
|
||||||
onPlaceholdersChange={onPlaceholdersChange}
|
onPlaceholdersChange={onPlaceholdersChange}
|
||||||
|
onAnchorsChange={onAnchorsChange}
|
||||||
open={open}
|
open={open}
|
||||||
floating={!zoomed}
|
floating={!zoomed}
|
||||||
position={modelPosition}
|
position={modelPosition}
|
||||||
@@ -70,6 +74,7 @@ export function RepairMissionCase({
|
|||||||
exiting={exiting}
|
exiting={exiting}
|
||||||
onExitComplete={onExitComplete}
|
onExitComplete={onExitComplete}
|
||||||
onPlaceholdersChange={onPlaceholdersChange}
|
onPlaceholdersChange={onPlaceholdersChange}
|
||||||
|
onAnchorsChange={onAnchorsChange}
|
||||||
open={open}
|
open={open}
|
||||||
floating={!zoomed}
|
floating={!zoomed}
|
||||||
position={modelPosition}
|
position={modelPosition}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useRef, useState } from "react";
|
||||||
import * as THREE from "three";
|
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 { RepairObjectModel } from "@/components/three/gameplay/RepairObjectModel";
|
||||||
import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo";
|
import { RepairPromptVideo } from "@/components/three/gameplay/RepairPromptVideo";
|
||||||
import { GrabbableObject } from "@/components/three/interaction/GrabbableObject";
|
import { GrabbableObject } from "@/components/three/interaction/GrabbableObject";
|
||||||
@@ -38,6 +41,7 @@ const STORED_BROKEN_PART_COLOR = "#38bdf8";
|
|||||||
let hasWarnedMissingPlaceholders = false;
|
let hasWarnedMissingPlaceholders = false;
|
||||||
|
|
||||||
interface RepairRepairingStepProps {
|
interface RepairRepairingStepProps {
|
||||||
|
anchors?: RepairCasePartAnchors;
|
||||||
brokenParts: readonly RepairScannedBrokenPart[];
|
brokenParts: readonly RepairScannedBrokenPart[];
|
||||||
config: RepairMissionConfig;
|
config: RepairMissionConfig;
|
||||||
placeholders: readonly RepairCasePlaceholder[];
|
placeholders: readonly RepairCasePlaceholder[];
|
||||||
@@ -63,6 +67,7 @@ interface RepairPartPlacementFeedbackProps {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function RepairRepairingStep({
|
export function RepairRepairingStep({
|
||||||
|
anchors = {},
|
||||||
brokenParts,
|
brokenParts,
|
||||||
config,
|
config,
|
||||||
placeholders,
|
placeholders,
|
||||||
@@ -193,7 +198,11 @@ export function RepairRepairingStep({
|
|||||||
<RepairPlaceholderMarkers positions={placeholderPositions} />
|
<RepairPlaceholderMarkers positions={placeholderPositions} />
|
||||||
|
|
||||||
{replacementParts.map((part, index) => {
|
{replacementParts.map((part, index) => {
|
||||||
|
const anchorPosition = part.caseAnchor
|
||||||
|
? anchors[part.caseAnchor]
|
||||||
|
: undefined;
|
||||||
const placeholderPosition =
|
const placeholderPosition =
|
||||||
|
anchorPosition ??
|
||||||
placeholderPositions[index % placeholderPositions.length] ??
|
placeholderPositions[index % placeholderPositions.length] ??
|
||||||
placeholderPositions[0]!;
|
placeholderPositions[0]!;
|
||||||
const isPlaced = Boolean(placedPartIds[part.id]);
|
const isPlaced = Boolean(placedPartIds[part.id]);
|
||||||
|
|||||||
@@ -38,13 +38,33 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
|||||||
replacementParts: [
|
replacementParts: [
|
||||||
{
|
{
|
||||||
id: "ebike-cooling-core-replacement",
|
id: "ebike-cooling-core-replacement",
|
||||||
label: "Replacement cooling core",
|
label: "Refroidisseur",
|
||||||
modelPath: "/models/refroidisseur/model.gltf",
|
modelPath: "/models/refroidisseur/model.gltf",
|
||||||
|
caseAnchor: "refroidisseur",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "ebike-glove-distractor",
|
id: "ebike-cable-right-distractor",
|
||||||
label: "Insulation glove",
|
label: "Câble droit",
|
||||||
modelPath: "/models/gant_l/model.gltf",
|
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<RepairMissionId, RepairMissionConfig> = {
|
|||||||
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
brokenUiPath: REPAIR_BROKEN_UI_PATH,
|
||||||
case: DEFAULT_REPAIR_CASE,
|
case: DEFAULT_REPAIR_CASE,
|
||||||
reassemblySeconds: 1.8,
|
reassemblySeconds: 1.8,
|
||||||
requiredReplacementPartIds: ["pylon-grid-relay-replacement"],
|
requiredReplacementPartIds: [
|
||||||
|
"pylon-cable-right-replacement",
|
||||||
|
"pylon-cable-left-replacement",
|
||||||
|
],
|
||||||
scanPartSeconds: 1.4,
|
scanPartSeconds: 1.4,
|
||||||
brokenParts: [
|
brokenParts: [
|
||||||
{
|
{
|
||||||
@@ -77,19 +100,36 @@ export const REPAIR_MISSIONS: Record<RepairMissionId, RepairMissionConfig> = {
|
|||||||
],
|
],
|
||||||
replacementParts: [
|
replacementParts: [
|
||||||
{
|
{
|
||||||
id: "pylon-grid-relay-replacement",
|
id: "pylon-cable-right-replacement",
|
||||||
label: "Replacement grid relay",
|
label: "Câble droit",
|
||||||
modelPath: "/models/pylone/model.gltf",
|
modelPath: "/models/cable1/model.gltf",
|
||||||
|
caseAnchor: "cabledroit",
|
||||||
|
caseLockGroup: "pylon-cable",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "pylon-stone-distractor",
|
id: "pylon-cable-left-replacement",
|
||||||
label: "Stone counterweight",
|
label: "Câble gauche",
|
||||||
modelPath: "/models/galet/model.gltf",
|
modelPath: "/models/cable2/model.gltf",
|
||||||
|
caseAnchor: "cablegauche",
|
||||||
|
caseLockGroup: "pylon-cable",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "pylon-cooling-distractor",
|
id: "pylon-cooling-distractor",
|
||||||
label: "Cooling core",
|
label: "Refroidisseur",
|
||||||
modelPath: "/models/refroidisseur/model.gltf",
|
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",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user