fix(ai): restore pre-AOS berserk energy vortexes and blade spirits (#2666)

#2132 let a player's energy vortex or blade spirits in pre-AOS Felucca target their own caster, gated by `FollowsAcquireRules`. #2232's AI overhaul moved the summon checks into `IsInvalidSummonTarget`, which excluded the caster in every era, and dropped the gate. Since then `FollowsAcquireRules` has had no readers and summons have never turned on their caster.

## Changes

- `IsInvalidSummonTarget` reads `FollowsAcquireRules` again. A summon that follows the acquire rules spares its caster and anyone the caster's harmful spells would spare. The vortex and blade spirits don't follow them in pre-AOS Felucca when a player summoned them.
- The animated-dead restrictions (no players, no other animated dead, no pets) apply to every animated summon regardless of that setting.
- `AcquireNewFocusMob` sets a berserk summon's caster aside and targets them only when nobody else is available. #2132 meant to prefer others, but its check only worked when the caster was scanned before the other candidates, and the vortex's Int-plus-Magery ranking usually puts a nearby caster first.
- The AOS summon-target check no longer dereferences a missing `SummonMaster`. A summon whose caster was deleted before a reload comes back without one.

Controlled summons are unaffected: since #2665 they target through `HandleControlled` and never reach this scan.

## Tests

`SummonAcquireRulesTests` has 6 cases, skipped without client map data. Pre-AOS Felucca: a vortex and blade spirits each target their caster when the caster is alone, and a vortex targets a bystander even when the caster ranks higher. Pre-AOS Trammel and AOS Felucca: the caster is still spared. An AOS scan next to a summon with no master doesn't throw. `UOContent.Tests`: 1148 passed, 2 skipped, 0 failed on Windows.
This commit is contained in:
Kamron Batman 2026-09-23 10:29:20 -07:00 • committed by GitHub
parent 6b910ff089
commit 3eb3f0fb5c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 149 additions and 13 deletions

View file

@ -996,6 +996,7 @@ public abstract partial class BaseAI
Mobile enemySummonMob = null;
var val = double.MinValue;
var enemySummonVal = double.MinValue;
Mobile summonMaster = null;
foreach (var m in map.GetMobilesInRange(Mobile.Location, iRange))
{
@ -1005,14 +1006,24 @@ public abstract partial class BaseAI
}
var bc = m as BaseCreature;
var pm = m as PlayerMobile;
if (IsInvalidSummonTarget(m, bc, pm) || IsInvalidFactionTarget(m, bFacFriend, bFacFoe)
|| IsInvalidFightModeTarget(m, acqType, bc))
if (IsInvalidSummonTarget(m, bc) || IsInvalidFactionTarget(m, bFacFriend, bFacFoe)
|| IsInvalidFightModeTarget(m, acqType, bc))
{
continue;
}
// Only a summon that ignores the acquire rules gets here with its caster; anyone else comes first.
if (m == Mobile.SummonMaster)
{
if (Mobile.InLOS(m))
{
summonMaster = m;
}
continue;
}
var theirVal = Mobile.GetFightModeRanking(m, acqType, bPlayerOnly);
if (theirVal > val && Mobile.InLOS(m))
@ -1020,15 +1031,14 @@ public abstract partial class BaseAI
newFocusMob = m;
val = theirVal;
}
else if (Core.AOS && theirVal > enemySummonVal
&& Mobile.InLOS(m) && bc?.Summoned == true && bc.Controlled != true)
else if (Core.AOS && theirVal > enemySummonVal && Mobile.InLOS(m) && bc is { Summoned: true, Controlled: false })
{
enemySummonMob = m;
enemySummonVal = theirVal;
}
}
Mobile.FocusMob = newFocusMob ?? enemySummonMob;
Mobile.FocusMob = newFocusMob ?? enemySummonMob ?? summonMaster;
return Mobile.FocusMob != null;
}
@ -1036,21 +1046,28 @@ public abstract partial class BaseAI
m.Deleted || m.Blessed || m == Mobile || m is BaseFamiliar || !m.Alive || m.IsDeadBondedPet ||
m.AccessLevel > AccessLevel.Player || bPlayerOnly && !m.Player || !Mobile.CanSee(m);
private bool IsInvalidSummonTarget(Mobile m, BaseCreature bc, PlayerMobile pm)
private bool IsInvalidSummonTarget(Mobile m, BaseCreature bc)
{
// A summon whose caster was deleted before a reload comes back with no master.
if (Core.AOS && bc?.Summoned == true &&
(bc.SummonMaster == Mobile || !bc.SummonMaster.Player && IsHostile(bc.SummonMaster)))
(bc.SummonMaster == Mobile || bc.SummonMaster is { Player: false } summonMaster && IsHostile(summonMaster)))
{
return true;
}
if (!Mobile.Summoned || Mobile.SummonMaster == null)
var master = Mobile.SummonMaster;
if (!Mobile.Summoned || master == null)
{
return false;
}
return m == Mobile.SummonMaster || !SpellHelper.ValidIndirectTarget(Mobile.SummonMaster, m) ||
Mobile.IsAnimatedDead && (pm != null || bc?.IsAnimatedDead == true || bc?.Controlled == true);
if (Mobile.IsAnimatedDead && (m.Player || bc?.IsAnimatedDead == true || bc?.Controlled == true))
{
return true;
}
return Mobile.FollowsAcquireRules && (m == master || !SpellHelper.ValidIndirectTarget(master, m));
}
private bool IsInvalidFactionTarget(Mobile m, bool bFacFriend, bool bFacFoe) =>