8.2 KiB
Enemy fear movement follow-up — 2026-10-06
This pass integrates source-informed movement for the pinned enemy shout fear auras into player, party and mobile owned-summon consumers. Voluntary movement, jumping, attacks and casts remain suppressed while fear controls the actor. Movement requires reviewed navigation and collision/LOS approval. Missing or blocked spatial support safely holds the actor. Other controls retain their existing policy; this does not claim exact historical-server path or RNG parity.
Historical core behavior
The read-only historical source is AzerothCore commit
e822d718322289dd036fb2daf8867ba41283bf9a, specifically
src/server/game/Movement/MovementGenerators/FleeingMovementGenerator.cpp,
src/server/game/Movement/MotionMaster.cpp,
src/server/game/Entities/Unit/Unit.cpp, and
src/server/game/Spells/Auras/SpellAuraEffects.cpp under
../wow-pvt-server-backups-2/azerothcore-wotlk.
HandleModFear passes the live aura caster to Unit::SetControlled; that
passes it to SetFeared, then MotionMaster::MoveFleeing. The generator looks
up the caster GUID when it chooses each new destination. It does not preserve
the caster's cast-time position. If that lookup fails, it picks a random angle
and follows its near-source distance branch with a source distance of zero. A
dead caster can still resolve while its unit remains in-world; after removal,
the source-less fallback applies.
The source's exact movement bounds and retries are:
| Core rule | Source value |
|---|---|
| Quiet-range lower bound | 28 yd |
| Quiet-range upper bound | 43 yd |
| Minimum accepted path | 2 yd |
| Path length limit | 30 yd |
| Retry after destination LOS failure | 200 ms |
| Retry after failed, short, or invalid path | 100 ms |
| Repath wait after spline travel | random 800–1500 ms |
Below 28 yards, the server picks a distance between 0.4 and 1.3 times the
remaining distance to 28 yards and a heading within ±π/8 of the direction away
from the source. Above 43 yards, it picks 0.4–1.0 times the 15-yard band
width and heads partly back toward the source, within ±π/4. Inside the quiet
band, it picks 0.6–1.2 times the band width in a random direction. A source
distance at or below 0.2 yards uses a random base angle. The core constrains the
candidate with MovePositionToFirstCollision, checks LOS to the resulting
point, then asks PathGenerator for a path and rejects shortcuts, no-path,
far-from-poly, paths shorter than 2 yards, and paths beyond the 30-yard limit.
After five failed paths it stops looking up the source and uses the
source-less random fallback.
SetControlled does not start fear while stun or confusion already owns the
unit. When the higher-priority control ends, it rechecks active fear auras and
resolves their caster GUID. This helper accepts the actor's active stun end time
and holds fear movement until that higher-priority control expires.
Planner boundary
src/game/enemyFearMovement.ts carries the
source distance bands, steering ranges, LOS/path retries, and path length
limits. Its hash-based choices are simulator decisions: they reproduce the
source's ranges with stable values derived from actor ID, source ID, aura
application time, and decision index. They do not reproduce the core's global
random-number stream. worldUnitsPerYard is required input because imported map
coordinates can have a different scale. The estimated time to the next choice
uses approved path length divided by this actor's current effective speed,
then adds a seeded wait from the core's 800–1500 ms range.
Each actor supplies its own fear expiry, stun expiry, base movement speed, and
source speed multiplier. The shout's +25% speed is therefore applied only when
that target has the still-active source speed aura. The current adapters are
limited to tagged source fear. getSourcePosition runs once per repath so
the goal uses the live caster location at that decision; the caller should
cache the resulting goal until nextDecisionAtMs instead of recomputing it
every frame.
The planner requires three map adapters: a projection that rejects excessive
snaps and floor changes, an LOS test, and a complete actor-safe path-length
query. Missing map support, an unsafe projection, blocked LOS, or an invalid
route yields a hold with the source retry timing. The destination is a route
goal only. The caller must move through actor collision and approved navigation
waypoints; it must never assign the destination directly to an actor position.
stepEnemyFearTowardDestination only computes a bounded request and does not
replace those checks.
Integration seams before this pass
PlayerRigresolves the player's shout control near its frame movement step.enemyControlPreventsMovementcurrently blocks all fear before movement, andRapier's character controller checks the player's capsule against terrain. For a tagged source fear with no active stun, the frame can use the cached flee goal to set the autonomous world movement vector, then submit its bounded delta through the existing character controller. The goal must not be derived from camera input or change camera yaw.PartyPopulationsimilarly blocks fear before companion route planning. It already has the reviewed navigation graph,planPartyRoute, direct-segment collision checks, and route-segment clearance validation. A fear goal should use those route helpers and hold when no safe route exists.- The summon loop in
combatStore.tsreturns early whencontrolledUntilis active, before movement and actions.CombatSummoncurrently keeps a control expiry but no control mechanic. A source fear adapter needs to preserve the per-summon fear and stun state, move only when fear is the active control, and continue to suppress actions.CombatSpatialProviderprovides live actor positions, LOS, ground projection, and the reviewed navigation graph; the adapter still needs to validate a complete route before advancing a summon. - The shout control resolver currently returns only mechanic and expiry. The
integration must identify the active tagged
ActiveAuraper actor to retain its source ID andappliedAtfor live source lookup and deterministic seeding. That lookup can use the mob runtime position while the caster exists; when the source ID no longer resolves, passnulland use the source-less branch.
The existing hostile NPC fear path is separate. MobPopulation already calls
stepFearedMobAway and clamps that direct step to the home leash, using the
current aggro target as the threat. That helper does not perform LOS or route
validation for the flee step. This follow-up leaves it unchanged and does not
claim it matches the historical generator.
Integrated consumers and verification
enemyFearMovementRuntime.ts retains the selected aura's source/application,
caches each goal and implements the five-failure source fallback. Concurrent
tagged stun and generic stun/confuse/root/sleep auras hold movement even when
the actor's longest control timer belongs to fear. Expiry or purge clears the
per-actor goal; a refreshed aura starts a new decision sequence.
PlayerRig follows approved route waypoints through its existing Rapier character controller, including gravity. Keyboard movement and jumping do not steer fear. PartyPopulation uses its reviewed graph and capsule route validators, normal party speed with actor-local source modifiers, and follows forced fear even under Stop. Fear never enables catch-up speed or stuck-recovery relocation. Camera look retains its ordinary hold-and-drag behavior.
enemyFearSummonMovement.ts requires a loaded graph and validates projection,
LOS, path segments and each bounded step. The actual combat tick consumes it
before summon actions, holds concurrent controls, and preserves the pet's Prowl
movement modifier. Totems and stationary zones remain stationary. Remote actor
authority and hostile-NPC flee policy remain outside this bounded integration.
Focused planner/cache/summon verification passes 20 tests, including actual pet ticks, blocked/missing navigation and simultaneous stun expiry. Input regressions pass 33 tests. Final build/live evidence is recorded in the combined follow-up report.