feat(ui): drive toolbar use state from selection
Port the retail selected-object availability predicate into Core and project it through the shared interaction owner. The imported hand now follows canonical selection/object notices, keeps weapon and targeted-tool activation on the existing wield/use cursor paths, and ghosts empty or explicitly unusable selections per the connected UX requirement. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
0134122c28
commit
60387668d0
11 changed files with 269 additions and 16 deletions
|
|
@ -496,6 +496,23 @@ public sealed class ItemInteractionController : IDisposable
|
|||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Projects retail <c>gmToolbarUI::HandleSelectionChanged @ 0x004BF380</c>
|
||||
/// from the live selected object's public description. This is deliberately
|
||||
/// a read-only query; activation still flows through <see cref="ActivateItem"/>.
|
||||
/// </summary>
|
||||
public bool IsToolbarUseEnabled(uint selectedObjectId)
|
||||
{
|
||||
if (selectedObjectId == 0
|
||||
|| _objects.Get(selectedObjectId) is not { } item)
|
||||
return false;
|
||||
|
||||
return ItemInteractionPolicy.IsToolbarUseEnabled(
|
||||
item.Type,
|
||||
item.CombatUse ?? 0,
|
||||
item.Useability ?? ItemUseability.Undef);
|
||||
}
|
||||
|
||||
public bool ActivateItem(uint itemGuid)
|
||||
{
|
||||
if (itemGuid == 0) return false;
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ using System.Collections.Generic;
|
|||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Selection;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
|
|
@ -73,6 +74,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
private readonly ItemInteractionController? _itemInteraction;
|
||||
private readonly Action<uint>? _selectItem;
|
||||
private readonly Func<uint> _selectedObjectId;
|
||||
private readonly SelectionState? _selection;
|
||||
private readonly Func<uint>? _playerGuid;
|
||||
private readonly Action<uint, uint, int>? _sendPutItemInContainer;
|
||||
private uint _ammoObjectId;
|
||||
|
|
@ -106,6 +108,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
Action? toggleCombat = null,
|
||||
Action<uint>? selectItem = null,
|
||||
Func<uint>? selectedObjectId = null,
|
||||
SelectionState? selection = null,
|
||||
Func<uint>? playerGuid = null,
|
||||
Action<uint, uint, int>? sendPutItemInContainer = null,
|
||||
UiDatFont? ammoFont = null,
|
||||
|
|
@ -125,6 +128,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
_sendRemoveShortcut = sendRemoveShortcut;
|
||||
_selectItem = selectItem;
|
||||
_selectedObjectId = selectedObjectId ?? (() => 0u);
|
||||
_selection = selection;
|
||||
_playerGuid = playerGuid;
|
||||
_sendPutItemInContainer = sendPutItemInContainer;
|
||||
|
||||
|
|
@ -196,6 +200,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
// Wire live combat-mode changes if a CombatState was provided.
|
||||
if (_combatState is not null)
|
||||
_combatState.CombatModeChanged += SetCombatMode;
|
||||
if (_selection is not null)
|
||||
_selection.Changed += OnSelectionChanged;
|
||||
|
||||
// D.5.4: the table now holds ALL objects (creatures, NPCs, etc.), so filter
|
||||
// to our 18 shortcut guids — else every creature spawn in a busy zone
|
||||
|
|
@ -206,6 +212,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
repo.ObjectMoved += OnRepositoryObjectMoved;
|
||||
repo.Cleared += OnRepositoryCleared;
|
||||
RefreshAmmo();
|
||||
RefreshUseButton();
|
||||
}
|
||||
|
||||
private void OnRepositoryObjectChanged(ClientObject obj)
|
||||
|
|
@ -214,6 +221,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
RefreshAmmo();
|
||||
if (IsShortcutGuid(obj.ObjectId))
|
||||
Populate();
|
||||
if (obj.ObjectId == _selectedObjectId())
|
||||
RefreshUseButton();
|
||||
}
|
||||
|
||||
private void OnRepositoryObjectMoved(ClientObjectMove move)
|
||||
|
|
@ -234,6 +243,23 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
{
|
||||
RefreshAmmo();
|
||||
Populate();
|
||||
RefreshUseButton();
|
||||
}
|
||||
|
||||
private void OnSelectionChanged(SelectionTransition _)
|
||||
=> RefreshUseButton();
|
||||
|
||||
private void RefreshUseButton()
|
||||
{
|
||||
if (_useButton is null)
|
||||
return;
|
||||
|
||||
// Retail normally leaves state 1 active when selectedID == 0 so the
|
||||
// click can enter generic TARGET_MODE_USE. acdream intentionally
|
||||
// ghosts that empty state per the connected UX requirement; every
|
||||
// selected-object classification below is the exact retail predicate.
|
||||
_useButton.Enabled =
|
||||
_itemInteraction?.IsToolbarUseEnabled(_selectedObjectId()) == true;
|
||||
}
|
||||
|
||||
private bool IsAmmoRelated(ClientObject obj)
|
||||
|
|
@ -330,6 +356,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
Action? toggleCombat = null,
|
||||
Action<uint>? selectItem = null,
|
||||
Func<uint>? selectedObjectId = null,
|
||||
SelectionState? selection = null,
|
||||
Func<uint>? playerGuid = null,
|
||||
Action<uint, uint, int>? sendPutItemInContainer = null,
|
||||
UiDatFont? ammoFont = null,
|
||||
|
|
@ -338,7 +365,7 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState,
|
||||
regularDigits, ghostedDigits, emptyDigits, itemInteraction,
|
||||
sendAddShortcut, sendRemoveShortcut, toggleCombat, selectItem,
|
||||
selectedObjectId, playerGuid, sendPutItemInContainer, ammoFont,
|
||||
selectedObjectId, selection, playerGuid, sendPutItemInContainer, ammoFont,
|
||||
dragIconIds);
|
||||
c.Populate();
|
||||
return c;
|
||||
|
|
@ -736,6 +763,8 @@ public sealed class ToolbarController : IItemListDragHandler, IRetainedPanelCont
|
|||
|
||||
if (_combatState is not null)
|
||||
_combatState.CombatModeChanged -= SetCombatMode;
|
||||
if (_selection is not null)
|
||||
_selection.Changed -= OnSelectionChanged;
|
||||
_repo.ObjectAdded -= OnRepositoryObjectChanged;
|
||||
_repo.ObjectUpdated -= OnRepositoryObjectChanged;
|
||||
_repo.ObjectRemoved -= OnRepositoryObjectChanged;
|
||||
|
|
|
|||
|
|
@ -689,6 +689,7 @@ public sealed class RetailUiRuntime : IDisposable
|
|||
toggleCombat: b.ToggleCombat,
|
||||
selectItem: guid => b.Selection.Select(guid, SelectionChangeSource.Toolbar),
|
||||
selectedObjectId: () => b.Selection.SelectedObjectId ?? 0u,
|
||||
selection: b.Selection,
|
||||
playerGuid: b.PlayerGuid,
|
||||
sendPutItemInContainer: b.SendPutItemInContainer,
|
||||
ammoFont: _bindings.Assets.DefaultFont,
|
||||
|
|
|
|||
|
|
@ -151,6 +151,22 @@ public static class ItemInteractionPolicy
|
|||
private static readonly EquipMask BodyLocations = (EquipMask)0x00007E00u;
|
||||
private static readonly EquipMask ClothingLocations = (EquipMask)0x080001FFu;
|
||||
private static readonly EquipMask HeldLocations = (EquipMask)0x7C0F8000u;
|
||||
private const ItemType ToolbarEquipmentTypes =
|
||||
ItemType.Armor | ItemType.Clothing | ItemType.Jewelry;
|
||||
|
||||
/// <summary>
|
||||
/// Exact selected-object predicate used by retail's toolbar hand.
|
||||
/// <c>gmToolbarUI::HandleSelectionChanged @ 0x004BF380</c> leaves the
|
||||
/// button active for combat-use objects, armor/clothing/jewelry, or an
|
||||
/// <c>ItemUses</c> value whose <c>USEABLE_NO</c> bit is clear.
|
||||
/// </summary>
|
||||
public static bool IsToolbarUseEnabled(
|
||||
ItemType type,
|
||||
byte combatUse,
|
||||
uint useability)
|
||||
=> combatUse != 0
|
||||
|| (type & ToolbarEquipmentTypes) != 0
|
||||
|| (useability & ItemUseability.No) == 0;
|
||||
|
||||
public static ItemPrimaryUseResult DetermineUseResult(
|
||||
in ItemPolicyObject item,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue