diff --git a/src/components/three/gameplay/RepairCaseModel.tsx b/src/components/three/gameplay/RepairCaseModel.tsx index 099cf5d..5e7bd1f 100644 --- a/src/components/three/gameplay/RepairCaseModel.tsx +++ b/src/components/three/gameplay/RepairCaseModel.tsx @@ -81,6 +81,7 @@ export function RepairCaseModel({ const initialOpen = useRef(open); const openedRotationZ = useRef(0); const parsedScale = toVector3Scale(scale); + const placeholderNodes = useRef([]); const placeholderSignature = useRef("__initial__"); const placeholderPosition = useRef(new THREE.Vector3()); const placeholderLocalPosition = useRef(new THREE.Vector3()); @@ -138,6 +139,15 @@ export function RepairCaseModel({ const lid = model.getObjectByName(REPAIR_CASE_LID_NODE_NAME); lidRef.current = lid ?? null; openedRotationZ.current = lid?.rotation.z ?? 0; + placeholderNodes.current = []; + + model.traverse((child) => { + if ( + child.name.toLowerCase().startsWith(REPAIR_CASE_PLACEHOLDER_NAME_PREFIX) + ) { + placeholderNodes.current.push(child); + } + }); if (lid) { lid.rotation.z = @@ -195,41 +205,35 @@ export function RepairCaseModel({ parsedScale[2] * pop.current.scale, ); - const placeholders: RepairCasePlaceholder[] = []; - model.traverse((child) => { - if ( - !child.name - .toLowerCase() - .startsWith(REPAIR_CASE_PLACEHOLDER_NAME_PREFIX) - ) { - return; - } - - child.getWorldPosition(placeholderPosition.current); - placeholderLocalPosition.current.copy(placeholderPosition.current); - group.parent?.worldToLocal(placeholderLocalPosition.current); - placeholders.push({ - name: child.name, - position: [ - placeholderLocalPosition.current.x, - placeholderLocalPosition.current.y, - placeholderLocalPosition.current.z, - ], + if (placeholderNodes.current.length > 0) { + const placeholders: RepairCasePlaceholder[] = []; + placeholderNodes.current.forEach((child) => { + child.getWorldPosition(placeholderPosition.current); + placeholderLocalPosition.current.copy(placeholderPosition.current); + group.parent?.worldToLocal(placeholderLocalPosition.current); + placeholders.push({ + name: child.name, + position: [ + placeholderLocalPosition.current.x, + placeholderLocalPosition.current.y, + placeholderLocalPosition.current.z, + ], + }); }); - }); - placeholders.sort((a, b) => a.name.localeCompare(b.name)); + placeholders.sort((a, b) => a.name.localeCompare(b.name)); - const nextSignature = placeholders - .map( - (placeholder) => - `${placeholder.name}:${placeholder.position - .map((value) => value.toFixed(3)) - .join(",")}`, - ) - .join("|"); - if (nextSignature !== placeholderSignature.current) { - placeholderSignature.current = nextSignature; - onPlaceholdersChangeRef.current?.(placeholders); + const nextSignature = placeholders + .map( + (placeholder) => + `${placeholder.name}:${placeholder.position + .map((value) => value.toFixed(3)) + .join(",")}`, + ) + .join("|"); + if (nextSignature !== placeholderSignature.current) { + placeholderSignature.current = nextSignature; + onPlaceholdersChangeRef.current?.(placeholders); + } } animationActiveRef.current = isNear; diff --git a/src/components/three/gameplay/RepairRepairingStep.tsx b/src/components/three/gameplay/RepairRepairingStep.tsx index 166da80..4c2539b 100644 --- a/src/components/three/gameplay/RepairRepairingStep.tsx +++ b/src/components/three/gameplay/RepairRepairingStep.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useRef, useState } from "react"; import * as THREE from "three"; import type { RepairCasePlaceholder } from "@/components/three/gameplay/RepairCaseModel"; import { RepairObjectModel } from "@/components/three/gameplay/RepairObjectModel"; @@ -56,6 +56,8 @@ export function RepairRepairingStep({ placeholders, onRepair, }: RepairRepairingStepProps): React.JSX.Element { + const groupRef = useRef(null); + const localPosition = useRef(new THREE.Vector3()); const [placedPartIds, setPlacedPartIds] = useState>( {}, ); @@ -97,16 +99,19 @@ export function RepairRepairingStep({ const installLabel = isReadyToInstall ? `Installer ${requiredReplacementLabel}` : hasWrongPartPlaced - ? `Mauvaise piece` + ? `Mauvaise pièce` : hasCorrectPartPlaced - ? `Ranger piece cassee` + ? `Ranger pièce cassée` : `Approcher ${requiredReplacementLabel}`; function handleReplacementPosition( partId: string, position: THREE.Vector3, ): void { - const isPlaced = isNearPlaceholder(position, placeholderPositions); + const isPlaced = isNearPlaceholder( + getStepLocalPosition(position, groupRef.current, localPosition.current), + placeholderPositions, + ); setPlacedPartIds((current) => { if (!current[partId] || isPlaced) return current; @@ -127,7 +132,10 @@ export function RepairRepairingStep({ position: THREE.Vector3, targets: readonly Vector3Tuple[], ): void { - const isDeposited = isNearPlaceholder(position, targets); + const isDeposited = isNearPlaceholder( + getStepLocalPosition(position, groupRef.current, localPosition.current), + targets, + ); setDepositedBrokenPartIds((current) => { if (!current[partId] || isDeposited) return current; @@ -144,7 +152,7 @@ export function RepairRepairingStep({ } return ( - + state.camera); const { hands } = useHandTrackingSnapshot(); + const spaceRef = useRef(null); const groupRef = useRef(null); const rbRef = useRef(null); const isHolding = useRef(false); @@ -160,17 +162,24 @@ export function GrabbableObject({ _currentPos.set(translation.x, translation.y, translation.z); let nearestTarget: Vector3Tuple | null = null; + let nearestTargetWorld: Vector3Tuple | null = null; let nearestDistance = snapRadius; snapTargets.forEach((target) => { - _snapPosition.set(target[0], target[1], target[2]); - const distance = _currentPos.distanceTo(_snapPosition); + _snapTargetWorldPosition.set(target[0], target[1], target[2]); + spaceRef.current?.localToWorld(_snapTargetWorldPosition); + const distance = _currentPos.distanceTo(_snapTargetWorldPosition); if (distance <= nearestDistance) { nearestDistance = distance; nearestTarget = target; + nearestTargetWorld = [ + _snapTargetWorldPosition.x, + _snapTargetWorldPosition.y, + _snapTargetWorldPosition.z, + ]; } }); - if (!nearestTarget) return; + if (!nearestTarget || !nearestTargetWorld) return; snapTween.current?.kill(); const animatedPosition = { @@ -182,9 +191,9 @@ export function GrabbableObject({ body.setLinvel({ x: 0, y: 0, z: 0 }, true); body.setAngvel(ZERO_ANGULAR_VELOCITY, true); snapTween.current = gsap.to(animatedPosition, { - x: nearestTarget[0], - y: nearestTarget[1], - z: nearestTarget[2], + x: nearestTargetWorld[0], + y: nearestTargetWorld[1], + z: nearestTargetWorld[2], duration: snapDuration, ease: "power2.out", onUpdate: () => { @@ -311,43 +320,45 @@ export function GrabbableObject({ }); return ( - - - { - isHolding.current = true; - }} - onRelease={() => { - isHolding.current = false; - snapToNearestTarget(); - if ( - !rbRef.current || - grabDebugParams.throwBoost === GRAB_THROW_BOOST_DEFAULT - ) - return; - const v = rbRef.current.linvel(); - rbRef.current.setLinvel( - { - x: v.x * grabDebugParams.throwBoost, - y: v.y * grabDebugParams.throwBoost, - z: v.z * grabDebugParams.throwBoost, - }, - true, - ); - }} - > - {children} - - - + + + + { + isHolding.current = true; + }} + onRelease={() => { + isHolding.current = false; + snapToNearestTarget(); + if ( + !rbRef.current || + grabDebugParams.throwBoost === GRAB_THROW_BOOST_DEFAULT + ) + return; + const v = rbRef.current.linvel(); + rbRef.current.setLinvel( + { + x: v.x * grabDebugParams.throwBoost, + y: v.y * grabDebugParams.throwBoost, + z: v.z * grabDebugParams.throwBoost, + }, + true, + ); + }} + > + {children} + + + + ); } diff --git a/src/components/three/interaction/InteractableObject.tsx b/src/components/three/interaction/InteractableObject.tsx index 21264fe..a7bf36e 100644 --- a/src/components/three/interaction/InteractableObject.tsx +++ b/src/components/three/interaction/InteractableObject.tsx @@ -148,6 +148,8 @@ export function InteractableObject( if (bodyRef?.current) { const t = bodyRef.current.translation(); _objectPos.set(t.x, t.y, t.z); + } else if (group) { + group.getWorldPosition(_objectPos); } else { _objectPos.set(...position); } diff --git a/src/components/three/interaction/TriggerObject.tsx b/src/components/three/interaction/TriggerObject.tsx index 77f4cc6..a5880c3 100644 --- a/src/components/three/interaction/TriggerObject.tsx +++ b/src/components/three/interaction/TriggerObject.tsx @@ -1,5 +1,6 @@ -import { useState } from "react"; +import { useRef, useState } from "react"; import { RigidBody } from "@react-three/rapier"; +import type { RapierRigidBody } from "@react-three/rapier"; import { InteractableObject } from "@/components/three/interaction/InteractableObject"; import { useClonedObject } from "@/hooks/three/useClonedObject"; import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF"; @@ -59,14 +60,21 @@ export function TriggerObject({ onTrigger, }: TriggerObjectProps): React.JSX.Element { const [spawned, setSpawned] = useState([]); + const rbRef = useRef(null); return ( <> - + { if (soundPath) { AudioManager.getInstance().playSound(soundPath, soundVolume); diff --git a/src/world/debug/TestMap.tsx b/src/world/debug/TestMap.tsx index 1c2576e..5733f66 100644 --- a/src/world/debug/TestMap.tsx +++ b/src/world/debug/TestMap.tsx @@ -142,9 +142,11 @@ export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element { {TEST_SCENE_REPAIR_ZONES.map((zone) => ( - - - + + + + + ))}