84 lines
2.2 KiB
TypeScript
84 lines
2.2 KiB
TypeScript
import { useEffect, useRef, useState } from "react";
|
|
import { useFrame, useThree } from "@react-three/fiber";
|
|
import * as THREE from "three";
|
|
import { isDebugEnabled } from "@/utils/debug/isDebugEnabled";
|
|
import type { ZoneConfig } from "@/types/gameplay/zone";
|
|
|
|
interface ZoneDetectionProps {
|
|
zone: ZoneConfig;
|
|
onEnter: () => void;
|
|
height?: number;
|
|
}
|
|
|
|
const _cameraPos = new THREE.Vector3();
|
|
|
|
function ZoneDebugVisual({
|
|
zone,
|
|
active,
|
|
}: {
|
|
zone: ZoneConfig;
|
|
active: boolean;
|
|
}): React.JSX.Element | null {
|
|
if (!isDebugEnabled()) return null;
|
|
return (
|
|
<group position={zone.position}>
|
|
<mesh rotation={[-Math.PI / 2, 0, 0]}>
|
|
<ringGeometry args={[zone.radius - 0.2, zone.radius, 32]} />
|
|
<meshBasicMaterial
|
|
color={active ? "#22c55e" : "#fbbf24"}
|
|
transparent
|
|
opacity={0.6}
|
|
side={THREE.DoubleSide}
|
|
/>
|
|
</mesh>
|
|
<mesh>
|
|
<cylinderGeometry
|
|
args={[zone.radius, zone.radius, zone.height, 16, 1, true]}
|
|
/>
|
|
<meshBasicMaterial
|
|
color={active ? "#22c55e" : "#fbbf24"}
|
|
transparent
|
|
opacity={0.08}
|
|
side={THREE.DoubleSide}
|
|
/>
|
|
</mesh>
|
|
</group>
|
|
);
|
|
}
|
|
|
|
export function ZoneDetection({
|
|
zone,
|
|
onEnter,
|
|
height,
|
|
}: ZoneDetectionProps): React.JSX.Element {
|
|
const camera = useThree((state) => state.camera);
|
|
const hasTriggeredRef = useRef(false);
|
|
const onEnterRef = useRef(onEnter);
|
|
const [isActive, setIsActive] = useState(false);
|
|
|
|
useEffect(() => {
|
|
onEnterRef.current = onEnter;
|
|
}, [onEnter]);
|
|
|
|
useFrame(() => {
|
|
if (hasTriggeredRef.current) return;
|
|
|
|
camera.getWorldPosition(_cameraPos);
|
|
const dx = _cameraPos.x - zone.position[0];
|
|
const dz = _cameraPos.z - zone.position[2];
|
|
const horizontalDist = Math.sqrt(dx * dx + dz * dz);
|
|
|
|
if (horizontalDist > zone.radius) return;
|
|
|
|
const zoneHeight = height ?? zone.height;
|
|
if (_cameraPos.y < zone.position[1] - zoneHeight / 2) return;
|
|
if (_cameraPos.y > zone.position[1] + zoneHeight / 2) return;
|
|
|
|
hasTriggeredRef.current = true;
|
|
setIsActive(true);
|
|
onEnterRef.current();
|
|
});
|
|
|
|
return <ZoneDebugVisual zone={zone} active={isActive} />;
|
|
}
|