export type CombatPosition = readonly [x: number, y: number, z: number]; export type MutableCombatPosition = [x: number, y: number, z: number]; interface RuntimeMobPosition { readonly value: MutableCombatPosition; yaw: number; mounted: boolean; } const runtimePositions = new Map(); function finite(value: number): number { return Number.isFinite(value) ? value : 0; } export function registerMobRuntime(id: string, initial: CombatPosition = [0, 0, 0], yaw = 0): () => void { const record: RuntimeMobPosition = { value: [finite(initial[0]), finite(initial[1]), finite(initial[2])], yaw: finite(yaw), mounted: true, }; runtimePositions.set(id, record); return () => { if (runtimePositions.get(id) === record) runtimePositions.delete(id); }; } /** Allocation-free update intended for roaming-pack useFrame callbacks. */ export function updateMobRuntimePosition(id: string, x: number, y: number, z: number, yaw?: number): void { const record = runtimePositions.get(id); if (record) { record.value[0] = finite(x); record.value[1] = finite(y); record.value[2] = finite(z); if (yaw !== undefined) record.yaw = finite(yaw); record.mounted = true; return; } runtimePositions.set(id, { value: [finite(x), finite(y), finite(z)], yaw: finite(yaw ?? 0), mounted: true }); } export function unregisterMobRuntime(id: string): void { runtimePositions.delete(id); } /** Copies into caller-owned storage to avoid allocations in simulation ticks. */ export function readMobRuntimePosition(id: string, out: MutableCombatPosition): boolean { const record = runtimePositions.get(id); if (!record?.mounted) return false; out[0] = record.value[0]; out[1] = record.value[1]; out[2] = record.value[2]; return true; } /** Snapshot helper for UI and occasional targeting operations. */ export function getMobRuntimePosition(id: string): CombatPosition | null { const record = runtimePositions.get(id); return record?.mounted ? [...record.value] as MutableCombatPosition : null; } export function getMobRuntimeTransform(id: string): { readonly position: CombatPosition; readonly yaw: number } | null { const record = runtimePositions.get(id); return record?.mounted ? { position: [...record.value] as MutableCombatPosition, yaw: record.yaw } : null; } export function mobRuntimeDistanceSquared(id: string, origin: CombatPosition): number { const record = runtimePositions.get(id); if (!record?.mounted) return Number.POSITIVE_INFINITY; const dx = record.value[0] - origin[0]; const dy = record.value[1] - origin[1]; const dz = record.value[2] - origin[2]; return dx * dx + dy * dy + dz * dz; } export function nearbyMobRuntimeIds(origin: CombatPosition, maxDistance: number): string[] { const limit = Math.max(0, maxDistance); const limitSquared = limit * limit; return Array.from(runtimePositions.entries()) .filter(([, record]) => { if (!record.mounted) return false; const dx = record.value[0] - origin[0]; const dy = record.value[1] - origin[1]; const dz = record.value[2] - origin[2]; return dx * dx + dy * dy + dz * dz <= limitSquared; }) .sort((left, right) => { const leftDistance = mobRuntimeDistanceSquared(left[0], origin); const rightDistance = mobRuntimeDistanceSquared(right[0], origin); return leftDistance - rightDistance || left[0].localeCompare(right[0]); }) .map(([id]) => id); } export function hasMobRuntimePosition(id: string): boolean { return runtimePositions.get(id)?.mounted ?? false; } export function clearMobRuntimeRegistry(): void { runtimePositions.clear(); } // Short integration aliases requested by the scene bridge. export function setMobPosition(id: string, position: CombatPosition): void { updateMobRuntimePosition(id, position[0], position[1], position[2]); } export function getMobPosition(id: string): CombatPosition | null { return getMobRuntimePosition(id); } export function removeMobPosition(id: string): void { unregisterMobRuntime(id); }