forked from phenom/healer-man
79 lines
2.6 KiB
TypeScript
79 lines
2.6 KiB
TypeScript
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);
|
|
}
|