Class BaseAttackingState
The one attacking state. Handles target selection, spacing, ability activation and mid-combat re-targeting for every NPC archetype.
[CreateAssetMenu(fileName = "New AI Attacking State", menuName = "FishMMO/Character/NPC/AI/Attacking State", order = 0)]
public class BaseAttackingState : BaseAIState, ICachedObject
- Inheritance
-
ObjectScriptableObjectBaseAttackingState
- Implements
- Derived
- Inherited Members
-
ScriptableObject.SetDirty()ScriptableObject.CreateInstance<T>()Object.GetEntityId()Object.GetInstanceID()Object.GetHashCode()Object.InstantiateAsync<T>(T)Object.InstantiateAsync<T>(T, Transform)Object.InstantiateAsync<T>(T, Vector3, Quaternion)Object.InstantiateAsync<T>(T, Transform, Vector3, Quaternion)Object.Instantiate(Object, Vector3, Quaternion)Object.Instantiate(Object, Vector3, Quaternion, Transform)Object.Instantiate(Object)Object.Instantiate(Object, Scene)Object.Instantiate<T>(T, InstantiateParameters)Object.Instantiate<T>(T, Vector3, Quaternion, InstantiateParameters)Object.Instantiate(Object, Transform)Object.Instantiate<T>(T)Object.Instantiate<T>(T, Vector3, Quaternion)Object.Instantiate<T>(T, Vector3, Quaternion, Transform)Object.Instantiate<T>(T, Transform)Object.Destroy(Object)Object.DestroyImmediate(Object)Object.DontDestroyOnLoad(Object)Object.DestroyObject(Object)Object.FindObjectsOfType<T>()Object.FindObjectsByType<T>(FindObjectsSortMode)Object.FindObjectsByType<T>(FindObjectsInactive, FindObjectsSortMode)Object.FindObjectOfType<T>()Object.FindFirstObjectByType<T>()Object.FindAnyObjectByType<T>()Object.FindFirstObjectByType<T>(FindObjectsInactive)Object.FindAnyObjectByType<T>(FindObjectsInactive)Object.FindObjectsByType<T>()Object.FindObjectsByType<T>(FindObjectsInactive)Object.ToString()Object.nameObject.hideFlags
Remarks
Archetypes are data, not subclasses. Melee, archer, caster, defender and rogue behaviour all fall out of four serialized numbers — PreferredDistance, MinComfortDistance, EmergencyRetreatThreshold and the personality attached to the controller — fed into the shared Plan(in AICombatContext). A designer builds a new archetype by creating an asset, not by writing a class.
The specialised subclasses that remain (HealerAttackingState, DefenderAttackingState, RogueAttackingState) exist only because they need something the numbers cannot express: healers must scan for injured allies, defenders must body-block for one, and rogues must open from behind.
Fields
AggressionSwitchThreshold
A candidate must exceed the current target's aggression by at least this many points before the NPC will switch targets mid-combat. Prevents constant flip-flopping.
[Tooltip("Aggression point lead required to switch targets mid-combat.")]
public float AggressionSwitchThreshold
Field Value
AttackCooldown
Minimum seconds between ability activations. A small deterministic jitter is added so a pack of identical NPCs does not fire in lockstep.
[Header("Pacing")]
[Tooltip("Minimum seconds between ability activations. 0 = as fast as cooldowns allow.")]
public float AttackCooldown
Field Value
AttackCooldownJitter
Maximum extra seconds randomly added to AttackCooldown per activation.
[Tooltip("Maximum random jitter added to AttackCooldown.")]
public float AttackCooldownJitter
Field Value
EmergencyRetreatState
Optional state to hand off to for an emergency retreat. When null the NPC backs away using the built-in logic without leaving this state.
[Tooltip("Optional state for emergency backing off. Null = back away in place.")]
public BaseAIState EmergencyRetreatState
Field Value
EmergencyRetreatThreshold
Fraction (0-1) of MinComfortDistance at which backing away escalates into an interrupt-and-run emergency retreat.
[Range(0, 1)]
[Tooltip("Fraction of MinComfortDistance that triggers an emergency retreat.")]
public float EmergencyRetreatThreshold
Field Value
MinComfortDistance
If the target is closer than this distance the NPC will try to back away. Only meaningful for ranged/caster archetypes. 0 disables retreat.
[Tooltip("Distance below which the NPC backs away. 0 = never back away.")]
public float MinComfortDistance
Field Value
MovementVarietyChance
Chance (0-1) per attack cycle to step into one of the VarietyStates instead of attacking from where the NPC stands.
[Range(0, 1)]
[Tooltip("Chance per attack cycle to enter a variety state.")]
public float MovementVarietyChance
Field Value
OwnerLeashRange
How far a pet may stray from its owner before it breaks off and returns to heel. Ignored for NPCs that are not pets. 0 disables.
[Header("Pet")]
[Tooltip("Pets only: distance from the owner at which the pet breaks off and returns. 0 disables.")]
public float OwnerLeashRange
Field Value
Remarks
Pet-awareness lives here rather than in a PetAttackingState subclass on purpose. A pet only differs from a wild NPC in two ways — its leash anchor follows its owner, and it returns to that owner instead of wandering — and putting those two rules in the shared base means a pet healer, defender or rogue gets them for free. A subclass could not, because it would have to inherit from the archetype instead.
PreferredDistance
Preferred combat distance. The NPC will try to maintain roughly this distance from its target. A value of 0 means the NPC will close to melee range (agent radius).
[Header("Spacing")]
[Tooltip("Preferred combat distance. 0 = close to melee range.")]
public float PreferredDistance
Field Value
TargetReevaluationRate
How often (seconds) the NPC re-evaluates its target mid-combat. Set to 0 to disable mid-combat re-evaluation.
[Header("Targeting")]
[Tooltip("Seconds between mid-combat target re-evaluation. 0 = disabled.")]
public float TargetReevaluationRate
Field Value
UnreachableTargetTimeout
Seconds the NPC will keep trying to reach an unreachable target before giving up on it. 0 disables the check.
[Header("Chase")]
[Tooltip("Seconds spent unable to reach a target before breaking off. 0 = chase forever.")]
public float UnreachableTargetTimeout
Field Value
UseCombatSlots
When true, attackers spread around their shared target instead of all pathing to the same point.
[Header("Spacing (multi-attacker)")]
[Tooltip("Spread attackers into a ring around a shared target instead of converging on one point.")]
public bool UseCombatSlots
Field Value
Remarks
The NavMeshAgent's own avoidance stops agents overlapping, but it has no say in where they are trying to go. Five NPCs told to reach one point will shove each other around it indefinitely no matter how avoidance is tuned, because the conflict is in the destinations, not the collisions. Slot assignment separates the destinations so avoidance is only asked to handle incidental crossings.
VarietyEngagementMultiplier
Only roll for a variety state when the target is within this multiple of the engagement distance. Stops an NPC from strafing while still closing the gap.
[Tooltip("Only roll for variety when within this multiple of the engagement distance.")]
public float VarietyEngagementMultiplier
Field Value
VarietyStates
Optional positioning states entered occasionally for combat variety. These must have KeepsCombatTarget enabled.
[Header("Movement Variety")]
[Tooltip("Optional positioning states (orbit / flank / strafe) entered mid-combat for variety.")]
public List<BaseAIState> VarietyStates
Field Value
Methods
ActivateAbility(AIController, IAbilityController, Ability)
Activates an ability and arms the per-NPC attack pacing timer.
protected void ActivateAbility(AIController controller, IAbilityController abilityController, Ability ability)
Parameters
controllerAIControllerThe AI controller.
abilityControllerIAbilityControllerThe ability controller to activate on.
abilityAbilityThe ability to activate.
Remarks
AttackCooldown was previously a documented but entirely unread field — NPCs fired on every tick that had anything off cooldown. The timer lives on the controller rather than on this shared ScriptableObject, which every NPC using this archetype has a reference to.
BuildContext(AIController, float, Ability)
Builds the decision context for this tick from the archetype's tuning and the current combat measurements.
protected AICombatContext BuildContext(AIController controller, float distance, Ability chosenAbility)
Parameters
controllerAIControllerThe AI controller.
distancefloatCurrent distance to the target.
chosenAbilityAbilityThe ability the picker returned, or null.
Returns
- AICombatContext
A context ready for Plan(in AICombatContext).
Enter(AIController)
Called when entering the attacking state. Sets agent speed to run.
public override void Enter(AIController controller)
Parameters
controllerAIControllerThe AI controller.
ExecutePlan(AIController, IAbilityController, ICharacter, AICombatPlan, in AICombatContext, Ability)
Carries out a decided AICombatPlan.
protected virtual void ExecutePlan(AIController controller, IAbilityController abilityController, ICharacter targetCharacter, AICombatPlan plan, in AICombatContext context, Ability chosenAbility)
Parameters
controllerAIControllerThe AI controller.
abilityControllerIAbilityControllerThe NPC's ability controller.
targetCharacterICharacterThe target character.
planAICombatPlanThe decision to execute.
contextAICombatContextThe context the decision was made from.
chosenAbilityAbilityThe ability the picker returned, or null.
Exit(AIController)
Called when exiting the attacking state. Resets agent speed and, unless the NPC is stepping into a combat sub-state, drops the target and interrupts any cast.
public override void Exit(AIController controller)
Parameters
controllerAIControllerThe AI controller.
FirstAlive(List<ICharacter>)
Returns the first living, active candidate in the list.
protected static ICharacter FirstAlive(List<ICharacter> targets)
Parameters
targetsList<ICharacter>Candidates to scan.
Returns
- ICharacter
The first valid candidate, or null.
GetMeleeReach(AIController)
The distance a melee archetype closes to, derived from the agent's footprint but never smaller than one metre so a small agent can still reach its target.
protected static float GetMeleeReach(AIController controller)
Parameters
controllerAIControllerThe AI controller.
Returns
- float
The melee engagement distance in world units.
HandleActivationInProgress(AIController, IAbilityController)
Returns true if the ability controller is currently activating or has a queued ability. Stops the agent and auto-releases charged abilities when their charge time completes. Call at the start of TryAttack(AIController, ICharacter) to skip attack logic while casting.
protected static bool HandleActivationInProgress(AIController controller, IAbilityController abilityController)
Parameters
controllerAIControllerThe AI controller.
abilityControllerIAbilityControllerThe ability controller to check.
Returns
- bool
True if an activation is in progress and the caller should return early.
HandleChaseObstruction(AIController, float)
Detects an NPC that cannot reach its target and breaks off rather than jogging on the spot forever.
protected void HandleChaseObstruction(AIController controller, float stopRange)
Parameters
controllerAIControllerThe AI controller.
stopRangefloatDistance from the target the NPC is trying to reach.
Remarks
A target that stands somewhere the NPC cannot path to — on a rock, across a gap, behind a door — produces a partial path. The NPC walks to the closest reachable point and stops, still "in combat", still holding threat, and never attacks or disengages. That is the melee equivalent of the stuck pet, and it is how players kite mobs into a permanent no-op.
Recovery first tries to walk around; failing that, the NPC gives up on the target so a leash or a fresh sweep can take over.
IsEnemyAbility(Ability)
True when an ability is something to aim at an enemy.
public static bool IsEnemyAbility(Ability ability)
Parameters
abilityAbilityThe ability to test.
Returns
- bool
True if the ability may be used against an enemy.
Remarks
The classifier reads what an ability does out of its ECA actions, so this needs no list of ability IDs on the state: an ability whose graph only heals or only buffs is filtered out of the attack rotation wherever it came from, and one added tomorrow is filtered out too.
Deliberately permissive at the edges. An ability that both damages and buffs stays in — the damage is the point. A self-cast shield stays in, because the NPC aims it at itself rather than at the enemy and casting it mid-fight is exactly right. Only an ability that is purely supportive and aimed at someone else is excluded, which is the case that produced the actual absurdity: an NPC healing the player it was trying to kill.
An ability with no recognisable actions classifies as None and is allowed through, so content that predates classification keeps working rather than silently disarming the NPC that knows it.
MoveTowardTarget(AIController, float)
Moves the NPC toward its target, stopping at the specified range.
protected void MoveTowardTarget(AIController controller, float stopRange)
Parameters
controllerAIControllerThe AI controller.
stopRangefloatDistance from the target at which to stop.
OnCombatEnded(AIController)
Called when the NPC has run out of targets: a wild NPC drifts into a movement state, a pet returns to its owner's heel.
protected virtual void OnCombatEnded(AIController controller)
Parameters
controllerAIControllerThe AI controller.
PerformAttack(AIController, IAbilityController, Ability, ICharacter, float)
Performs the attack by activating the chosen ability via the ability controller.
public virtual void PerformAttack(AIController controller, IAbilityController abilityController, Ability ability, ICharacter targetCharacter, float distance)
Parameters
controllerAIControllerThe AI controller.
abilityControllerIAbilityControllerThe ability controller to activate on.
abilityAbilityThe chosen ability to activate.
targetCharacterICharacterThe target character being attacked.
distancefloatCurrent distance to the target.
PickAbility(AIController)
Chooses the ability to use against the current enemy target. Overridden by archetypes that must reorder their spellbook — a defender leads with a taunt, a healer breaks off to heal.
protected virtual Ability PickAbility(AIController controller)
Parameters
controllerAIControllerThe AI controller.
Returns
- Ability
The chosen ability, or null when nothing is usable.
PickByTargetingMode(AIController, List<ICharacter>)
Selects a target according to the controller personality's AITargetingMode. Falls back to the aggression table when no personality is assigned.
protected static ICharacter PickByTargetingMode(AIController controller, List<ICharacter> targets)
Parameters
controllerAIControllerThe AI controller.
targetsList<ICharacter>Available candidates.
Returns
- ICharacter
The chosen target, or null when the mode produced nothing.
PickRoleBasedTarget(AIController, List<ICharacter>)
Selects a target based on the controller's NPCGroupRole. Returns null if the role doesn't produce a valid target (caller falls through to default logic).
protected virtual ICharacter PickRoleBasedTarget(AIController controller, List<ICharacter> targets)
Parameters
controllerAIControllerThe AI controller with group and role info.
targetsList<ICharacter>Available targets from the sweep.
Returns
- ICharacter
A role-appropriate target, or null.
PickTarget(AIController, List<ICharacter>)
Picks a valid target from the provided list using the group role, the personality's AITargetingMode, and the aggression table, in that order.
public virtual void PickTarget(AIController controller, List<ICharacter> targets)
Parameters
controllerAIControllerThe AI controller.
targetsList<ICharacter>List of potential targets.
ReevaluateTarget(AIController, float)
Periodically reconsiders the current target.
protected virtual void ReevaluateTarget(AIController controller, float deltaTime)
Parameters
controllerAIControllerThe AI controller.
deltaTimefloatSeconds elapsed since the previous AI tick.
Remarks
Threat-based NPCs switch when a tracked aggressor exceeds the current target by AggressionSwitchThreshold; a rampaging NPC instead re-rolls onto a random nearby enemy at its personality's retarget chance, which is what makes it impossible to hold with threat.
Candidates come from a fresh detection sweep. The previous implementation scanned
controller.SweepHits directly, but that buffer is only filled by
AIController's out-of-combat sweep, which is skipped for the whole
duration of a fight — so it held stale colliders from before combat started (and
entries past the last hit count from sweeps before that), and could retarget onto
something no longer present.
RefusesNewTarget(AIController)
A passive pet stops fighting the moment its ordered target is gone; it does not go looking for the next one.
protected static bool RefusesNewTarget(AIController controller)
Parameters
controllerAIControllerThe AI controller.
Returns
- bool
True if the NPC is a pet that refuses to pick a new target.
ReleaseCombatSlot(AIController)
Gives up this NPC's slot in whatever ring it currently occupies.
protected static void ReleaseCombatSlot(AIController controller)
Parameters
controllerAIControllerThe AI controller.
ResolveApproachPosition(AIController, float)
Picks the world position this attacker should walk to, given how many others are already engaging the same target.
protected Vector3 ResolveApproachPosition(AIController controller, float stopDistance)
Parameters
controllerAIControllerThe AI controller.
stopDistancefloatDistance from the target to stand at.
Returns
- Vector3
The world position to path to.
Remarks
With slots disabled, or when this attacker is the only one on its target, the NPC simply projects its own position onto a circle at range — the shortest approach, and the one that looks most natural for a single attacker. Once a second attacker joins, both take assigned angular slots so they arrive on opposite sides rather than fighting over the same metre of ground.
RetreatFromTarget(AIController, float)
Moves the NPC away from its target to the specified safe distance.
protected void RetreatFromTarget(AIController controller, float safeDistance)
Parameters
controllerAIControllerThe AI controller.
safeDistancefloatThe distance to retreat to.
TryAttack(AIController, ICharacter)
Attempts to attack the current target: picks an ability, asks AICombatDecision what to do about it, then executes that intent.
protected virtual void TryAttack(AIController controller, ICharacter targetCharacter)
Parameters
controllerAIControllerThe AI controller.
targetCharacterICharacterThe target character being attacked.
TryMovementVariety(AIController, float)
Rolls for a movement-variety transition into one of the VarietyStates.
protected bool TryMovementVariety(AIController controller, float distance)
Parameters
controllerAIControllerThe AI controller.
distancefloatCurrent distance to the target.
Returns
- bool
True if a transition happened and the caller should stop.
UpdatePetLeash(AIController)
Keeps a pet's leash anchored to its owner and breaks off the fight if the pet has chased its target too far from them.
protected bool UpdatePetLeash(AIController controller)
Parameters
controllerAIControllerThe AI controller.
Returns
- bool
False if the pet disengaged and the caller should stop.
UpdateState(AIController, float)
Called every AI tick to update the attacking state. Handles death, personality-driven flight, target loss, and attack logic.
public override void UpdateState(AIController controller, float deltaTime)
Parameters
controllerAIControllerThe AI controller.
deltaTimefloatSeconds elapsed since the previous AI tick.