forked from phenom/healer-man
71 lines
2.4 KiB
TypeScript
71 lines
2.4 KiB
TypeScript
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<Group>(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 (
|
|
<group ref={anchorRef} position={[position[0], position[1], position[2]]}>
|
|
{visible ? (
|
|
<DungeonOccludedHtml center zIndexRange={[zIndexRange[0], zIndexRange[1]]}>
|
|
{children}
|
|
</DungeonOccludedHtml>
|
|
) : null}
|
|
</group>
|
|
);
|
|
}
|