## Symptom Since #2614, on any shard with `taming.petsStandDownOnCommand` (default `Core.ML`), every wild creature hits `"I'm being attacked but my master told me not to fight."` when struck. Brigands still chase (acquisition is a separate path in the think loop) but the retaliation path is dead: no `OnAggressiveAction`, no `StopFlee`, no `ForceReacquire`, `Combatant` never set, and `Warmode` forced off on every hit. ## Root cause #2614 correctly added `OrderType.None` to `BaseAI.IsStandDownOrder` — a stopped pet rests on `None` and Publish 51 says it must not fight back. But the gate in `BaseCreature.AggressiveAction` never asked whether anybody could have given the order. The old predicate (`ct != Follow && ct != Stop && ct != Stay`) had only excluded wild creatures by accident: `None` was not in its set, so nothing ever needed to spell the check out. A wild creature's `_controlOrder` is always `None`. ## Fix Both halves of `AggressiveAction` now gate on `Controlled && ControlMaster != null && Commandable` — the same predicate every order entry point already uses (`OnSpeech`, context menu, `IsValidTarget`). Only a creature somebody can command has been told to stand down. That excludes, and lets fight back: | Creature | Why it was standing down | |---|---| | Wild creature | rests on `None` | | Energy vortex, blade spirits, animated weapon, animate dead | `Summoned` with a `SummonMaster` but never `Controlled`; rests on `None` | | Familiar, talisman summon, escortee, mirror image | `Controlled` with a master but `Commandable => false`; sits on a system-issued `Follow` | The last row is a deliberate behaviour change from #2614 for ML+ shards: a familiar or escortee on `Follow` no longer stands down when attacked. The publish speaks of commanded pets; a creature that cannot take an order was never told anything, and pre-ML it always fought back. Commandable summons (Summon Creature, elementals, daemons) are `Controlled` with `ControlMaster == SummonMaster`, so they stand down exactly as pets do. ## Also: stop after stay Found while testing: `all come` then `all stop` left the pet on `Stay`, ticking *"I have been ordered to stay"*. `come` rests into `Stay` on arrival (deliberate ModernUO divergence, kept), and #2614 had `IssueStop` keep a previous `Stay`. RunUO and ServUO never consult the previous order — `DoOrderStop` is "wander around here" (or `None` pre-ML) — and Publish 51 says a stopped pet *"may wander"*. `Stay` now joins `Follow`/`Guard` in `IssueStop`: stop cancels the standing order and the pet idles anchored where it stands. `Stop_WhileStaying_RemainsStayingAtOriginalPost` is replaced by `Stop_WhileStaying_CancelsToIdleNone` plus the `come, stop` repro. ## Tests `PetRetaliationTests` gains one case per row above: `WildCreature_`, `UncontrolledSummon_`, `UncommandableCreature_Retaliates_UnderStandDown`. Each fails on main and passes here; full `UOContent.Tests` green (1058 passed).
1062 lines
39 KiB
C#
1062 lines
39 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using Server;
|
|
using Server.Mobiles;
|
|
using Xunit;
|
|
|
|
namespace UOContent.Tests.Mobiles.AI;
|
|
|
|
[Collection("Sequential UOContent Tests")]
|
|
public class PetOrderTests : IDisposable
|
|
{
|
|
// Track and delete every mobile we spawn so they don't linger in the shared static World
|
|
// and pollute other tests in this collection (e.g. Tracking's nearby-mobile scan).
|
|
private readonly List<Mobile> _created = new();
|
|
|
|
private (PlayerMobile master, PetTestStub pet) Spawn(Point3D masterLoc, Point3D petLoc)
|
|
{
|
|
var pair = PetTestSetup.SpawnControlledPet(masterLoc, petLoc);
|
|
_created.Add(pair.master);
|
|
_created.Add(pair.pet);
|
|
return pair;
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
foreach (var m in _created)
|
|
{
|
|
m?.Delete();
|
|
}
|
|
|
|
_created.Clear();
|
|
}
|
|
[Fact]
|
|
public void SetPersistentOrder_Stay_AnchorsHomeToCurrentLocation()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1005, 1000, 0));
|
|
|
|
pet.AIObject.SetPersistentOrder(OrderType.Stay);
|
|
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void SetPersistentOrder_Follow_ClearsAnchor()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1005, 1000, 0));
|
|
pet.Home = new Point3D(900, 900, 0); // stale anchor
|
|
|
|
pet.AIObject.SetPersistentOrder(OrderType.Follow);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(Point3D.Zero, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_WhileAttacking_FallsBackToPersistentFollow()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1002, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow; // persistent = Follow
|
|
pet.ControlOrder = OrderType.Attack; // transient
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
|
|
pet.ControlOrder = OrderType.Stop;
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder); // resumed standing order
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_WhileAttacking_ResumedFollowTargetsTheMaster()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlTarget = master;
|
|
pet.ControlOrder = OrderType.Follow; // persistent = Follow
|
|
pet.ControlOrder = OrderType.Attack; // transient
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
|
|
pet.ControlOrder = OrderType.Stop;
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Equal(master, pet.ControlTarget);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_WhileAttacking_ResumedFollowKeepsAPetFriendAsItsTarget()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = new PlayerMobile(World.NewMobile);
|
|
friend.DefaultMobileInit();
|
|
friend.MoveToWorld(new Point3D(1002, 1000, 0), pet.Map);
|
|
_created.Add(friend);
|
|
|
|
var victim = new PetTestStub();
|
|
victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map);
|
|
_created.Add(victim);
|
|
|
|
pet.ControlTarget = friend; // a pet friend said "all follow me"
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.ControlTarget = victim; // then "all kill" — the attack order takes the target
|
|
pet.ControlOrder = OrderType.Attack;
|
|
|
|
pet.ControlOrder = OrderType.Stop;
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Equal(friend, pet.ControlTarget); // still the friend, not the owner
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_WhileAttacking_ResumedFollowSurvivesTheNextThink()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.ControlOrder = OrderType.Attack;
|
|
pet.ControlOrder = OrderType.Stop; // resumes Follow
|
|
|
|
pet.AIObject.Obey();
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder); // not dropped to idle
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_WhileFollowing_CancelsToIdleNone()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1002, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow;
|
|
|
|
pet.ControlOrder = OrderType.Stop;
|
|
|
|
Assert.Equal(OrderType.None, pet.ControlOrder);
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(pet.Location, pet.Home); // idle anchor = where stopped
|
|
}
|
|
|
|
// Publish 51: "Stop: ... may wander." A stay is a standing order like follow or guard;
|
|
// stop cancels it and the pet idles, anchored where it stands.
|
|
[Fact]
|
|
public void Stop_WhileStaying_CancelsToIdleNone()
|
|
{
|
|
var post = new Point3D(1005, 1005, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
pet.ControlOrder = OrderType.Stay; // Home = post
|
|
Assert.Equal(post, pet.Home);
|
|
|
|
pet.ControlOrder = OrderType.Stop;
|
|
|
|
Assert.Equal(OrderType.None, pet.ControlOrder);
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(pet.Location, pet.Home); // idle anchor = where stopped
|
|
}
|
|
|
|
// "all come" rests into Stay on arrival; "all stop" must not leave the pet at that post.
|
|
[Fact]
|
|
public void Come_ThenArrive_ThenStop_IdlesRatherThanStaying()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
Assert.Equal(OrderType.Come, pet.ControlOrder);
|
|
|
|
pet.AIObject.Obey(); // within 2 tiles -> Stay
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
|
|
pet.ControlOrder = OrderType.Stop;
|
|
|
|
Assert.Equal(OrderType.None, pet.ControlOrder);
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stay_ThenFollow_ThenStop_DoesNotReturnToOldStayAnchor() // report 2
|
|
{
|
|
var postA = new Point3D(1005, 1005, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), postA);
|
|
pet.ControlOrder = OrderType.Stay; // Home = A
|
|
pet.ControlOrder = OrderType.Follow; // Home cleared to Zero
|
|
pet.MoveToWorld(new Point3D(1050, 1050, 0), pet.Map); // walked to B
|
|
pet.ControlOrder = OrderType.Stop; // stop while following
|
|
|
|
Assert.NotEqual(postA, pet.Home); // never re-acquires A
|
|
Assert.Equal(pet.Location, pet.Home); // idles at B
|
|
}
|
|
|
|
[Fact]
|
|
public void AttackTargetLost_ResumesPersistentFollow()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1002, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow; // persistent = Follow
|
|
pet.ControlOrder = OrderType.Attack;
|
|
pet.ControlTarget = null; // target gone
|
|
|
|
pet.AIObject.DoOrderAttack(); // invalid-target path
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void StayPet_AttacksThenTargetLost_ReturnsToOriginalPost() // report 1
|
|
{
|
|
var post = new Point3D(1005, 1005, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
pet.ControlOrder = OrderType.Stay; // persistent = Stay, Home = post
|
|
pet.ControlOrder = OrderType.Attack;
|
|
pet.MoveToWorld(new Point3D(1060, 1060, 0), pet.Map); // chased far to the "corpse"
|
|
pet.ControlTarget = null;
|
|
|
|
pet.AIObject.DoOrderAttack();
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(post, pet.Home); // anchor still the original post, not the corpse
|
|
}
|
|
|
|
// NOTE: the test fixture does not load tile data, so Mobile.Move is blocked and Location
|
|
// never changes here. DoMoveImpl still sets Mobile.Direction before the (blocked) move,
|
|
// so an *attempted* wander is observable via Direction. The subjective wander cadence is
|
|
// covered by manual QA; these tests verify the gate/frozen wiring deterministically.
|
|
[Fact]
|
|
public void IdlePet_DoesNotAttemptToMove_WhileResting()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1002, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.ControlOrder = OrderType.Stop; // -> idle None
|
|
Assert.Equal(OrderType.None, pet.ControlOrder);
|
|
|
|
pet.ForceIdle = true; // CheckIdle() reports resting -> idle wander must be skipped
|
|
pet.Direction = Direction.North;
|
|
for (var i = 0; i < 40; i++)
|
|
{
|
|
pet.AIObject.DoOrderNone();
|
|
}
|
|
|
|
Assert.Equal(Direction.North, pet.Direction); // gated by CheckIdle -> never attempts a step
|
|
}
|
|
|
|
[Fact]
|
|
public void StayingPet_DoesNotAttemptToMove()
|
|
{
|
|
var post = new Point3D(1005, 1005, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
pet.ControlOrder = OrderType.Stay;
|
|
pet.Direction = Direction.North;
|
|
|
|
for (var i = 0; i < 40; i++)
|
|
{
|
|
pet.AIObject.DoOrderStay();
|
|
}
|
|
|
|
Assert.Equal(Direction.North, pet.Direction); // frozen -> no wander attempts
|
|
}
|
|
|
|
[Fact]
|
|
public void ReleaseOrder_ClearsTheMaster_AndStartsTheDeleteCountdown()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.IsBonded = true;
|
|
var followers = master.Followers;
|
|
|
|
pet.ControlOrder = OrderType.Release;
|
|
|
|
Assert.False(pet.Controlled);
|
|
Assert.Null(pet.ControlMaster);
|
|
Assert.False(pet.IsBonded);
|
|
Assert.Equal(followers - pet.ControlSlots, master.Followers);
|
|
Assert.True(pet.PendingDeleteTimer?.Running);
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoyaltyRelease_ClearsTheMaster_AndStartsTheDeleteCountdown()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var followers = master.Followers;
|
|
|
|
// What the loyalty drain assigns when loyalty reaches zero.
|
|
pet.ControlOrder = OrderType.Release;
|
|
|
|
Assert.False(pet.Controlled);
|
|
Assert.Null(pet.ControlMaster);
|
|
Assert.Equal(followers - pet.ControlSlots, master.Followers);
|
|
Assert.True(pet.PendingDeleteTimer?.Running);
|
|
}
|
|
|
|
[Fact]
|
|
public void Release_WithoutSpawner_AnchorsHomeToCurrentLocation()
|
|
{
|
|
var loc = new Point3D(1010, 1010, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), loc);
|
|
pet.ControlOrder = OrderType.Stay; // sets Home to loc
|
|
pet.Home = new Point3D(800, 800, 0); // simulate a stale anchor
|
|
pet.Spawner = null;
|
|
|
|
pet.ControlOrder = OrderType.Release;
|
|
|
|
Assert.Equal(loc, pet.Home); // released where it stands, not the stale point
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_NearMaster_DerivesFollow()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
|
|
PetLoginHandler.DeriveFollowerOrders(master);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_FarFromMaster_DerivesStay()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1040, 1000, 0));
|
|
|
|
PetLoginHandler.DeriveFollowerOrders(master);
|
|
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_RestoredStay_KeepsItsPostAnchor()
|
|
{
|
|
var post = new Point3D(1005, 1005, 0);
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
pet.ControlOrder = OrderType.Stay; // Home = post
|
|
pet.ChangeAIType(pet.AI); // what AfterDeserialization does: fresh AI, PersistentOrder = None
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
|
|
PetLoginHandler.DeriveFollowerOrders(master); // master within 12 tiles
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(post, pet.Home); // not zeroed by a proximity-derived Follow
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_RestoredNone_NearMaster_IssuesFollow()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.ControlOrder = OrderType.Stop; // -> None, no standing order
|
|
pet.ChangeAIType(pet.AI);
|
|
master.Hidden = true;
|
|
|
|
PetLoginHandler.DeriveFollowerOrders(master);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Same(master, pet.ControlTarget);
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
Assert.True(master.Hidden); // system-issued: nobody revealed
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_RestoredAttack_FarFromMaster_IssuesStay()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1040, 1000, 0));
|
|
pet.ControlOrder = OrderType.Attack; // rests with no valid target
|
|
pet.ChangeAIType(pet.AI);
|
|
|
|
PetLoginHandler.DeriveFollowerOrders(master);
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void Login_RestoredAttack_NearMaster_FollowsTheMaster_NotTheVictim()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = SpawnPlayer(new Point3D(1003, 1000, 0));
|
|
pet.IssueOrder(OrderType.Attack, master, victim); // saved mid-fight: ControlTarget = victim
|
|
pet.ChangeAIType(pet.AI); // post-load fresh AI
|
|
|
|
PetLoginHandler.DeriveFollowerOrders(master);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Same(master, pet.ControlTarget);
|
|
Assert.Equal(OrderType.Follow, pet.AIObject.PersistentOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_WhileFollowing_CancelsToIdle_NonML()
|
|
{
|
|
var previous = Core.Expansion;
|
|
try
|
|
{
|
|
Core.Expansion = Expansion.SE; // pre-ML: Core.ML is false
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1002, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow;
|
|
|
|
pet.ControlOrder = OrderType.Stop;
|
|
|
|
// Stop/idle resolution is era-independent.
|
|
Assert.Equal(OrderType.None, pet.ControlOrder);
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
finally
|
|
{
|
|
Core.Expansion = previous;
|
|
}
|
|
}
|
|
// The parameterless ctor fully initializes a player; the Serial ctor leaves that to Deserialize.
|
|
private PlayerMobile SpawnPlayer(Point3D loc)
|
|
{
|
|
var pm = new PlayerMobile { Player = true };
|
|
pm.MoveToWorld(loc, Map.Felucca);
|
|
_created.Add(pm);
|
|
return pm;
|
|
}
|
|
|
|
// Administrative commands are not a change of what the pet is doing: it keeps fighting.
|
|
[Fact]
|
|
public void Drop_MidAttack_KeepsTheAttackAndItsTarget()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = new PetTestStub();
|
|
victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map);
|
|
_created.Add(victim);
|
|
|
|
pet.ControlOrder = OrderType.Follow; // standing order
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
|
|
pet.IssueOrder(OrderType.Drop, master);
|
|
|
|
Assert.Equal(OrderType.Attack, pet.ControlOrder);
|
|
Assert.Same(victim, pet.ControlTarget);
|
|
Assert.Same(victim, pet.Combatant);
|
|
}
|
|
|
|
[Fact]
|
|
public void Rename_MidAttack_KeepsTheAttack()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = new PetTestStub();
|
|
victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map);
|
|
_created.Add(victim);
|
|
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
|
|
pet.IssueOrder(OrderType.Rename, master);
|
|
|
|
Assert.Equal(OrderType.Attack, pet.ControlOrder);
|
|
Assert.Same(victim, pet.ControlTarget);
|
|
}
|
|
|
|
[Fact]
|
|
public void FriendRefusal_MidAttack_KeepsTheAttack()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend); // already a friend -> refusal
|
|
var victim = new PetTestStub();
|
|
victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map);
|
|
_created.Add(victim);
|
|
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
|
|
pet.IssueOrder(OrderType.Friend, master, friend);
|
|
|
|
Assert.Equal(OrderType.Attack, pet.ControlOrder);
|
|
Assert.Same(victim, pet.ControlTarget);
|
|
}
|
|
|
|
// Nothing to resume into: the interrupted attack's target is gone.
|
|
[Fact]
|
|
public void Drop_MidAttack_WithTheTargetGone_FallsBackToTheStandingOrder()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = new PetTestStub();
|
|
victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map);
|
|
_created.Add(victim);
|
|
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
victim.Delete();
|
|
|
|
pet.IssueOrder(OrderType.Drop, master);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
}
|
|
|
|
// Resuming an attack must not re-run the aggression that ordering it performed.
|
|
[Fact]
|
|
public void Drop_MidAttack_DoesNotRepeatTheHarm()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = new PetTestStub();
|
|
victim.MoveToWorld(new Point3D(1003, 1000, 0), pet.Map);
|
|
_created.Add(victim);
|
|
|
|
pet.ControlOrder = OrderType.Follow;
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
var aggressors = victim.Aggressors.Count;
|
|
|
|
pet.IssueOrder(OrderType.Drop, master);
|
|
|
|
Assert.Equal(aggressors, victim.Aggressors.Count);
|
|
}
|
|
|
|
[Fact]
|
|
public void Friend_Refused_RestsAtPersistentOrder_AndObeyDoesNotRepeat()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow; // persistent = Follow
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend); // already a friend -> refusal 1049691
|
|
|
|
pet.IssueOrder(OrderType.Friend, pet.ControlMaster, friend);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder); // never rests at Friend
|
|
Assert.True(BaseAI.IsRestableOrder(pet.ControlOrder));
|
|
|
|
// Obey must not repeat the refusal.
|
|
pet.AIObject.Obey();
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Unfriend_OfNonFriend_RestsAtPersistentOrder()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Stay; // persistent = Stay
|
|
var stranger = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
|
|
pet.IssueOrder(OrderType.Unfriend, pet.ControlMaster, stranger);
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Friend_Accepted_LeavesTheStandingOrderAlone()
|
|
{
|
|
var post = new Point3D(1001, 1000, 0);
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
pet.ControlOrder = OrderType.Stay; // the owner's standing order, anchored at the post
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
|
|
pet.IssueOrder(OrderType.Friend, master, friend);
|
|
|
|
Assert.True(pet.IsPetFriend(friend));
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder); // still staying
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder); // owner's order not rewritten
|
|
Assert.Equal(post, pet.Home); // and not re-anchored
|
|
}
|
|
|
|
[Fact]
|
|
public void Unfriend_Accepted_LeavesTheStandingOrderAlone()
|
|
{
|
|
var post = new Point3D(1001, 1000, 0);
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
pet.ControlOrder = OrderType.Stay;
|
|
|
|
pet.IssueOrder(OrderType.Unfriend, master, friend);
|
|
|
|
Assert.False(pet.IsPetFriend(friend));
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(post, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void Rename_RestsAtARestableOrder()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow;
|
|
|
|
pet.IssueOrder(OrderType.Rename, master);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.True(BaseAI.IsRestableOrder(pet.ControlOrder));
|
|
}
|
|
|
|
[Fact]
|
|
public void Drop_OnAPetThatCannotDrop_StillResolves()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Stay;
|
|
pet.IsDeadPet = true; // refuses to drop
|
|
|
|
pet.IssueOrder(OrderType.Drop, master);
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_ResolvesToARestableOrder_FromEveryPrevious()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
OrderType[] previousOrders = [OrderType.Come, OrderType.Attack, OrderType.Guard, OrderType.Follow, OrderType.Stay, OrderType.None];
|
|
|
|
for (var i = 0; i < previousOrders.Length; i++)
|
|
{
|
|
pet.ControlOrder = previousOrders[i];
|
|
pet.ControlOrder = OrderType.Stop;
|
|
Assert.True(BaseAI.IsRestableOrder(pet.ControlOrder));
|
|
Assert.NotEqual(OrderType.Stop, pet.ControlOrder);
|
|
}
|
|
}
|
|
|
|
[Fact]
|
|
public void IssueOrder_RevealsTheIssuer_NeverTheMaster()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
master.Hidden = true;
|
|
friend.Hidden = true;
|
|
|
|
pet.IssueOrder(OrderType.Stay, friend);
|
|
|
|
Assert.False(friend.Hidden);
|
|
Assert.True(master.Hidden);
|
|
}
|
|
|
|
[Fact]
|
|
public void SystemIssuedOrder_RevealsNobody()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
master.Hidden = true;
|
|
|
|
pet.ControlOrder = OrderType.Follow; // raw assignment = system-issued
|
|
|
|
Assert.True(master.Hidden);
|
|
}
|
|
|
|
[Fact]
|
|
public void EndPickTarget_Attack_SetsCombatantAndFocus()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = SpawnPlayer(new Point3D(1003, 1000, 0));
|
|
|
|
pet.AIObject.EndPickTarget(master, victim, OrderType.Attack);
|
|
|
|
Assert.Equal(OrderType.Attack, pet.ControlOrder);
|
|
Assert.Same(victim, pet.ControlTarget);
|
|
Assert.Same(victim, pet.Combatant);
|
|
Assert.Same(victim, pet.FocusMob);
|
|
Assert.True(pet.Warmode);
|
|
Assert.Equal(1, pet.CombatantSets); // the Issue phase is the only writer
|
|
|
|
pet.AIObject.Obey(); // the tick does not rewrite it
|
|
Assert.Equal(1, pet.CombatantSets);
|
|
}
|
|
|
|
[Fact]
|
|
public void ReIssuedAttack_OnTheSameTarget_DoesNotRewriteCombatantOrFlapWarmode()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = SpawnPlayer(new Point3D(1003, 1000, 0));
|
|
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
Assert.Equal(1, pet.CombatantSets);
|
|
Assert.True(pet.Warmode);
|
|
|
|
// Dropping Warmode would null Combatant and make the re-issue replay DoHarmful.
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
|
|
Assert.Equal(1, pet.CombatantSets);
|
|
Assert.True(pet.Warmode);
|
|
Assert.Same(victim, pet.Combatant);
|
|
Assert.Same(victim, pet.FocusMob);
|
|
}
|
|
|
|
[Fact]
|
|
public void OrderedAttack_ReassertsTheCommandedTarget_AfterAnAggressorStealsCombatant()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var victim = SpawnPlayer(new Point3D(1003, 1000, 0));
|
|
var other = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
|
|
pet.IssueOrder(OrderType.Attack, master, victim);
|
|
|
|
// what OnAggressiveAction does
|
|
pet.Combatant = other;
|
|
Assert.Same(other, pet.Combatant);
|
|
|
|
pet.AIObject.Obey(); // the tick puts the kill order back on the commanded target
|
|
|
|
Assert.Same(victim, pet.Combatant);
|
|
Assert.Equal(OrderType.Attack, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void Rename_WhileFollowing_KeepsFollowingTheMaster()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.IssueOrder(OrderType.Follow, master, master);
|
|
|
|
pet.IssueOrder(OrderType.Rename, master); // the menu passes no target
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Same(master, pet.ControlTarget); // restored, not null
|
|
|
|
pet.AIObject.Obey();
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder); // no "no one to follow" -> None
|
|
}
|
|
|
|
[Fact]
|
|
public void TransferRefused_ResumesFollowingTheMaster_NotTheRecipient()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.IssueOrder(OrderType.Follow, master, master);
|
|
var recipient = SpawnPlayer(new Point3D(1002, 1000, 0)); // no NetState -> the transfer is refused
|
|
|
|
pet.IssueOrder(OrderType.Transfer, master, recipient);
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Same(master, pet.ControlTarget);
|
|
Assert.True(pet.Controlled);
|
|
Assert.Same(master, pet.ControlMaster);
|
|
}
|
|
|
|
[Fact]
|
|
public void SameOrderTwice_ReRunsIssue()
|
|
{
|
|
var postA = new Point3D(1005, 1005, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), postA);
|
|
pet.ControlOrder = OrderType.Stay; // Home = A
|
|
pet.MoveToWorld(new Point3D(1050, 1050, 0), pet.Map);
|
|
|
|
pet.ControlOrder = OrderType.Stay; // reissued: re-anchor
|
|
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void LoyaltyRelease_AndManualRelease_ProduceTheSameEndState()
|
|
{
|
|
var (masterA, petA) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var (masterB, petB) = Spawn(new Point3D(1100, 1100, 0), new Point3D(1101, 1100, 0));
|
|
petA.Name = "Rex";
|
|
petB.Name = "Rex";
|
|
petA.IsBonded = true;
|
|
petB.IsBonded = true;
|
|
|
|
petA.IssueOrder(OrderType.Release, masterA); // player
|
|
petB.ControlOrder = OrderType.Release; // what the loyalty drain does
|
|
|
|
PetTestStub[] pets = [petA, petB];
|
|
|
|
for (var i = 0; i < pets.Length; i++)
|
|
{
|
|
var pet = pets[i];
|
|
Assert.False(pet.Controlled);
|
|
Assert.Null(pet.ControlMaster);
|
|
Assert.False(pet.IsBonded);
|
|
Assert.Null(pet.Name);
|
|
Assert.Equal(OrderType.None, pet.ControlOrder);
|
|
Assert.True(pet.PendingDeleteTimer?.Running);
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
Assert.Equal(masterA.Followers, masterB.Followers);
|
|
}
|
|
|
|
[Fact]
|
|
public void SummonedPet_Released_IsKilledNotReleased()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.Summoned = true;
|
|
pet.SummonMaster = master;
|
|
|
|
pet.ControlOrder = OrderType.Release;
|
|
|
|
Assert.True(pet.Deleted || !pet.Alive);
|
|
}
|
|
|
|
[Fact]
|
|
public void Stop_WithNoStandingOrder_IdlesAnchoredWhereItStands()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
Assert.Equal(OrderType.Come, pet.ControlOrder); // fresh tame: no standing order, Home = Zero
|
|
Assert.Equal(Point3D.Zero, pet.Home);
|
|
|
|
pet.ControlOrder = OrderType.Stop; // what a vendor does after SetControlMaster(buyer)
|
|
|
|
Assert.Equal(OrderType.None, pet.ControlOrder);
|
|
Assert.Equal(pet.Location, pet.Home); // anchored: no unbounded wander
|
|
}
|
|
|
|
[Fact]
|
|
public void Release_ClearsFriendsAndTheStandingOrder()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
pet.ControlOrder = OrderType.Guard; // persistent = Guard
|
|
|
|
pet.IssueOrder(OrderType.Release, master);
|
|
|
|
Assert.False(pet.IsPetFriend(friend));
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void PetDeath_IssuesFollowMaster_WithoutRevealingAnyone()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.IsBonded = true;
|
|
pet.ControlOrder = OrderType.Stay;
|
|
pet.ControlTarget = null;
|
|
master.Hidden = true;
|
|
|
|
pet.Kill(); // bonded pet death -> IsDeadPet, follows the master
|
|
|
|
Assert.True(pet.IsDeadPet);
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Same(master, pet.ControlTarget);
|
|
Assert.True(master.Hidden);
|
|
Assert.False(pet.Warmode);
|
|
}
|
|
|
|
[Fact]
|
|
public void ObeyOnALegacyTransientOrder_FallsBackToPersistent()
|
|
{
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow; // persistent = Follow
|
|
|
|
// a pre-refactor save resting at Rename
|
|
var field = typeof(BaseCreature).GetField("_controlOrder", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
|
|
field!.SetValue(pet, OrderType.Rename);
|
|
Assert.Equal(OrderType.Rename, pet.ControlOrder);
|
|
|
|
pet.AIObject.Obey();
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void SpeechCommand_FromAFriend_RevealsTheFriend_NotTheMaster()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
master.Hidden = true;
|
|
friend.Hidden = true;
|
|
|
|
// "all stay" keyword 0x170
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(friend, "all stay", MessageType.Regular, 0x3B2, [0x170]));
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.False(friend.Hidden);
|
|
Assert.True(master.Hidden);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContextMenuCommand_FromAFriend_RevealsTheFriend_NotTheMaster()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
master.Hidden = true;
|
|
friend.Hidden = true;
|
|
|
|
new InternalEntry(3006114, 14, OrderType.Stay, true).OnClick(friend, pet); // Command: Stay
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.False(friend.Hidden);
|
|
Assert.True(master.Hidden);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContextMenuCommand_FromAFriend_RefusesNonFriendOrders()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
pet.ControlOrder = OrderType.Follow;
|
|
|
|
new InternalEntry(3006107, 14, OrderType.Guard, true).OnClick(friend, pet); // Command: Guard
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void ContextMenuRename_LeavesThePetOnARestableOrder()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.ControlOrder = OrderType.Follow;
|
|
|
|
new InternalEntry(3006098, 14, OrderType.Rename, true).OnClick(master, pet); // Rename
|
|
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
}
|
|
|
|
// Come with no standing order (fresh tame or post-load) must settle into one.
|
|
[Fact]
|
|
public void RestingCome_WithNoStandingOrder_SettlesIntoStayBesideTheMaster()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
Assert.Equal(OrderType.Come, pet.ControlOrder);
|
|
Assert.Equal(OrderType.None, pet.AIObject.PersistentOrder);
|
|
|
|
pet.AIObject.Obey(); // within 2 tiles -> Stay
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
Assert.Equal(pet.Location, pet.Home);
|
|
}
|
|
|
|
// Load never runs the Issue phase. Home (field 12) is read before ControlOrder (18); with an AI
|
|
// present, a setter-routed load would re-anchor Home to the restored Location.
|
|
[Fact]
|
|
public void ControlOrder_RoundTrips_AndLoadDoesNotRunIssue()
|
|
{
|
|
var post = new Point3D(1001, 1000, 0);
|
|
var (_, pet) = Spawn(new Point3D(1000, 1000, 0), post);
|
|
pet.ControlOrder = OrderType.Stay; // Home = post
|
|
pet.MoveToWorld(new Point3D(1020, 1000, 0), pet.Map); // displaced: Home != Location
|
|
Assert.Equal(post, pet.Home);
|
|
|
|
var writer = new BufferWriter(true);
|
|
pet.Serialize(writer);
|
|
var buffer = new byte[writer.Position];
|
|
writer.Buffer.AsSpan(0, (int)writer.Position).CopyTo(buffer);
|
|
|
|
var copy = new PetTestStub(World.NewMobile);
|
|
_created.Add(copy);
|
|
copy.ChangeAIType(AIType.AI_Animal); // the Issue gate is ai != null
|
|
|
|
copy.Deserialize(new BufferReader(buffer));
|
|
|
|
Assert.Equal(OrderType.Stay, copy.ControlOrder);
|
|
Assert.Equal(new Point3D(1020, 1000, 0), copy.Location);
|
|
Assert.Equal(post, copy.Home); // not re-anchored
|
|
}
|
|
|
|
[Fact]
|
|
public void SpeechCommand_WithoutThePetsName_IsIgnored()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.Name = "Rex";
|
|
pet.ControlOrder = OrderType.Stay;
|
|
|
|
// bare "come" (keyword 0x155) with no name: not for this pet
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(master, "come", MessageType.Regular, 0x3B2, [0x155]));
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(master, "Rex come", MessageType.Regular, 0x3B2, [0x155]));
|
|
Assert.Equal(OrderType.Come, pet.ControlOrder);
|
|
}
|
|
|
|
[Fact]
|
|
public void AllCommand_IssuesOnce()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.Name = "Rex";
|
|
pet.ControlOrder = OrderType.Follow;
|
|
var post = pet.Location;
|
|
|
|
// The client emits both 0x170 ("all stay") and 0x16F ("*stay") for "all stay".
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(master, "all stay", MessageType.Regular, 0x3B2, [0x170, 0x16F]));
|
|
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(post, pet.Home);
|
|
// The named keyword alone must be ignored: the speech starts with "all", not the name.
|
|
pet.MoveToWorld(new Point3D(1010, 1010, 0), pet.Map);
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(master, "all stay", MessageType.Regular, 0x3B2, [0x16F]));
|
|
Assert.Equal(post, pet.Home);
|
|
}
|
|
|
|
[Fact]
|
|
public void SpeechCommand_FromAFriend_CannotComeGuardOrDrop()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.Name = "Rex";
|
|
var friend = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
pet.AddPetFriend(friend);
|
|
pet.ControlOrder = OrderType.Stay;
|
|
var post = pet.Home;
|
|
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(friend, "Rex come", MessageType.Regular, 0x3B2, [0x155]));
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(friend, "Rex guard", MessageType.Regular, 0x3B2, [0x15C]));
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(OrderType.Stay, pet.AIObject.PersistentOrder);
|
|
|
|
pet.IsBonded = true; // CanDrop
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(friend, "Rex drop", MessageType.Regular, 0x3B2, [0x156]));
|
|
Assert.Equal(OrderType.Stay, pet.ControlOrder);
|
|
Assert.Equal(post, pet.Home); // never re-issued
|
|
|
|
// The friend can still Stay/Follow/Stop.
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(friend, "Rex follow me", MessageType.Regular, 0x3B2, [0x163]));
|
|
Assert.Equal(OrderType.Follow, pet.ControlOrder);
|
|
Assert.Same(friend, pet.ControlTarget);
|
|
}
|
|
|
|
[Fact]
|
|
public void GMObey_TakesControlOfACommandablePet()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.Name = "Rex";
|
|
var gm = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
gm.AccessLevel = AccessLevel.GameMaster;
|
|
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(gm, "Rex obey", MessageType.Regular, 0x3B2, []));
|
|
|
|
Assert.Same(gm, pet.ControlMaster);
|
|
}
|
|
|
|
[Fact]
|
|
public void GMAllObey_DoesNotTakeControlledPets()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.Name = "Rex";
|
|
var gm = SpawnPlayer(new Point3D(1002, 1000, 0));
|
|
gm.AccessLevel = AccessLevel.GameMaster;
|
|
|
|
// The mass form is for wild creatures; a controlled pet must be named.
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(gm, "all obey", MessageType.Regular, 0x3B2, []));
|
|
Assert.Same(master, pet.ControlMaster);
|
|
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(gm, "Rex obey", MessageType.Regular, 0x3B2, []));
|
|
Assert.Same(gm, pet.ControlMaster);
|
|
}
|
|
|
|
// Release is relinquishing control, not exerting it: no roll, so no loyalty either way.
|
|
[Fact]
|
|
public void MenuRelease_TouchesNoLoyalty()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.Loyalty = 50;
|
|
|
|
new InternalEntry(3006118, 14, OrderType.Release, true).OnClick(master, pet); // Release
|
|
|
|
Assert.Equal(50, pet.Loyalty); // no roll: neither the +1 for passing nor the -3 for failing
|
|
}
|
|
|
|
[Fact]
|
|
public void SpeechRelease_TouchesNoLoyalty()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.Name = "Rex";
|
|
pet.Loyalty = 50;
|
|
|
|
pet.AIObject.OnSpeech(new SpeechEventArgs(master, "Rex release", MessageType.Regular, 0x3B2, [0x16D]));
|
|
|
|
Assert.Equal(50, pet.Loyalty);
|
|
}
|
|
|
|
// A creature nobody can command is not released either.
|
|
[Fact]
|
|
public void MenuRelease_OnAnUncontrollablePet_IsRefusedWithoutCost()
|
|
{
|
|
var (master, pet) = Spawn(new Point3D(1000, 1000, 0), new Point3D(1001, 1000, 0));
|
|
pet.MinTameSkill = 120.0; // control chance at or below zero
|
|
master.Skills.AnimalTaming.Base = 0;
|
|
master.Skills.AnimalLore.Base = 0;
|
|
pet.Loyalty = 50;
|
|
|
|
new InternalEntry(3006118, 14, OrderType.Release, true).OnClick(master, pet);
|
|
|
|
Assert.Equal(50, pet.Loyalty); // refused, but never punished
|
|
Assert.True(pet.Controlled);
|
|
}
|
|
}
|