Table of Contents

Class BaseAttackingState

Namespace
FishMMO.Shared
Assembly
FishMMO.Shared.dll

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
Object
ScriptableObject
BaseAttackingState
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.name
Object.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

float

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

float

AttackCooldownJitter

Maximum extra seconds randomly added to AttackCooldown per activation.

[Tooltip("Maximum random jitter added to AttackCooldown.")]
public float AttackCooldownJitter

Field Value

float

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

BaseAIState

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

float

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

float

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

float

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

float

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

float

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

float

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

float

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

bool

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

float

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

List<BaseAIState>

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

controller AIController

The AI controller.

abilityController IAbilityController

The ability controller to activate on.

ability Ability

The 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

controller AIController

The AI controller.

distance float

Current distance to the target.

chosenAbility Ability

The 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

controller AIController

The 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

controller AIController

The AI controller.

abilityController IAbilityController

The NPC's ability controller.

targetCharacter ICharacter

The target character.

plan AICombatPlan

The decision to execute.

context AICombatContext

The context the decision was made from.

chosenAbility Ability

The 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

controller AIController

The AI controller.

FirstAlive(List<ICharacter>)

Returns the first living, active candidate in the list.

protected static ICharacter FirstAlive(List<ICharacter> targets)

Parameters

targets List<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

controller AIController

The 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

controller AIController

The AI controller.

abilityController IAbilityController

The 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

controller AIController

The AI controller.

stopRange float

Distance 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

ability Ability

The 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

controller AIController

The AI controller.

stopRange float

Distance 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

controller AIController

The 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

controller AIController

The AI controller.

abilityController IAbilityController

The ability controller to activate on.

ability Ability

The chosen ability to activate.

targetCharacter ICharacter

The target character being attacked.

distance float

Current 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

controller AIController

The 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

controller AIController

The AI controller.

targets List<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

controller AIController

The AI controller with group and role info.

targets List<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

controller AIController

The AI controller.

targets List<ICharacter>

List of potential targets.

ReevaluateTarget(AIController, float)

Periodically reconsiders the current target.

protected virtual void ReevaluateTarget(AIController controller, float deltaTime)

Parameters

controller AIController

The AI controller.

deltaTime float

Seconds 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

controller AIController

The 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

controller AIController

The 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

controller AIController

The AI controller.

stopDistance float

Distance 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

controller AIController

The AI controller.

safeDistance float

The 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

controller AIController

The AI controller.

targetCharacter ICharacter

The 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

controller AIController

The AI controller.

distance float

Current 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

controller AIController

The 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

controller AIController

The AI controller.

deltaTime float

Seconds elapsed since the previous AI tick.