fix(items): finish corpse pickup and combat switching
Port retail's first-slot ShowPendingInPlayer path for double-click loot and carry the current owned-container destination through deferred pickup. Retire the previous ground-container view as soon as a replacement is requested so its range close cannot cancel ACE's active MoveTo chain. Preserve active-combat weapon intent across ACE's authoritative wand-to-missile stance tail while leaving peace-mode switches unchanged. Release build succeeds and all 5,885 tests pass with five intentional skips. Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
parent
d51a0fc825
commit
0392c6d721
17 changed files with 631 additions and 81 deletions
|
|
@ -1,4 +1,5 @@
|
|||
using AcDream.App.UI;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.App.Tests.UI;
|
||||
|
|
@ -21,11 +22,14 @@ public sealed class ItemInteractionControllerTests
|
|||
public readonly List<(uint Item, uint Container, int Placement)> Puts = new();
|
||||
public readonly List<(uint Item, uint Container, uint Placement, uint Amount)> SplitPuts = new();
|
||||
public readonly List<uint> ExternalRequests = new();
|
||||
public readonly List<(uint Item, uint Container, int Placement)> BackpackPlacements = new();
|
||||
public readonly List<uint> Drops = new();
|
||||
public readonly List<(uint Item, uint Amount)> SplitDrops = new();
|
||||
public readonly List<(uint Target, uint Item, uint Amount)> Gives = new();
|
||||
public readonly List<string> Toasts = new();
|
||||
public readonly List<string> SystemMessages = new();
|
||||
public readonly List<CombatMode> CombatModeRequests = new();
|
||||
public readonly CombatState Combat = new();
|
||||
public readonly StackSplitQuantityState SplitQuantity = new();
|
||||
public uint SelectedObject;
|
||||
public bool NonCombatMode;
|
||||
|
|
@ -65,6 +69,8 @@ public sealed class ItemInteractionControllerTests
|
|||
stackSplitQuantity: SplitQuantity,
|
||||
inNonCombatMode: () => NonCombatMode,
|
||||
groundObjectId: () => GroundObject,
|
||||
placeInBackpack: (item, container, placement) =>
|
||||
BackpackPlacements.Add((item, container, placement)),
|
||||
sendPutItemInContainer: (item, container, placement) =>
|
||||
Puts.Add((item, container, placement)),
|
||||
sendGive: (target, item, amount) => Gives.Add((target, item, amount)),
|
||||
|
|
@ -76,7 +82,9 @@ public sealed class ItemInteractionControllerTests
|
|||
{
|
||||
GroundObject = id;
|
||||
ExternalRequests.Add(id);
|
||||
});
|
||||
},
|
||||
combatState: Combat,
|
||||
sendChangeCombatMode: CombatModeRequests.Add);
|
||||
}
|
||||
|
||||
public ItemInteractionController Controller { get; }
|
||||
|
|
@ -158,6 +166,38 @@ public sealed class ItemInteractionControllerTests
|
|||
Assert.Empty(h.Puts);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DoubleClickItemInOpenCorpse_placesThatItemInBackpack()
|
||||
{
|
||||
var h = new Harness();
|
||||
const uint corpse = 0x70000012u;
|
||||
const uint loot = 0x70000013u;
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = corpse,
|
||||
Type = ItemType.Container,
|
||||
ItemsCapacity = 24,
|
||||
PublicWeenieBitfield = (uint)(
|
||||
PublicWeenieFlags.Openable
|
||||
| PublicWeenieFlags.Stuck
|
||||
| PublicWeenieFlags.Corpse),
|
||||
});
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = loot,
|
||||
Name = "Loot",
|
||||
Type = ItemType.Misc,
|
||||
});
|
||||
h.Objects.MoveItem(loot, corpse, 0);
|
||||
h.GroundObject = corpse;
|
||||
|
||||
Assert.True(h.Controller.ActivateItem(loot));
|
||||
|
||||
Assert.Equal(new[] { (loot, Player, 0) }, h.BackpackPlacements);
|
||||
Assert.Empty(h.Uses);
|
||||
Assert.Empty(h.Wields);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DropOwnedItemOnOpenExternalContainer_SendsPutWithoutOptimisticMove()
|
||||
{
|
||||
|
|
@ -548,6 +588,107 @@ public sealed class ItemInteractionControllerTests
|
|||
Assert.Equal(Player, h.Objects.Get(bow)!.ContainerId);
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(CombatMode.NonCombat, false)]
|
||||
[InlineData(CombatMode.Magic, true)]
|
||||
public void WandToBow_reassertsMissileModeOnlyForActiveCombatTransaction(
|
||||
CombatMode startingMode,
|
||||
bool expectsMissileMode)
|
||||
{
|
||||
var h = new Harness();
|
||||
h.Combat.SetCombatMode(startingMode);
|
||||
const uint wand = 0x50000B91u;
|
||||
const uint bow = 0x50000B92u;
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = wand,
|
||||
Name = "Training Wand",
|
||||
Type = ItemType.Caster,
|
||||
CombatUse = 2,
|
||||
ValidLocations = EquipMask.Held,
|
||||
});
|
||||
h.Objects.MoveItem(wand, Player, -1, EquipMask.Held);
|
||||
h.AddContained(bow, item =>
|
||||
{
|
||||
item.Name = "Shortbow";
|
||||
item.Type = ItemType.MissileWeapon;
|
||||
item.CombatUse = 2;
|
||||
item.ValidLocations = EquipMask.MissileWeapon;
|
||||
});
|
||||
|
||||
Assert.True(h.Controller.ActivateItem(bow));
|
||||
Assert.Equal(new[] { (wand, Player, 0) }, h.Puts);
|
||||
|
||||
// ACE reports several transitional combat modes while putting the
|
||||
// wand away. The transaction retains the initial active-combat intent
|
||||
// rather than consulting this intermediate state on its second pass.
|
||||
h.Combat.SetCombatMode(CombatMode.Melee);
|
||||
h.Objects.MoveItem(wand, Player, 0, EquipMask.None);
|
||||
Assert.Equal(new[] { (bow, (uint)EquipMask.MissileWeapon) }, h.Wields);
|
||||
Assert.Empty(h.CombatModeRequests);
|
||||
|
||||
Assert.True(h.Objects.ApplyConfirmedServerWield(
|
||||
bow,
|
||||
Player,
|
||||
EquipMask.MissileWeapon));
|
||||
Assert.Empty(h.CombatModeRequests);
|
||||
|
||||
// The first authoritative post-wield mode is the transaction's server
|
||||
// settlement stream. Local ACE publishes Peace -> Missile -> Peace;
|
||||
// only the trailing Peace is late enough to accept the reassertion.
|
||||
if (expectsMissileMode)
|
||||
{
|
||||
h.Combat.SetCombatMode(CombatMode.NonCombat);
|
||||
Assert.Empty(h.CombatModeRequests);
|
||||
h.Combat.SetCombatMode(CombatMode.Missile);
|
||||
Assert.Empty(h.CombatModeRequests);
|
||||
h.Combat.SetCombatMode(CombatMode.NonCombat);
|
||||
}
|
||||
else
|
||||
{
|
||||
h.Combat.SetCombatMode(CombatMode.Missile);
|
||||
}
|
||||
|
||||
if (expectsMissileMode)
|
||||
Assert.Equal(new[] { CombatMode.Missile }, h.CombatModeRequests);
|
||||
else
|
||||
Assert.Empty(h.CombatModeRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ActiveWeaponReplacement_serverSettlesDirectly_doesNotReassertLaterPeace()
|
||||
{
|
||||
var h = new Harness();
|
||||
h.Combat.SetCombatMode(CombatMode.Magic);
|
||||
const uint wand = 0x50000BA1u;
|
||||
const uint bow = 0x50000BA2u;
|
||||
h.Objects.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = wand,
|
||||
Type = ItemType.Caster,
|
||||
CombatUse = 2,
|
||||
ValidLocations = EquipMask.Held,
|
||||
});
|
||||
h.Objects.MoveItem(wand, Player, -1, EquipMask.Held);
|
||||
h.AddContained(bow, item =>
|
||||
{
|
||||
item.Type = ItemType.MissileWeapon;
|
||||
item.CombatUse = 2;
|
||||
item.ValidLocations = EquipMask.MissileWeapon;
|
||||
});
|
||||
|
||||
Assert.True(h.Controller.ActivateItem(bow));
|
||||
h.Combat.SetCombatMode(CombatMode.Melee);
|
||||
h.Objects.MoveItem(wand, Player, 0, EquipMask.None);
|
||||
Assert.True(h.Objects.ApplyConfirmedServerWield(
|
||||
bow, Player, EquipMask.MissileWeapon));
|
||||
|
||||
h.Combat.SetCombatMode(CombatMode.Missile);
|
||||
h.Combat.SetCombatMode(CombatMode.NonCombat);
|
||||
|
||||
Assert.Empty(h.CombatModeRequests);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PreviouslyWieldedBow_AfterAuthoritativeUnwield_DoubleClickWieldsAgain()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue