Files
healer-man/src/scene/WorldNpcPrompt.tsx
T
2026-09-09 22:18:15 -04:00

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>
);
}