feat(ui): port retail item interaction policy
This commit is contained in:
parent
0cf780478a
commit
a8da4fd05a
20 changed files with 1110 additions and 104 deletions
289
tests/AcDream.Core.Tests/Items/ItemInteractionPolicyTests.cs
Normal file
289
tests/AcDream.Core.Tests/Items/ItemInteractionPolicyTests.cs
Normal file
|
|
@ -0,0 +1,289 @@
|
|||
using AcDream.Core.Items;
|
||||
|
||||
namespace AcDream.Core.Tests.Items;
|
||||
|
||||
public sealed class ItemInteractionPolicyTests
|
||||
{
|
||||
private const uint Player = 0x50000001u;
|
||||
private const uint GroundContainer = 0x70000001u;
|
||||
|
||||
[Fact]
|
||||
public void DetermineUseResult_looseItemAndComponentPack_matchRetailPickupBranch()
|
||||
{
|
||||
Assert.Equal(ItemPrimaryUseResult.PlaceInBackpack,
|
||||
Determine(Obj(0x6001)));
|
||||
|
||||
var requiresSlot = Obj(0x6002) with
|
||||
{
|
||||
Flags = PublicWeenieFlags.RequiresPackSlot,
|
||||
ItemsCapacity = 12,
|
||||
};
|
||||
Assert.Equal(ItemPrimaryUseResult.None, Determine(requiresSlot));
|
||||
Assert.Equal(ItemPrimaryUseResult.PlaceInBackpack,
|
||||
Determine(requiresSlot with { IsComponentPack = true }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetermineUseResult_onlyAllowsContainedPickupFromViewedGroundContainer()
|
||||
{
|
||||
var item = Obj(0x6001) with { ContainerId = GroundContainer };
|
||||
|
||||
Assert.Equal(ItemPrimaryUseResult.None,
|
||||
ItemInteractionPolicy.DetermineUseResult(item, Player, 0));
|
||||
Assert.Equal(ItemPrimaryUseResult.PlaceInBackpack,
|
||||
ItemInteractionPolicy.DetermineUseResult(item, Player, GroundContainer));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetermineUseResult_portsAllOwnedPrimaryClassifications()
|
||||
{
|
||||
var owned = Obj(0x6001) with
|
||||
{
|
||||
ContainerId = Player,
|
||||
OwnedByPlayer = true,
|
||||
Flags = PublicWeenieFlags.RequiresPackSlot,
|
||||
};
|
||||
|
||||
Assert.Equal(ItemPrimaryUseResult.WieldRight,
|
||||
Determine(owned with { CombatUse = 1 }));
|
||||
Assert.Equal(ItemPrimaryUseResult.WieldLeft,
|
||||
Determine(owned with { CombatUse = 1, Flags = owned.Flags | PublicWeenieFlags.WieldLeft }));
|
||||
Assert.Equal(ItemPrimaryUseResult.AutoSort,
|
||||
Determine(owned with { ValidLocations = EquipMask.HeadWear }));
|
||||
Assert.Equal(ItemPrimaryUseResult.OpenSalvage,
|
||||
Determine(owned with { Type = ItemType.TinkeringTool }));
|
||||
Assert.Equal(ItemPrimaryUseResult.ItemUse,
|
||||
Determine(owned with { Useability = ItemUseability.Contained }));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetermineUseResult_nonOwnedGameAndPlayerPaths_areDistinct()
|
||||
{
|
||||
Assert.Equal(ItemPrimaryUseResult.BeginGame,
|
||||
Determine(Obj(0x6001) with
|
||||
{
|
||||
Type = ItemType.Gameboard,
|
||||
Flags = PublicWeenieFlags.RequiresPackSlot,
|
||||
}));
|
||||
Assert.Equal(ItemPrimaryUseResult.OpenSecureTrade,
|
||||
Determine(Obj(0x6002) with
|
||||
{
|
||||
Flags = PublicWeenieFlags.Player | PublicWeenieFlags.RequiresPackSlot,
|
||||
}));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseObject_excludesResultEightFromEarlyClassifiedBound_thenStillWieldsLeft()
|
||||
{
|
||||
var source = Obj(0x6001) with
|
||||
{
|
||||
ContainerId = Player,
|
||||
OwnedByPlayer = true,
|
||||
CombatUse = 1,
|
||||
Flags = PublicWeenieFlags.RequiresPackSlot | PublicWeenieFlags.WieldLeft,
|
||||
};
|
||||
|
||||
var decision = ItemInteractionPolicy.DecideUse(Use(source));
|
||||
|
||||
Assert.Equal(new[] { ItemPolicyActionKind.WieldLeft },
|
||||
decision.Actions.Select(a => a.Kind));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseObject_readyVendorTradeAndWieldedSourceGates_areOrdered()
|
||||
{
|
||||
var direct = OwnedDirect();
|
||||
|
||||
Assert.Empty(ItemInteractionPolicy.DecideUse(
|
||||
Use(direct) with { ReadyForInventoryRequest = false }).Actions);
|
||||
Assert.Empty(ItemInteractionPolicy.DecideUse(
|
||||
Use(direct) with { ActiveVendorId = 0x7001, Source = direct with { ContainerId = 0x7001 } }).Actions);
|
||||
Assert.Equal(ItemPolicyActionKind.Reject,
|
||||
Assert.Single(ItemInteractionPolicy.DecideUse(
|
||||
Use(direct with { TradeState = 1 })).Actions).Kind);
|
||||
Assert.Contains("wield", Assert.Single(ItemInteractionPolicy.DecideUse(
|
||||
Use(direct with { Useability = ItemUseability.Wielded })).Actions).Message,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseObject_targetedUse_appliesKindAndLocationGate_thenSendsAndMarksBusy()
|
||||
{
|
||||
var source = OwnedDirect() with
|
||||
{
|
||||
Useability = 0x00080008u,
|
||||
TargetType = (uint)ItemType.Misc,
|
||||
};
|
||||
var target = Obj(0x6002) with
|
||||
{
|
||||
ContainerId = Player,
|
||||
OwnedByPlayer = true,
|
||||
Type = ItemType.Misc,
|
||||
};
|
||||
|
||||
var decision = ItemInteractionPolicy.DecideUse(Use(source) with
|
||||
{
|
||||
UseCurrentSelection = true,
|
||||
SelectedTarget = target,
|
||||
});
|
||||
|
||||
Assert.Equal(new[]
|
||||
{
|
||||
ItemPolicyActionKind.SendUseWithTarget,
|
||||
ItemPolicyActionKind.IncrementBusy,
|
||||
}, decision.Actions.Select(a => a.Kind));
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(PublicWeenieFlags.PlayerKillerSwitch, ItemPolicyActionKind.ConfirmPlayerKillerSwitch)]
|
||||
[InlineData(PublicWeenieFlags.NonPlayerKillerSwitch, ItemPolicyActionKind.ConfirmNonPlayerKillerSwitch)]
|
||||
[InlineData(PublicWeenieFlags.VolatileRare, ItemPolicyActionKind.ConfirmVolatileRare)]
|
||||
public void UseObject_confirmationFlags_blockWireUse(
|
||||
PublicWeenieFlags flag,
|
||||
ItemPolicyActionKind expected)
|
||||
{
|
||||
var decision = ItemInteractionPolicy.DecideUse(
|
||||
Use(OwnedDirect() with { Flags = flag | PublicWeenieFlags.RequiresPackSlot }));
|
||||
|
||||
Assert.Equal(expected, Assert.Single(decision.Actions).Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Placement_vendorAndSecureTrade_actButPreserveRetailFalseReturn()
|
||||
{
|
||||
var item = OwnedDirect() with { StackSize = 10, MaxSplitSize = 10 };
|
||||
var vendor = Obj(0x7001) with { Flags = PublicWeenieFlags.Vendor };
|
||||
var sale = ItemInteractionPolicy.DecidePlacement(Place(item, vendor));
|
||||
Assert.False(sale.ReturnValue);
|
||||
Assert.Equal(ItemPolicyActionKind.SellToVendor, Assert.Single(sale.Actions).Kind);
|
||||
|
||||
var player = Obj(0x7002) with { Flags = PublicWeenieFlags.Player };
|
||||
var trade = ItemInteractionPolicy.DecidePlacement(Place(item, player));
|
||||
Assert.False(trade.ReturnValue);
|
||||
Assert.Equal(ItemPolicyActionKind.StartSecureTrade, Assert.Single(trade.Actions).Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Placement_groundStackAndAirborneMatrix_matchesRetail()
|
||||
{
|
||||
var item = OwnedDirect() with { StackSize = 10, MaxSplitSize = 10 };
|
||||
|
||||
var airborne = ItemInteractionPolicy.DecidePlacement(PlaceOnGround(item) with
|
||||
{
|
||||
PlayerOnGround = false,
|
||||
});
|
||||
Assert.False(airborne.ReturnValue);
|
||||
Assert.Equal(ItemPolicyActionKind.Reject, Assert.Single(airborne.Actions).Kind);
|
||||
|
||||
var split = ItemInteractionPolicy.DecidePlacement(PlaceOnGround(item) with { SplitSize = 4 });
|
||||
Assert.True(split.ReturnValue);
|
||||
Assert.Equal(ItemPolicyActionKind.SplitToWorld, Assert.Single(split.Actions).Kind);
|
||||
|
||||
var full = ItemInteractionPolicy.DecidePlacement(PlaceOnGround(item));
|
||||
Assert.True(full.ReturnValue);
|
||||
Assert.Equal(ItemPolicyActionKind.DropToWorld, Assert.Single(full.Actions).Kind);
|
||||
|
||||
var alreadyWorld = ItemInteractionPolicy.DecidePlacement(
|
||||
PlaceOnGround(item with { IsIn3DView = true }));
|
||||
Assert.False(alreadyWorld.ReturnValue);
|
||||
Assert.Contains("cancelled", Assert.Single(alreadyWorld.Actions).Message,
|
||||
StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Placement_containerRequiresOpenableAndCurrentGroundObject()
|
||||
{
|
||||
var item = OwnedDirect();
|
||||
var container = Obj(GroundContainer) with { IsContainer = true };
|
||||
|
||||
var locked = ItemInteractionPolicy.DecidePlacement(Place(item, container) with
|
||||
{
|
||||
GroundObjectId = GroundContainer,
|
||||
});
|
||||
Assert.False(locked.ReturnValue);
|
||||
|
||||
var open = container with { Flags = PublicWeenieFlags.Openable };
|
||||
var closedView = ItemInteractionPolicy.DecidePlacement(Place(item, open));
|
||||
Assert.False(closedView.ReturnValue);
|
||||
|
||||
var placed = ItemInteractionPolicy.DecidePlacement(Place(item, open) with
|
||||
{
|
||||
GroundObjectId = GroundContainer,
|
||||
});
|
||||
Assert.True(placed.ReturnValue);
|
||||
Assert.Equal(ItemPolicyActionKind.PlaceInContainer, Assert.Single(placed.Actions).Kind);
|
||||
}
|
||||
|
||||
private static ItemPrimaryUseResult Determine(ItemPolicyObject item)
|
||||
=> ItemInteractionPolicy.DetermineUseResult(item, Player, 0);
|
||||
|
||||
private static ItemPolicyObject OwnedDirect()
|
||||
=> Obj(0x6001) with
|
||||
{
|
||||
ContainerId = Player,
|
||||
OwnedByPlayer = true,
|
||||
Flags = PublicWeenieFlags.RequiresPackSlot,
|
||||
Useability = ItemUseability.Contained,
|
||||
};
|
||||
|
||||
private static ItemPolicyObject Obj(uint id) => new(
|
||||
id,
|
||||
ItemType.Misc,
|
||||
PublicWeenieFlags.None,
|
||||
ContainerId: 0,
|
||||
WielderId: 0,
|
||||
ValidLocations: EquipMask.None,
|
||||
CurrentLocation: EquipMask.None,
|
||||
CombatUse: 0,
|
||||
ItemsCapacity: 0,
|
||||
ContainersCapacity: 0,
|
||||
Useability: 0,
|
||||
TargetType: 0,
|
||||
OwnedByPlayer: false,
|
||||
IsContainer: false,
|
||||
IsComponentPack: false,
|
||||
TradeState: 0,
|
||||
StackSize: 1,
|
||||
MaxSplitSize: 1,
|
||||
IsIn3DView: false);
|
||||
|
||||
private static ItemUsePolicyInput Use(ItemPolicyObject source) => new(
|
||||
source,
|
||||
Player,
|
||||
GroundObjectId: 0,
|
||||
ReadyForInventoryRequest: true,
|
||||
ActiveVendorId: 0,
|
||||
BypassClassification: false,
|
||||
UseCurrentSelection: false,
|
||||
SelectedTarget: null,
|
||||
ConfirmVolatileRareUses: true,
|
||||
InNonCombatMode: false);
|
||||
|
||||
private static ItemPlacementPolicyInput Place(
|
||||
ItemPolicyObject item,
|
||||
ItemPolicyObject target) => new(
|
||||
item,
|
||||
Player,
|
||||
GroundObjectId: 0,
|
||||
ReadyForInventoryRequest: true,
|
||||
TargetId: target.Id,
|
||||
Target: target,
|
||||
AllowGroundFallback: false,
|
||||
MergeAccepted: false,
|
||||
DragOnPlayerOpensSecureTrade: true,
|
||||
PlayerOnGround: true,
|
||||
SplitSize: item.StackSize);
|
||||
|
||||
private static ItemPlacementPolicyInput PlaceOnGround(ItemPolicyObject item) => new(
|
||||
item,
|
||||
Player,
|
||||
GroundObjectId: 0,
|
||||
ReadyForInventoryRequest: true,
|
||||
TargetId: 0,
|
||||
Target: null,
|
||||
AllowGroundFallback: true,
|
||||
MergeAccepted: false,
|
||||
DragOnPlayerOpensSecureTrade: true,
|
||||
PlayerOnGround: true,
|
||||
SplitSize: item.StackSize);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue