diff --git a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs
index 9b42136a6..6e5f1c295 100644
--- a/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs
+++ b/Projects/UOContent/Mobiles/AI/BaseAI/BaseAI.cs
@@ -131,21 +131,51 @@ public abstract partial class BaseAI
}
}
- public virtual void OnAggressiveAction(Mobile aggressor)
+ ///
+ /// The retaliation policy: how the creature answers . Returns false when it
+ /// stands down, and then skips its combat bookkeeping.
+ ///
+ public virtual bool OnAggressiveAction(Mobile aggressor)
+ {
+ // Only a creature somebody can command has been told anything; wild creatures rest on None.
+ var toldToStandDown = Mobile.Controlled && Mobile.ControlMaster != null && Mobile.Commandable &&
+ IsStandDownOrder(Mobile.ControlOrder);
+
+ if (Mobile.StandsDownOnCommand && toldToStandDown)
+ {
+ DebugSay("I'm being attacked but my master told me not to fight.");
+ Mobile.Warmode = false;
+ return false;
+ }
+
+ PreferCloserAggressor(aggressor);
+
+ if (aggressor.ChangingCombatant && toldToStandDown)
+ {
+ // With stand-down off, a resting pet answers a direct attack with a full Attack order.
+ Mobile.IssueOrder(OrderType.Attack, null, aggressor);
+ }
+ else if (Mobile.Combatant == null && !Mobile.BardPacified)
+ {
+ Mobile.Warmode = true;
+ Mobile.Combatant = aggressor;
+ }
+
+ return true;
+ }
+
+ // A visible aggressor closer than the current combatant takes over.
+ protected void PreferCloserAggressor(Mobile aggressor)
{
if (aggressor.Hidden)
{
return;
}
- var currentCombat = Mobile.Combatant;
+ var current = Mobile.Combatant;
- if (currentCombat == null || currentCombat == aggressor)
- {
- return;
- }
-
- if (Mobile.GetDistanceToSqrt(aggressor) < Mobile.GetDistanceToSqrt(currentCombat))
+ if (current != null && current != aggressor &&
+ Mobile.GetDistanceToSqrt(aggressor) < Mobile.GetDistanceToSqrt(current))
{
Mobile.Combatant = aggressor;
}
diff --git a/Projects/UOContent/Mobiles/AI/FamiliarAI.cs b/Projects/UOContent/Mobiles/AI/FamiliarAI.cs
index fcaf96c69..2da24dc8d 100644
--- a/Projects/UOContent/Mobiles/AI/FamiliarAI.cs
+++ b/Projects/UOContent/Mobiles/AI/FamiliarAI.cs
@@ -47,21 +47,25 @@ public class FamiliarAI : BaseAI
return Mobile.Controlled ? OrderType.Come : OrderType.None;
}
- public override void OnAggressiveAction(Mobile aggressor)
+ // No orders to stand down on; never fights for a hidden caster.
+ public override bool OnAggressiveAction(Mobile aggressor)
{
if (!Familiar.AssistsMaster || aggressor.Hidden || Familiar.ControlMaster?.Hidden == true)
{
- return;
+ return false;
}
if (Mobile.Combatant == null)
{
Mobile.Warmode = true;
Mobile.Combatant = aggressor;
- return;
+ }
+ else
+ {
+ PreferCloserAggressor(aggressor);
}
- base.OnAggressiveAction(aggressor);
+ return true;
}
private bool Act()
diff --git a/Projects/UOContent/Mobiles/BaseCreature.cs b/Projects/UOContent/Mobiles/BaseCreature.cs
index 9d7e77959..f159c70e1 100644
--- a/Projects/UOContent/Mobiles/BaseCreature.cs
+++ b/Projects/UOContent/Mobiles/BaseCreature.cs
@@ -2735,18 +2735,12 @@ namespace Server.Mobiles
aggressor.Aggressors.Add(AggressorInfo.Create(this, aggressor, true));
}
- var toldToStandDown = _controlled && _controlMaster != null && Commandable &&
- BaseAI.IsStandDownOrder(_controlOrder);
-
- if (StandsDownOnCommand && toldToStandDown)
+ // The AI owns the retaliation policy; a creature that stands down keeps its flee and acquire state.
+ if (AIObject?.OnAggressiveAction(aggressor) != true)
{
- AIObject?.DebugSay("I'm being attacked but my master told me not to fight.");
- Warmode = false;
return;
}
- AIObject?.OnAggressiveAction(aggressor);
-
StopFlee();
ForceReacquire();
@@ -2760,17 +2754,6 @@ namespace Server.Mobiles
pl.FinishShield();
}
}
-
- // Only reachable when the pet does not stand down: the orders above returned early.
- if (aggressor.ChangingCombatant && toldToStandDown)
- {
- IssueOrder(OrderType.Attack, null, aggressor);
- }
- else if (Combatant == null && !BardPacified)
- {
- Warmode = true;
- Combatant = aggressor;
- }
}
public override bool OnMoveOver(Mobile m)
diff --git a/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md b/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md
index 7310c104e..9a7c08ab4 100644
--- a/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md
+++ b/dev-docs/claude-skills/migrate-from-runuo/migrate-items-mobiles.md
@@ -31,6 +31,7 @@ description: >
- Expression-bodied overrides: `public override int Meat { get { return 1; } }` -> `public override int Meat => 1;`
- AI movement calls lose the `run` flag: `MoveTo(m, true, range)` -> `MoveTo(m, range)` (also `WalkMobileRange`, `ApproachTarget`, `MoveToPoint`, `PathFollower.Follow`); the Running bit is derived from step pace -> `dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md` § AI Movement
- `AcquireOnApproach` (bool) -> `AcquireOnApproachDelay` (TimeSpan; `Zero` = old instant behavior) -> same doc § Target Acquisition
+- `BaseAI.OnAggressiveAction(aggressor)` is `bool`, not `void`: the AI owns the retaliation policy (`BaseCreature` no longer sets `Combatant` after it); an early `return;` that refused a fight -> `return false;`, otherwise `return true;`; swap-only overrides call `PreferCloserAggressor(aggressor)` instead of `base` -> same doc § Retaliation
- `DamageEntries` is an inline `ref readonly ValueLinkList`, not a `List`: indexer/`Add`/`Remove`/`Clear` -> `foreach` / `.ByDescending()` (needs `using Server.Collections;`) and `ClearDamageEntries()`; `GetLootingRights` takes it by `in` -> same doc § Damage Entries
## Anti-Patterns
diff --git a/dev-docs/content-patterns.md b/dev-docs/content-patterns.md
index 85ce6075c..e583364c6 100644
--- a/dev-docs/content-patterns.md
+++ b/dev-docs/content-patterns.md
@@ -231,6 +231,9 @@ class — `OnThink` is for content extras (see the excess-call contract below).
creature is dispatched through `Obey()`, an uncontrolled one through `Think()`; an AI that owns
both routes them into one decision (`FamiliarAI.Act`). Overriding `IssueOrder` to return a
fixed order is how "command immunity" is expressed without bypassing the order machinery.
+`OnAggressiveAction(aggressor) -> bool` is the retaliation policy: `BaseCreature` sets no
+combatant of its own and runs `StopFlee`/`ForceReacquire` only on `true`, so an AI that
+returns `false` has refused the fight outright.
### Fight Modes
| FightMode | Behavior |
diff --git a/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md b/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md
index def2ecf48..389c845e5 100644
--- a/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md
+++ b/dev-docs/runuo-migration-docs/09-items-mobiles-creatures.md
@@ -497,6 +497,49 @@ An isolated step (after the creature stood for at least a walk interval) goes ou
walk regardless of pace — only a continuing cadence, or a pace faster than the run
interpolation, flags run.
+## Retaliation: `OnAggressiveAction` Returns `bool`
+
+RunUO's `BaseAI.OnAggressiveAction` was a `void` hook that only swapped to a closer
+aggressor; `BaseCreature.AggressiveAction` then decided the response itself and set
+`Combatant` regardless of what the AI had done. ModernUO moves the whole retaliation policy
+into the hook — the stand-down verdict for commanded pets, the closer-aggressor swap
+(`PreferCloserAggressor`), and the `IssueOrder(Attack)` vs `Combatant =` choice — and
+`BaseCreature` only runs its bookkeeping (`StopFlee`, `ForceReacquire`, ethics shield) when
+the hook returns `true`:
+
+```csharp
+// RunUO — swap only; BaseCreature picks the combatant afterwards
+public override void OnAggressiveAction(Mobile aggressor)
+{
+ if (m_Mobile.Combatant == null)
+ {
+ m_Mobile.Combatant = aggressor;
+ return;
+ }
+
+ base.OnAggressiveAction(aggressor);
+}
+
+// ModernUO — the AI owns the response; false = stood down, nothing else fires
+public override bool OnAggressiveAction(Mobile aggressor)
+{
+ if (Mobile.Combatant == null)
+ {
+ Mobile.Combatant = aggressor;
+ }
+ else
+ {
+ PreferCloserAggressor(aggressor);
+ }
+
+ return true;
+}
+```
+
+An override that used to `return;` early to refuse a fight now returns `false`; calling
+`base.OnAggressiveAction` runs the full stock policy, including setting `Combatant`, so an AI
+that only wants the closer-aggressor swap calls `PreferCloserAggressor` instead.
+
## Target Acquisition: `AcquireOnApproach` Is a Delay
RunUO's `AcquireOnApproach` bool (paragon insta-aggro on approach) is now
diff --git a/dev-docs/runuo-migration-docs/11-api-reference.md b/dev-docs/runuo-migration-docs/11-api-reference.md
index b6c12bfe8..788a7cd78 100644
--- a/dev-docs/runuo-migration-docs/11-api-reference.md
+++ b/dev-docs/runuo-migration-docs/11-api-reference.md
@@ -134,6 +134,7 @@ Alphabetical by RunUO API name. Use Ctrl+F / Cmd+F to search.
| `WalkMobileRange(m, steps, run, min, max)` | `WalkMobileRange(m, steps, min, max)` | Same |
| `PathFollower.Follow(run, range)` | `Follow(range)` | Same |
| `AcquireOnApproach` (bool) | `AcquireOnApproachDelay` (TimeSpan) | Reaction-time gradient; `Zero` = old instant behavior |
+| `BaseAI.OnAggressiveAction(m)` (`void`) | `OnAggressiveAction(m)` (`bool`) | The AI owns the retaliation policy; `false` = stood down; swap-only overrides call `PreferCloserAggressor` |
| `m.DamageEntries` (`List`) | `m.DamageEntries` (`ref readonly ValueLinkList`) | Inline, least→most recent; `foreach` / `.ByDescending()` only, needs `using Server.Collections;`; no indexer, `Add`, `Remove`, `Clear` |
| `m.DamageEntries.Clear()` | `m.ClearDamageEntries()` | |
| `GetLootingRights(List, int)` | `GetLootingRights(in ValueLinkList, int)` | Callers passing `m.DamageEntries` compile unchanged |