Files
healer-man/src/scene/dungeonLineOfSight.ts
T
2026-08-22 15:53:43 -04:00

61 lines
1.8 KiB
TypeScript

import { interactionGroups } from "@react-three/rapier";
import type {
QueryFilterFlags,
Ray,
Vector,
World,
} from "@dimforge/rapier3d-compat";
interface RapierApi {
readonly Ray: new (origin: Vector, direction: Vector) => Ray;
readonly QueryFilterFlags: {
readonly EXCLUDE_DYNAMIC: QueryFilterFlags;
readonly EXCLUDE_KINEMATIC: QueryFilterFlags;
};
}
type WorldPosition = readonly [number, number, number];
export const DUNGEON_LINE_OF_SIGHT_GROUP = 15;
export const DUNGEON_PHYSICAL_COLLISION_GROUPS = interactionGroups(0);
export const DUNGEON_SIGHT_BLOCKER_COLLISION_GROUPS = interactionGroups(DUNGEON_LINE_OF_SIGHT_GROUP);
export const DUNGEON_WALL_COLLISION_GROUPS = interactionGroups([0, DUNGEON_LINE_OF_SIGHT_GROUP]);
export const DUNGEON_SIGHT_QUERY_GROUPS = interactionGroups(
DUNGEON_LINE_OF_SIGHT_GROUP,
DUNGEON_LINE_OF_SIGHT_GROUP,
);
const ACTOR_SIGHT_HEIGHT = 1.05;
const TARGET_MARGIN = 0.08;
/**
* Checks only the dedicated wall/pillar collision layer. Walkable terrain and
* ceilings remain physical, but never participate in combat visibility.
*/
export function hasDungeonLineOfSight(
rapier: RapierApi,
world: World,
start: WorldPosition,
end: WorldPosition,
): boolean {
const dx = end[0] - start[0];
const dy = end[1] - start[1];
const dz = end[2] - start[2];
const distance = Math.hypot(dx, dy, dz);
if (!Number.isFinite(distance)) return false;
if (distance <= TARGET_MARGIN) return true;
const ray = new rapier.Ray(
{ x: start[0], y: start[1] + ACTOR_SIGHT_HEIGHT, z: start[2] },
{ x: dx / distance, y: dy / distance, z: dz / distance },
);
const hit = world.castRay(
ray,
distance,
true,
rapier.QueryFilterFlags.EXCLUDE_DYNAMIC | rapier.QueryFilterFlags.EXCLUDE_KINEMATIC,
DUNGEON_SIGHT_QUERY_GROUPS,
);
return !hit || hit.timeOfImpact >= distance - TARGET_MARGIN;
}