diff --git a/src/components/three/gameplay/RepairRepairingStep.tsx b/src/components/three/gameplay/RepairRepairingStep.tsx index a2ca839..8b30064 100644 --- a/src/components/three/gameplay/RepairRepairingStep.tsx +++ b/src/components/three/gameplay/RepairRepairingStep.tsx @@ -80,8 +80,8 @@ export function RepairRepairingStep({ useState(false); const replacementParts = getReplacementParts(config); const brokenPartsToDeposit = getBrokenPartsToDeposit(config, brokenParts); - const requiredReplacementPart = replacementParts.find( - (part) => part.id === config.requiredReplacementPartId, + const requiredReplacementPart = replacementParts.find((part) => + config.requiredReplacementPartIds.includes(part.id), ); const requiredReplacementLabel = requiredReplacementPart?.label ?? config.label; @@ -89,15 +89,16 @@ export function RepairRepairingStep({ const placeholderPositions = placeholderTargets.map( (target) => target.position, ); - const hasCorrectPartPlaced = Boolean( - placedPartIds[config.requiredReplacementPartId], + const hasCorrectPartPlaced = config.requiredReplacementPartIds.some( + (id) => placedPartIds[id], ); const hasDepositedBrokenParts = brokenPartsToDeposit.every( (part) => depositedBrokenPartIds[part.id], ); const hasWrongPartPlaced = replacementParts.some( (part) => - part.id !== config.requiredReplacementPartId && placedPartIds[part.id], + !config.requiredReplacementPartIds.includes(part.id) && + placedPartIds[part.id], ); const isReadyToInstall = hasCorrectPartPlaced && hasDepositedBrokenParts; const installColor = isReadyToInstall @@ -198,7 +199,7 @@ export function RepairRepairingStep({ const isPlaced = Boolean(placedPartIds[part.id]); const feedbackState = getReplacementFeedbackState( part.id, - config.requiredReplacementPartId, + config.requiredReplacementPartIds, isPlaced, ); @@ -387,12 +388,12 @@ function getPlacementFeedbackColor( function getReplacementFeedbackState( partId: string, - requiredPartId: string, + requiredPartIds: readonly string[], isPlaced: boolean, ): RepairPartPlacementFeedbackProps["state"] { if (!isPlaced) return null; - return partId === requiredPartId ? "valid" : "invalid"; + return requiredPartIds.includes(partId) ? "valid" : "invalid"; } function getPlaceholderTargets( @@ -466,9 +467,12 @@ function getReplacementParts( ): readonly RepairMissionPartConfig[] { if (config.replacementParts.length > 0) return config.replacementParts; + const fallbackId = + config.requiredReplacementPartIds[0] ?? `${config.id}-replacement`; + return [ { - id: config.requiredReplacementPartId, + id: fallbackId, label: config.label, modelPath: config.modelPath, }, diff --git a/src/data/gameplay/repairMissions.ts b/src/data/gameplay/repairMissions.ts index fb676db..261d29a 100644 --- a/src/data/gameplay/repairMissions.ts +++ b/src/data/gameplay/repairMissions.ts @@ -25,7 +25,7 @@ export const REPAIR_MISSIONS: Record = { interactUiPath: REPAIR_INTERACT_UI_PATH, brokenUiPath: REPAIR_BROKEN_UI_PATH, case: DEFAULT_REPAIR_CASE, - requiredReplacementPartId: "ebike-cooling-core-replacement", + requiredReplacementPartIds: ["ebike-cooling-core-replacement"], brokenParts: [ { id: "ebike-cooling-core", @@ -59,7 +59,7 @@ export const REPAIR_MISSIONS: Record = { brokenUiPath: REPAIR_BROKEN_UI_PATH, case: DEFAULT_REPAIR_CASE, reassemblySeconds: 1.8, - requiredReplacementPartId: "pylon-grid-relay-replacement", + requiredReplacementPartIds: ["pylon-grid-relay-replacement"], scanPartSeconds: 1.4, brokenParts: [ { @@ -104,7 +104,7 @@ export const REPAIR_MISSIONS: Record = { brokenUiPath: REPAIR_BROKEN_UI_PATH, case: DEFAULT_REPAIR_CASE, reassemblySeconds: 1.2, - requiredReplacementPartId: "farm-irrigation-pump-replacement", + requiredReplacementPartIds: ["farm-irrigation-pump-replacement"], scanPartSeconds: 0.9, brokenParts: [ { diff --git a/src/types/gameplay/repairMission.ts b/src/types/gameplay/repairMission.ts index 17163f5..336a24e 100644 --- a/src/types/gameplay/repairMission.ts +++ b/src/types/gameplay/repairMission.ts @@ -3,6 +3,7 @@ import type { Vector3Scale, Vector3Tuple, } from "@/types/three/three"; +import type { RepairCasePartAnchorName } from "@/data/gameplay/repairCaseConfig"; export const REPAIR_MISSION_IDS = ["ebike", "pylon", "farm"] as const; @@ -24,7 +25,28 @@ export interface RepairMissionPartConfig { id: string; label: string; nodeName?: string; + /** + * Name of a node inside the broken model where this part should snap on + * install. Used by replacement parts that target a slot in the broken + * model itself (e.g. pylon cable installs at the world-position of the + * pylon's `cable2` node), and by broken parts that should spawn at their + * original location on the broken model rather than a static offset. + */ + targetNodeName?: string; caseSlotName?: string; + /** + * Anchor name in the packderelance case where this replacement part is + * visually injected. When set, the part spawns at the world-position of + * that anchor instead of a generic placeholder slot. + */ + caseAnchor?: RepairCasePartAnchorName; + /** + * Group identifier for mutually exclusive replacement parts (e.g. pylon + * cables: only one cable can be held/installed at a time). When one part + * of the group is held, others in the same group are visually ghosted + * and non-interactive. + */ + caseLockGroup?: string; modelPath?: string; } @@ -33,6 +55,7 @@ export interface RepairScannedBrokenPart { label: string; modelPath: string; caseSlotName?: string; + targetNodeName?: string; } export interface RepairMissionConfig { @@ -46,7 +69,13 @@ export interface RepairMissionConfig { brokenUiPath: string; case: RepairMissionCaseConfig; reassemblySeconds?: number; - requiredReplacementPartId: string; + /** + * Replacement part IDs accepted as the correct install. Multiple values + * are used when several alternatives are valid (e.g. pylon accepts either + * cable model). Install validation succeeds when any one of these parts + * is snapped into a placeholder slot. + */ + requiredReplacementPartIds: readonly string[]; scanPartSeconds?: number; brokenParts: readonly RepairMissionPartConfig[]; replacementParts: readonly RepairMissionPartConfig[];