fix(ai): guarding pets never target their owner or the owner's pets (#2665)
Supersedes #2657 (rebased onto #2662, which the original branch conflicted with). Credit to @MithrilHammer for the diagnosis, the fix and the regression tests; their three commits are kept as authored. Fixes #2656. ## Root cause When a guarding pet's combatant hides or becomes invalid, its AI calls `AcquireFocusMob` to find a new target. A controlled creature with no `ControlTarget` (Guard normally has none) fell out of `HandleControlled` and into the wild-creature scan, where `IsEnemy` treats any player as an enemy. With `FightMode.Closest`, the owner won. The fall-through dates to #2232. RunUO never let a controlled creature reach the wild scan. ## Changes - `AcquireFocusMob`: a controlled creature returns whatever `HandleControlled` decides. Guard uses `FindGuardTarget`, Attack uses its `ControlTarget`, and every other order acquires nothing. - `FindGuardTarget` skips guard allies: the master and creatures the master controls, including controlled summons. Uncontrolled summons (energy vortex, blade spirits) attack their caster, so a guard still defends against them, as in RunUO. - Friendly fire: `BaseAI.OnAggressiveAction` stands a guard down when an ally is the aggressor. `BaseCreature.Combatant` refuses an ally while guarding, because `Mobile.AggressiveAction` assigns the first aggressor as `Combatant` before any retaliation policy runs. The veto sits on `Combatant` rather than `CanBeHarmful`, so what a pet can harm doesn't depend on its current order. An explicit Attack order and bard provocation can still target an ally. ## Tests 16 regression cases in `GuardFriendlyFireTests` (skipped without client map data), including a real `WhiteWyrm` reproduction. `UOContent.Tests`: 1142 passed, 2 skipped, 0 failed on Windows.
This commit is contained in:
parent
28c38168bc
commit
6b910ff089
4 changed files with 287 additions and 5 deletions
|
|
@ -137,6 +137,12 @@ public abstract partial class BaseAI
|
|||
/// </summary>
|
||||
public virtual bool OnAggressiveAction(Mobile aggressor)
|
||||
{
|
||||
if (Mobile.RefusesGuardTarget(aggressor))
|
||||
{
|
||||
DebugSay("Friendly fire; I keep guarding.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
|
@ -856,7 +862,19 @@ public abstract partial class BaseAI
|
|||
return false;
|
||||
}
|
||||
|
||||
if (HandleBardProvoked() || HandleControlled() || HandleConstantFocus())
|
||||
if (HandleBardProvoked())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// A controlled creature must not fall through to wild target selection when
|
||||
// its order has no target. Guard in particular normally has no ControlTarget.
|
||||
if (Mobile.Controlled)
|
||||
{
|
||||
return HandleControlled();
|
||||
}
|
||||
|
||||
if (HandleConstantFocus())
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
|
@ -911,8 +929,17 @@ public abstract partial class BaseAI
|
|||
|
||||
private bool HandleControlled()
|
||||
{
|
||||
if (!Mobile.Controlled)
|
||||
if (Mobile.ControlOrder == OrderType.Guard)
|
||||
{
|
||||
Mobile.FocusMob = FindGuardTarget();
|
||||
return Mobile.FocusMob != null;
|
||||
}
|
||||
|
||||
// Follow and other non-combat orders may target the owner or a friend.
|
||||
// Only an explicit attack order supplies a combat target.
|
||||
if (Mobile.ControlOrder != OrderType.Attack)
|
||||
{
|
||||
Mobile.FocusMob = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -769,6 +769,15 @@ public abstract partial class BaseAI
|
|||
}
|
||||
}
|
||||
|
||||
// Uncontrolled summons (energy vortex, blade spirits) turn on their caster, so a guard still defends against them.
|
||||
internal bool IsGuardAlly(Mobile target)
|
||||
{
|
||||
var master = Mobile.ControlMaster;
|
||||
|
||||
return target == Mobile || master != null &&
|
||||
(target == master || target is BaseCreature { Controlled: true } creature && creature.ControlMaster == master);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Selects the aggressor closest to the master. The current combatant is kept
|
||||
/// unless a strictly closer one exists. Never mutates order state.
|
||||
|
|
@ -779,12 +788,12 @@ public abstract partial class BaseAI
|
|||
var anchor = controlMaster ?? Mobile;
|
||||
|
||||
var current = Mobile.Combatant;
|
||||
var best = current != controlMaster && IsValidCombatant(current) ? current : null;
|
||||
var best = !IsGuardAlly(current) && IsValidCombatant(current) ? current : null;
|
||||
var bestDist = best?.GetDistanceToSqrt(anchor) ?? double.MaxValue;
|
||||
|
||||
foreach (var aggr in Mobile.GetMobilesInRange(Mobile.RangePerception))
|
||||
{
|
||||
if (aggr == best || aggr == Mobile || aggr == controlMaster ||
|
||||
if (aggr == best || IsGuardAlly(aggr) ||
|
||||
aggr.IsDeadBondedPet || !aggr.Alive ||
|
||||
aggr.Combatant != Mobile && (controlMaster == null || aggr.Combatant != controlMaster))
|
||||
{
|
||||
|
|
@ -808,7 +817,7 @@ public abstract partial class BaseAI
|
|||
{
|
||||
var aggressor = aggressors[i].Attacker;
|
||||
|
||||
if (aggressor == best || aggressor?.Deleted != false || !aggressor.Alive ||
|
||||
if (aggressor == best || aggressor?.Deleted != false || IsGuardAlly(aggressor) || !aggressor.Alive ||
|
||||
aggressor.IsDeadBondedPet || !Mobile.InRange(aggressor, Mobile.RangePerception))
|
||||
{
|
||||
continue;
|
||||
|
|
|
|||
|
|
@ -3650,6 +3650,26 @@ namespace Server.Mobiles
|
|||
m?.InvalidateProperties();
|
||||
}
|
||||
|
||||
// A guard never fights its master or the master's other pets. An explicit Attack order and bard
|
||||
// provocation still may.
|
||||
public bool RefusesGuardTarget(Mobile target) =>
|
||||
_controlled && _controlOrder == OrderType.Guard && !BardProvoked && AIObject?.IsGuardAlly(target) == true;
|
||||
|
||||
// Mobile.AggressiveAction assigns the first aggressor as Combatant before any retaliation policy runs.
|
||||
public override Mobile Combatant
|
||||
{
|
||||
get => base.Combatant;
|
||||
set
|
||||
{
|
||||
if (value != null && RefusesGuardTarget(value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
base.Combatant = value;
|
||||
}
|
||||
}
|
||||
|
||||
public override bool CanBeHarmful(Mobile target, bool message, bool ignoreOurBlessedness)
|
||||
{
|
||||
if (target is BaseFactionGuard)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue