feat(inventory): port retail weapon switching
Sequence primary weapon replacement through the server-confirmed AutoWield transaction: return the occupied weapon to the player pack, wait for its move event, then wield the requested item. Route authoritative CombatMode property updates so peace stays peace and war adopts the new weapon stance without client-synthesized animation. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
8e9c538519
commit
17b5712d53
14 changed files with 736 additions and 155 deletions
|
|
@ -18,41 +18,6 @@ public enum ItemPrimaryClickResult
|
|||
/// </summary>
|
||||
public sealed class ItemInteractionController : IDisposable
|
||||
{
|
||||
private static readonly EquipMask[] AutoEquipOrder =
|
||||
{
|
||||
EquipMask.HeadWear,
|
||||
EquipMask.ChestWear,
|
||||
EquipMask.AbdomenWear,
|
||||
EquipMask.UpperArmWear,
|
||||
EquipMask.LowerArmWear,
|
||||
EquipMask.HandWear,
|
||||
EquipMask.UpperLegWear,
|
||||
EquipMask.LowerLegWear,
|
||||
EquipMask.FootWear,
|
||||
EquipMask.ChestArmor,
|
||||
EquipMask.AbdomenArmor,
|
||||
EquipMask.UpperArmArmor,
|
||||
EquipMask.LowerArmArmor,
|
||||
EquipMask.UpperLegArmor,
|
||||
EquipMask.LowerLegArmor,
|
||||
EquipMask.NeckWear,
|
||||
EquipMask.WristWearLeft,
|
||||
EquipMask.WristWearRight,
|
||||
EquipMask.FingerWearLeft,
|
||||
EquipMask.FingerWearRight,
|
||||
EquipMask.Shield,
|
||||
EquipMask.MissileAmmo,
|
||||
EquipMask.MeleeWeapon,
|
||||
EquipMask.MissileWeapon,
|
||||
EquipMask.Held,
|
||||
EquipMask.TwoHanded,
|
||||
EquipMask.TrinketOne,
|
||||
EquipMask.Cloak,
|
||||
EquipMask.SigilOne,
|
||||
EquipMask.SigilTwo,
|
||||
EquipMask.SigilThree,
|
||||
};
|
||||
|
||||
private const long RetailUseThrottleMs = 200;
|
||||
private const long RetailDoubleClickMs = 500;
|
||||
|
||||
|
|
@ -77,6 +42,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
private readonly InteractionState _interactionState;
|
||||
private readonly Func<uint> _selectedObjectId;
|
||||
private readonly StackSplitQuantityState? _stackSplitQuantity;
|
||||
private readonly AutoWieldController _autoWield;
|
||||
|
||||
private long _lastUseMs = long.MinValue / 2;
|
||||
private uint _consumedPrimaryClickTarget;
|
||||
|
|
@ -105,7 +71,8 @@ public sealed class ItemInteractionController : IDisposable
|
|||
Action<ItemPolicyAction>? auxiliaryAction = null,
|
||||
Action<uint, uint>? sendSplitToWorld = null,
|
||||
Func<uint>? selectedObjectId = null,
|
||||
StackSplitQuantityState? stackSplitQuantity = null)
|
||||
StackSplitQuantityState? stackSplitQuantity = null,
|
||||
Action<uint, uint, int>? sendPutItemInContainer = null)
|
||||
{
|
||||
_objects = objects ?? throw new ArgumentNullException(nameof(objects));
|
||||
_playerGuid = playerGuid ?? throw new ArgumentNullException(nameof(playerGuid));
|
||||
|
|
@ -129,6 +96,12 @@ public sealed class ItemInteractionController : IDisposable
|
|||
_stackSplitQuantity = stackSplitQuantity;
|
||||
_interactionState = interactionState ?? new InteractionState();
|
||||
_interactionState.Changed += OnInteractionModeChanged;
|
||||
_autoWield = new AutoWieldController(
|
||||
_objects,
|
||||
_playerGuid,
|
||||
_sendWield,
|
||||
sendPutItemInContainer,
|
||||
_toast);
|
||||
}
|
||||
|
||||
public event Action? StateChanged;
|
||||
|
|
@ -364,7 +337,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
case ItemPolicyActionKind.WieldLeft:
|
||||
case ItemPolicyActionKind.AutoSort:
|
||||
if (_objects.Get(action.ObjectId) is { } item)
|
||||
acted |= TryAutoWield(item);
|
||||
acted |= _autoWield.TryWield(item);
|
||||
break;
|
||||
case ItemPolicyActionKind.OpenContainedContainer:
|
||||
case ItemPolicyActionKind.SendUse:
|
||||
|
|
@ -472,6 +445,7 @@ public sealed class ItemInteractionController : IDisposable
|
|||
if (_disposed) return;
|
||||
_disposed = true;
|
||||
_interactionState.Changed -= OnInteractionModeChanged;
|
||||
_autoWield.Dispose();
|
||||
}
|
||||
|
||||
/// <summary>Retail UseDone (0x01C7) releases one UI busy reference.</summary>
|
||||
|
|
@ -491,119 +465,6 @@ public sealed class ItemInteractionController : IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
private bool TryAutoWield(ClientObject item)
|
||||
{
|
||||
if (item.ValidLocations == EquipMask.None
|
||||
|| item.CurrentlyEquippedLocation != EquipMask.None)
|
||||
return false;
|
||||
|
||||
EquipMask mask = BestAvailableEquipMask(item);
|
||||
if (mask == EquipMask.None)
|
||||
{
|
||||
_toast?.Invoke("That slot is already in use");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_objects.WieldItemOptimistic(item.ObjectId, _playerGuid(), mask))
|
||||
return false;
|
||||
|
||||
_sendWield?.Invoke(item.ObjectId, (uint)mask);
|
||||
return true;
|
||||
}
|
||||
|
||||
private EquipMask BestAvailableEquipMask(ClientObject item)
|
||||
{
|
||||
if (ItemEquipRules.IsAutoWearItem(item))
|
||||
return AutoWearIsLegal(item) ? item.ValidLocations : EquipMask.None;
|
||||
|
||||
return FirstAvailableEquipMask(item);
|
||||
}
|
||||
|
||||
private bool AutoWearIsLegal(ClientObject item)
|
||||
{
|
||||
uint priorityMask = EquippedAutoWearPriorityMask(item.ObjectId);
|
||||
if ((item.Priority & priorityMask) == 0)
|
||||
return true;
|
||||
|
||||
EquipMask occupiedLocations = EquippedAutoWearLocationMask(item.ObjectId) & item.ValidLocations;
|
||||
return GetEquippedObjectAtLocation(occupiedLocations, item.Priority, item.ObjectId) is null;
|
||||
}
|
||||
|
||||
private EquipMask FirstAvailableEquipMask(ClientObject item)
|
||||
{
|
||||
foreach (var mask in AutoEquipOrder)
|
||||
{
|
||||
if ((item.ValidLocations & mask) == EquipMask.None)
|
||||
continue;
|
||||
if (!EquipMaskOccupied(mask, item.ObjectId))
|
||||
return mask;
|
||||
}
|
||||
return EquipMask.None;
|
||||
}
|
||||
|
||||
private bool EquipMaskOccupied(EquipMask mask, uint exceptGuid)
|
||||
{
|
||||
foreach (var o in _objects.Objects)
|
||||
{
|
||||
if (o.ObjectId == exceptGuid)
|
||||
continue;
|
||||
if ((o.CurrentlyEquippedLocation & mask) == EquipMask.None)
|
||||
continue;
|
||||
if (IsEquippedByPlayer(o))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private uint EquippedAutoWearPriorityMask(uint exceptGuid)
|
||||
{
|
||||
uint mask = 0;
|
||||
foreach (var o in _objects.Objects)
|
||||
{
|
||||
if (o.ObjectId == exceptGuid)
|
||||
continue;
|
||||
if ((o.CurrentlyEquippedLocation & ItemEquipRules.AutoWearMask) == EquipMask.None)
|
||||
continue;
|
||||
if (IsEquippedByPlayer(o))
|
||||
mask |= o.Priority;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
private EquipMask EquippedAutoWearLocationMask(uint exceptGuid)
|
||||
{
|
||||
EquipMask mask = EquipMask.None;
|
||||
foreach (var o in _objects.Objects)
|
||||
{
|
||||
if (o.ObjectId == exceptGuid)
|
||||
continue;
|
||||
if ((o.CurrentlyEquippedLocation & ItemEquipRules.AutoWearMask) == EquipMask.None)
|
||||
continue;
|
||||
if (IsEquippedByPlayer(o))
|
||||
mask |= o.CurrentlyEquippedLocation;
|
||||
}
|
||||
return mask;
|
||||
}
|
||||
|
||||
private ClientObject? GetEquippedObjectAtLocation(EquipMask locationMask, uint priority, uint exceptGuid)
|
||||
{
|
||||
if (locationMask == EquipMask.None)
|
||||
return null;
|
||||
|
||||
foreach (var o in _objects.Objects)
|
||||
{
|
||||
if (o.ObjectId == exceptGuid)
|
||||
continue;
|
||||
if (!IsEquippedByPlayer(o))
|
||||
continue;
|
||||
if ((o.CurrentlyEquippedLocation & locationMask) == EquipMask.None)
|
||||
continue;
|
||||
if ((o.Priority & priority) != 0 || priority == 0)
|
||||
return o;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool IsContainer(ClientObject item)
|
||||
=> item.ContainerTypeHint != 0
|
||||
|| item.Type.HasFlag(ItemType.Container)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue