import type { MobVector3, MutableMobVector3 } from "./mobPopulation"; const POSITION_EPSILON = 1e-6; function finitePosition(position: MobVector3): boolean { return Number.isFinite(position[0]) && Number.isFinite(position[1]) && Number.isFinite(position[2]); } /** * Moves a feared mob away from its threat on the dungeon plane while keeping * it inside its home leash. The caller may reuse `current` as `out`; no * per-frame objects are allocated. */ export function stepFearedMobAway( current: MobVector3, threat: MobVector3, home: MobVector3, maximumStep: number, leashRange: number, out: MutableMobVector3, ): number { if (!finitePosition(current) || !finitePosition(threat) || !finitePosition(home)) { out[0] = Number.isFinite(current[0]) ? current[0] : 0; out[1] = Number.isFinite(current[1]) ? current[1] : 0; out[2] = Number.isFinite(current[2]) ? current[2] : 0; return 0; } const safeStep = Number.isFinite(maximumStep) ? Math.max(0, maximumStep) : 0; const safeLeash = Number.isFinite(leashRange) ? Math.max(0, leashRange) : 0; if (safeStep <= 0 || safeLeash <= 0) { out[0] = current[0]; out[1] = current[1]; out[2] = current[2]; return 0; } // Capture these before writing because high-frequency callers intentionally // pass the same tuple for `current` and `out`. const currentX = current[0]; const currentY = current[1]; const currentZ = current[2]; let awayX = currentX - threat[0]; let awayZ = currentZ - threat[2]; let awayLength = Math.hypot(awayX, awayZ); if (awayLength <= POSITION_EPSILON) { // Coincident threat positions need a stable escape direction. Prefer the // direction already leading away from home; use +X only at home itself. awayX = currentX - home[0]; awayZ = currentZ - home[2]; awayLength = Math.hypot(awayX, awayZ); if (awayLength <= POSITION_EPSILON) { awayX = 1; awayZ = 0; awayLength = 1; } } const candidateX = currentX + awayX / awayLength * safeStep; const candidateZ = currentZ + awayZ / awayLength * safeStep; const homeX = candidateX - home[0]; const homeZ = candidateZ - home[2]; const candidateHomeDistance = Math.hypot(homeX, homeZ); if (candidateHomeDistance > safeLeash) { const leashScale = safeLeash / candidateHomeDistance; out[0] = home[0] + homeX * leashScale; out[2] = home[2] + homeZ * leashScale; } else { out[0] = candidateX; out[2] = candidateZ; } // Dungeon floor height remains owned by the sampled spawn/path position. out[1] = currentY; return Math.hypot(out[0] - currentX, out[2] - currentZ); }