fix repair game interaction coordinate spaces
This commit is contained in:
@@ -81,6 +81,7 @@ export function RepairCaseModel({
|
|||||||
const initialOpen = useRef(open);
|
const initialOpen = useRef(open);
|
||||||
const openedRotationZ = useRef(0);
|
const openedRotationZ = useRef(0);
|
||||||
const parsedScale = toVector3Scale(scale);
|
const parsedScale = toVector3Scale(scale);
|
||||||
|
const placeholderNodes = useRef<THREE.Object3D[]>([]);
|
||||||
const placeholderSignature = useRef("__initial__");
|
const placeholderSignature = useRef("__initial__");
|
||||||
const placeholderPosition = useRef(new THREE.Vector3());
|
const placeholderPosition = useRef(new THREE.Vector3());
|
||||||
const placeholderLocalPosition = 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);
|
const lid = model.getObjectByName(REPAIR_CASE_LID_NODE_NAME);
|
||||||
lidRef.current = lid ?? null;
|
lidRef.current = lid ?? null;
|
||||||
openedRotationZ.current = lid?.rotation.z ?? 0;
|
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) {
|
if (lid) {
|
||||||
lid.rotation.z =
|
lid.rotation.z =
|
||||||
@@ -195,41 +205,35 @@ export function RepairCaseModel({
|
|||||||
parsedScale[2] * pop.current.scale,
|
parsedScale[2] * pop.current.scale,
|
||||||
);
|
);
|
||||||
|
|
||||||
const placeholders: RepairCasePlaceholder[] = [];
|
if (placeholderNodes.current.length > 0) {
|
||||||
model.traverse((child) => {
|
const placeholders: RepairCasePlaceholder[] = [];
|
||||||
if (
|
placeholderNodes.current.forEach((child) => {
|
||||||
!child.name
|
child.getWorldPosition(placeholderPosition.current);
|
||||||
.toLowerCase()
|
placeholderLocalPosition.current.copy(placeholderPosition.current);
|
||||||
.startsWith(REPAIR_CASE_PLACEHOLDER_NAME_PREFIX)
|
group.parent?.worldToLocal(placeholderLocalPosition.current);
|
||||||
) {
|
placeholders.push({
|
||||||
return;
|
name: child.name,
|
||||||
}
|
position: [
|
||||||
|
placeholderLocalPosition.current.x,
|
||||||
child.getWorldPosition(placeholderPosition.current);
|
placeholderLocalPosition.current.y,
|
||||||
placeholderLocalPosition.current.copy(placeholderPosition.current);
|
placeholderLocalPosition.current.z,
|
||||||
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
|
const nextSignature = placeholders
|
||||||
.map(
|
.map(
|
||||||
(placeholder) =>
|
(placeholder) =>
|
||||||
`${placeholder.name}:${placeholder.position
|
`${placeholder.name}:${placeholder.position
|
||||||
.map((value) => value.toFixed(3))
|
.map((value) => value.toFixed(3))
|
||||||
.join(",")}`,
|
.join(",")}`,
|
||||||
)
|
)
|
||||||
.join("|");
|
.join("|");
|
||||||
if (nextSignature !== placeholderSignature.current) {
|
if (nextSignature !== placeholderSignature.current) {
|
||||||
placeholderSignature.current = nextSignature;
|
placeholderSignature.current = nextSignature;
|
||||||
onPlaceholdersChangeRef.current?.(placeholders);
|
onPlaceholdersChangeRef.current?.(placeholders);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
animationActiveRef.current = isNear;
|
animationActiveRef.current = isNear;
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import * as THREE from "three";
|
import * as THREE from "three";
|
||||||
import type { RepairCasePlaceholder } from "@/components/three/gameplay/RepairCaseModel";
|
import type { RepairCasePlaceholder } from "@/components/three/gameplay/RepairCaseModel";
|
||||||
import { RepairObjectModel } from "@/components/three/gameplay/RepairObjectModel";
|
import { RepairObjectModel } from "@/components/three/gameplay/RepairObjectModel";
|
||||||
@@ -56,6 +56,8 @@ export function RepairRepairingStep({
|
|||||||
placeholders,
|
placeholders,
|
||||||
onRepair,
|
onRepair,
|
||||||
}: RepairRepairingStepProps): React.JSX.Element {
|
}: RepairRepairingStepProps): React.JSX.Element {
|
||||||
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
|
const localPosition = useRef(new THREE.Vector3());
|
||||||
const [placedPartIds, setPlacedPartIds] = useState<Record<string, boolean>>(
|
const [placedPartIds, setPlacedPartIds] = useState<Record<string, boolean>>(
|
||||||
{},
|
{},
|
||||||
);
|
);
|
||||||
@@ -97,16 +99,19 @@ export function RepairRepairingStep({
|
|||||||
const installLabel = isReadyToInstall
|
const installLabel = isReadyToInstall
|
||||||
? `Installer ${requiredReplacementLabel}`
|
? `Installer ${requiredReplacementLabel}`
|
||||||
: hasWrongPartPlaced
|
: hasWrongPartPlaced
|
||||||
? `Mauvaise piece`
|
? `Mauvaise pièce`
|
||||||
: hasCorrectPartPlaced
|
: hasCorrectPartPlaced
|
||||||
? `Ranger piece cassee`
|
? `Ranger pièce cassée`
|
||||||
: `Approcher ${requiredReplacementLabel}`;
|
: `Approcher ${requiredReplacementLabel}`;
|
||||||
|
|
||||||
function handleReplacementPosition(
|
function handleReplacementPosition(
|
||||||
partId: string,
|
partId: string,
|
||||||
position: THREE.Vector3,
|
position: THREE.Vector3,
|
||||||
): void {
|
): void {
|
||||||
const isPlaced = isNearPlaceholder(position, placeholderPositions);
|
const isPlaced = isNearPlaceholder(
|
||||||
|
getStepLocalPosition(position, groupRef.current, localPosition.current),
|
||||||
|
placeholderPositions,
|
||||||
|
);
|
||||||
setPlacedPartIds((current) => {
|
setPlacedPartIds((current) => {
|
||||||
if (!current[partId] || isPlaced) return current;
|
if (!current[partId] || isPlaced) return current;
|
||||||
|
|
||||||
@@ -127,7 +132,10 @@ export function RepairRepairingStep({
|
|||||||
position: THREE.Vector3,
|
position: THREE.Vector3,
|
||||||
targets: readonly Vector3Tuple[],
|
targets: readonly Vector3Tuple[],
|
||||||
): void {
|
): void {
|
||||||
const isDeposited = isNearPlaceholder(position, targets);
|
const isDeposited = isNearPlaceholder(
|
||||||
|
getStepLocalPosition(position, groupRef.current, localPosition.current),
|
||||||
|
targets,
|
||||||
|
);
|
||||||
setDepositedBrokenPartIds((current) => {
|
setDepositedBrokenPartIds((current) => {
|
||||||
if (!current[partId] || isDeposited) return current;
|
if (!current[partId] || isDeposited) return current;
|
||||||
|
|
||||||
@@ -144,7 +152,7 @@ export function RepairRepairingStep({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<group>
|
<group ref={groupRef}>
|
||||||
<RepairInstallTarget
|
<RepairInstallTarget
|
||||||
fillColor={installFillColor}
|
fillColor={installFillColor}
|
||||||
isReadyToInstall={isReadyToInstall}
|
isReadyToInstall={isReadyToInstall}
|
||||||
@@ -333,6 +341,17 @@ function isNearPlaceholder(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getStepLocalPosition(
|
||||||
|
worldPosition: THREE.Vector3,
|
||||||
|
group: THREE.Group | null,
|
||||||
|
target: THREE.Vector3,
|
||||||
|
): THREE.Vector3 {
|
||||||
|
target.copy(worldPosition);
|
||||||
|
group?.worldToLocal(target);
|
||||||
|
|
||||||
|
return target;
|
||||||
|
}
|
||||||
|
|
||||||
function getReplacementParts(
|
function getReplacementParts(
|
||||||
config: RepairMissionConfig,
|
config: RepairMissionConfig,
|
||||||
): readonly RepairMissionPartConfig[] {
|
): readonly RepairMissionPartConfig[] {
|
||||||
|
|||||||
@@ -62,6 +62,7 @@ const _handHitDirection = new THREE.Vector3();
|
|||||||
const _cameraPos = new THREE.Vector3();
|
const _cameraPos = new THREE.Vector3();
|
||||||
const _objectPos = new THREE.Vector3();
|
const _objectPos = new THREE.Vector3();
|
||||||
const _snapPosition = new THREE.Vector3();
|
const _snapPosition = new THREE.Vector3();
|
||||||
|
const _snapTargetWorldPosition = new THREE.Vector3();
|
||||||
const _handRaycaster = new THREE.Raycaster();
|
const _handRaycaster = new THREE.Raycaster();
|
||||||
|
|
||||||
const HAND_GRAB_SCREEN_RADIUS = 0.04;
|
const HAND_GRAB_SCREEN_RADIUS = 0.04;
|
||||||
@@ -138,6 +139,7 @@ export function GrabbableObject({
|
|||||||
}: GrabbableObjectProps): React.JSX.Element {
|
}: GrabbableObjectProps): React.JSX.Element {
|
||||||
const camera = useThree((state) => state.camera);
|
const camera = useThree((state) => state.camera);
|
||||||
const { hands } = useHandTrackingSnapshot();
|
const { hands } = useHandTrackingSnapshot();
|
||||||
|
const spaceRef = useRef<THREE.Group>(null);
|
||||||
const groupRef = useRef<THREE.Group>(null);
|
const groupRef = useRef<THREE.Group>(null);
|
||||||
const rbRef = useRef<RapierRigidBody>(null);
|
const rbRef = useRef<RapierRigidBody>(null);
|
||||||
const isHolding = useRef(false);
|
const isHolding = useRef(false);
|
||||||
@@ -160,17 +162,24 @@ export function GrabbableObject({
|
|||||||
_currentPos.set(translation.x, translation.y, translation.z);
|
_currentPos.set(translation.x, translation.y, translation.z);
|
||||||
|
|
||||||
let nearestTarget: Vector3Tuple | null = null;
|
let nearestTarget: Vector3Tuple | null = null;
|
||||||
|
let nearestTargetWorld: Vector3Tuple | null = null;
|
||||||
let nearestDistance = snapRadius;
|
let nearestDistance = snapRadius;
|
||||||
snapTargets.forEach((target) => {
|
snapTargets.forEach((target) => {
|
||||||
_snapPosition.set(target[0], target[1], target[2]);
|
_snapTargetWorldPosition.set(target[0], target[1], target[2]);
|
||||||
const distance = _currentPos.distanceTo(_snapPosition);
|
spaceRef.current?.localToWorld(_snapTargetWorldPosition);
|
||||||
|
const distance = _currentPos.distanceTo(_snapTargetWorldPosition);
|
||||||
if (distance <= nearestDistance) {
|
if (distance <= nearestDistance) {
|
||||||
nearestDistance = distance;
|
nearestDistance = distance;
|
||||||
nearestTarget = target;
|
nearestTarget = target;
|
||||||
|
nearestTargetWorld = [
|
||||||
|
_snapTargetWorldPosition.x,
|
||||||
|
_snapTargetWorldPosition.y,
|
||||||
|
_snapTargetWorldPosition.z,
|
||||||
|
];
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!nearestTarget) return;
|
if (!nearestTarget || !nearestTargetWorld) return;
|
||||||
|
|
||||||
snapTween.current?.kill();
|
snapTween.current?.kill();
|
||||||
const animatedPosition = {
|
const animatedPosition = {
|
||||||
@@ -182,9 +191,9 @@ export function GrabbableObject({
|
|||||||
body.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
body.setLinvel({ x: 0, y: 0, z: 0 }, true);
|
||||||
body.setAngvel(ZERO_ANGULAR_VELOCITY, true);
|
body.setAngvel(ZERO_ANGULAR_VELOCITY, true);
|
||||||
snapTween.current = gsap.to(animatedPosition, {
|
snapTween.current = gsap.to(animatedPosition, {
|
||||||
x: nearestTarget[0],
|
x: nearestTargetWorld[0],
|
||||||
y: nearestTarget[1],
|
y: nearestTargetWorld[1],
|
||||||
z: nearestTarget[2],
|
z: nearestTargetWorld[2],
|
||||||
duration: snapDuration,
|
duration: snapDuration,
|
||||||
ease: "power2.out",
|
ease: "power2.out",
|
||||||
onUpdate: () => {
|
onUpdate: () => {
|
||||||
@@ -311,43 +320,45 @@ export function GrabbableObject({
|
|||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<RigidBody
|
<group ref={spaceRef}>
|
||||||
ref={rbRef}
|
<RigidBody
|
||||||
type="dynamic"
|
ref={rbRef}
|
||||||
colliders={colliders}
|
type="dynamic"
|
||||||
position={position}
|
colliders={colliders}
|
||||||
>
|
position={position}
|
||||||
<group ref={groupRef}>
|
>
|
||||||
<InteractableObject
|
<group ref={groupRef}>
|
||||||
kind="grab"
|
<InteractableObject
|
||||||
label={label}
|
kind="grab"
|
||||||
position={position}
|
label={label}
|
||||||
bodyRef={rbRef}
|
position={position}
|
||||||
onPress={() => {
|
bodyRef={rbRef}
|
||||||
isHolding.current = true;
|
onPress={() => {
|
||||||
}}
|
isHolding.current = true;
|
||||||
onRelease={() => {
|
}}
|
||||||
isHolding.current = false;
|
onRelease={() => {
|
||||||
snapToNearestTarget();
|
isHolding.current = false;
|
||||||
if (
|
snapToNearestTarget();
|
||||||
!rbRef.current ||
|
if (
|
||||||
grabDebugParams.throwBoost === GRAB_THROW_BOOST_DEFAULT
|
!rbRef.current ||
|
||||||
)
|
grabDebugParams.throwBoost === GRAB_THROW_BOOST_DEFAULT
|
||||||
return;
|
)
|
||||||
const v = rbRef.current.linvel();
|
return;
|
||||||
rbRef.current.setLinvel(
|
const v = rbRef.current.linvel();
|
||||||
{
|
rbRef.current.setLinvel(
|
||||||
x: v.x * grabDebugParams.throwBoost,
|
{
|
||||||
y: v.y * grabDebugParams.throwBoost,
|
x: v.x * grabDebugParams.throwBoost,
|
||||||
z: v.z * grabDebugParams.throwBoost,
|
y: v.y * grabDebugParams.throwBoost,
|
||||||
},
|
z: v.z * grabDebugParams.throwBoost,
|
||||||
true,
|
},
|
||||||
);
|
true,
|
||||||
}}
|
);
|
||||||
>
|
}}
|
||||||
{children}
|
>
|
||||||
</InteractableObject>
|
{children}
|
||||||
</group>
|
</InteractableObject>
|
||||||
</RigidBody>
|
</group>
|
||||||
|
</RigidBody>
|
||||||
|
</group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -148,6 +148,8 @@ export function InteractableObject(
|
|||||||
if (bodyRef?.current) {
|
if (bodyRef?.current) {
|
||||||
const t = bodyRef.current.translation();
|
const t = bodyRef.current.translation();
|
||||||
_objectPos.set(t.x, t.y, t.z);
|
_objectPos.set(t.x, t.y, t.z);
|
||||||
|
} else if (group) {
|
||||||
|
group.getWorldPosition(_objectPos);
|
||||||
} else {
|
} else {
|
||||||
_objectPos.set(...position);
|
_objectPos.set(...position);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { RigidBody } from "@react-three/rapier";
|
import { RigidBody } from "@react-three/rapier";
|
||||||
|
import type { RapierRigidBody } from "@react-three/rapier";
|
||||||
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
import { InteractableObject } from "@/components/three/interaction/InteractableObject";
|
||||||
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
import { useClonedObject } from "@/hooks/three/useClonedObject";
|
||||||
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
import { useLoggedGLTF } from "@/hooks/three/useLoggedGLTF";
|
||||||
@@ -59,14 +60,21 @@ export function TriggerObject({
|
|||||||
onTrigger,
|
onTrigger,
|
||||||
}: TriggerObjectProps): React.JSX.Element {
|
}: TriggerObjectProps): React.JSX.Element {
|
||||||
const [spawned, setSpawned] = useState<SpawnedModel[]>([]);
|
const [spawned, setSpawned] = useState<SpawnedModel[]>([]);
|
||||||
|
const rbRef = useRef<RapierRigidBody>(null);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RigidBody type="fixed" colliders={colliders} position={position}>
|
<RigidBody
|
||||||
|
ref={rbRef}
|
||||||
|
type="fixed"
|
||||||
|
colliders={colliders}
|
||||||
|
position={position}
|
||||||
|
>
|
||||||
<InteractableObject
|
<InteractableObject
|
||||||
kind="trigger"
|
kind="trigger"
|
||||||
label={label}
|
label={label}
|
||||||
position={position}
|
position={position}
|
||||||
|
bodyRef={rbRef}
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
if (soundPath) {
|
if (soundPath) {
|
||||||
AudioManager.getInstance().playSound(soundPath, soundVolume);
|
AudioManager.getInstance().playSound(soundPath, soundVolume);
|
||||||
|
|||||||
@@ -142,9 +142,11 @@ export function TestMap({ onOctreeReady }: TestMapProps): React.JSX.Element {
|
|||||||
</TriggerObject>
|
</TriggerObject>
|
||||||
|
|
||||||
{TEST_SCENE_REPAIR_ZONES.map((zone) => (
|
{TEST_SCENE_REPAIR_ZONES.map((zone) => (
|
||||||
<group key={zone.mission} position={zone.position}>
|
<group key={zone.mission}>
|
||||||
<RepairPlaygroundZoneMarker color={zone.color} />
|
<group position={zone.position}>
|
||||||
<RepairGame mission={zone.mission} position={[0, 0, 0]} />
|
<RepairPlaygroundZoneMarker color={zone.color} />
|
||||||
|
</group>
|
||||||
|
<RepairGame mission={zone.mission} position={zone.position} />
|
||||||
</group>
|
</group>
|
||||||
))}
|
))}
|
||||||
</Physics>
|
</Physics>
|
||||||
|
|||||||
Reference in New Issue
Block a user