export const MOB_ANIMATION_NEAR_DISTANCE = 32; export const MOB_ANIMATION_MEDIUM_DISTANCE = 62; export const MOB_ANIMATION_MEDIUM_INTERVAL = 1 / 30; export const MOB_ANIMATION_FAR_INTERVAL = 1 / 15; const MAX_FRAME_DELTA = 0.05; const CADENCE_EPSILON = 1e-6; /** `0` means every render frame; `null` means the animation may stay frozen. */ export type MobAnimationInterval = number | null; export interface MobAnimationCadenceState { accumulatedDelta: number; } export function createMobAnimationCadenceState(): MobAnimationCadenceState { return { accumulatedDelta: 0 }; } /** * Keeps combat-critical actors at full fidelity while throttling background * presentation only. Gameplay movement and combat simulation do not use this. */ export function resolveMobAnimationInterval( active: boolean, forceFullFidelity: boolean, distanceSquared: number, inFrontOfCamera: boolean, ): MobAnimationInterval { if (!active) return null; if (forceFullFidelity) return 0; if (distanceSquared <= MOB_ANIMATION_NEAR_DISTANCE ** 2) return 0; if (!inFrontOfCamera) return null; if (distanceSquared <= MOB_ANIMATION_MEDIUM_DISTANCE ** 2) { return MOB_ANIMATION_MEDIUM_INTERVAL; } return MOB_ANIMATION_FAR_INTERVAL; } /** * Accumulates capped render deltas and returns the elapsed time to evaluate on * this frame. This preserves animation speed at a lower sampling frequency. */ export function advanceMobAnimationCadence( state: MobAnimationCadenceState, delta: number, interval: MobAnimationInterval, ): number { const safeDelta = Number.isFinite(delta) ? Math.min(Math.max(0, delta), MAX_FRAME_DELTA) : 0; if (interval === null) { state.accumulatedDelta = 0; return 0; } state.accumulatedDelta += safeDelta; if (interval > 0 && state.accumulatedDelta + CADENCE_EPSILON < interval) return 0; const updateDelta = state.accumulatedDelta; state.accumulatedDelta = 0; return updateDelta; }