import { useFrame } from "@react-three/fiber"; import { useRef, useState, type ReactNode } from "react"; import { Group, Vector3 } from "three"; import type { MobVector3 } from "../game/mobPopulation"; import { useGameStore } from "../game/store"; import { DungeonOccludedHtml } from "./DungeonOccludedHtml"; export const WORLD_NPC_PROMPT_MAX_DISTANCE = 22; const VISIBILITY_CHECK_INTERVAL_SECONDS = 0.1; const VISIBILITY_HYSTERESIS = 2; export function worldNpcPromptIsInRange( viewerPosition: MobVector3, promptPosition: MobVector3, maxDistance = WORLD_NPC_PROMPT_MAX_DISTANCE, ): boolean { if (!Number.isFinite(maxDistance) || maxDistance < 0) return false; const dx = viewerPosition[0] - promptPosition[0]; const dy = viewerPosition[1] - promptPosition[1]; const dz = viewerPosition[2] - promptPosition[2]; return dx * dx + dy * dy + dz * dz <= maxDistance * maxDistance; } export function WorldNpcPrompt({ children, position = [0, 0, 0], maxDistance = WORLD_NPC_PROMPT_MAX_DISTANCE, zIndexRange = [8, 0], }: { readonly children: ReactNode; readonly position?: MobVector3; readonly maxDistance?: number; readonly zIndexRange?: readonly [number, number]; }) { const anchorRef = useRef(null); const worldPositionRef = useRef(new Vector3()); const elapsedRef = useRef(VISIBILITY_CHECK_INTERVAL_SECONDS); const visibleRef = useRef(false); const [visible, setVisible] = useState(false); useFrame((_, delta) => { elapsedRef.current += delta; if (elapsedRef.current < VISIBILITY_CHECK_INTERVAL_SECONDS) return; elapsedRef.current = 0; if (!anchorRef.current) return; anchorRef.current.getWorldPosition(worldPositionRef.current); const worldPosition = worldPositionRef.current; const distanceLimit = maxDistance + (visibleRef.current ? VISIBILITY_HYSTERESIS : 0); const nextVisible = worldNpcPromptIsInRange( useGameStore.getState().playerPosition, [worldPosition.x, worldPosition.y, worldPosition.z], distanceLimit, ); if (nextVisible === visibleRef.current) return; visibleRef.current = nextVisible; setVisible(nextVisible); }); return ( {visible ? ( {children} ) : null} ); }