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
|
|
@ -107,7 +107,7 @@ accepted-divergence entries (#96, #49, #50).
|
|||
|
||||
---
|
||||
|
||||
## 3. Documented approximation (AP) — 88 active rows
|
||||
## 3. Documented approximation (AP) — 89 active rows
|
||||
|
||||
Wave-0 UI ledger repair (2026-07-10) retired stale AP-38, resolved the AP-84
|
||||
collision, restored overwritten paperdoll rows as AP-92/AP-93, and registered
|
||||
|
|
@ -218,6 +218,7 @@ AP-94..AP-112 for the confirmed retail-UI completion gaps.
|
|||
| AP-119 | Equal-generation CreateObject refresh applies the packet's complete `PhysicsDesc` to the existing `EntityEffectProfile`, including replacing its network sound/PES-table/default-script description. Retail's equal-`INSTANCE_TS` branch applies the individual ObjDesc/Parent-or-Position/Movement/State/Vector/WeenieDesc tail and does not call `CPhysicsObj::set_description` again. | `src/AcDream.App/Physics/LiveEntityNetworkUpdateController.cs` (`ILiveEntitySameGenerationUpdateSink.OnDescription`); `LiveEntitySameGenerationUpdateRouter.cs` | Existing spell/projectile/portal VFX tests and connected behavior were accepted with this refresh. Slice 4 records and isolates it rather than silently changing DAT-effect ownership during an architecture extraction. | A same-generation CreateObject whose PeTable/sound/default-script fields differ from the original can replace effect lookup state where retail would retain the original table, producing a different later typed effect. | `SmartBox::HandleCreateObject @ 0x00454C80`; `CPhysicsObj::set_description @ 0x00514F40`; `docs/research/2026-07-13-retail-projectile-vfx-pseudocode.md` |
|
||||
| AP-120 | `ObjectTableWiring.ApplyEntitySpawn` publishes the CreateObject's WeenieDesc/item state before the same-generation physics update tail. Retail applies WeenieDesc after ObjDesc, Parent-or-Position/Pickup, Movement, State, and Vector. | `src/AcDream.Core.Net/ObjectTableWiring.cs` (`ApplyEntitySpawn`); `src/AcDream.App/World/LiveEntitySameGenerationUpdateRouter.cs` | Core.Net owns item-table ingestion before App callbacks and the current single-thread FIFO prevents a second network packet from interleaving; changing publication order crosses the Core.Net/App ownership boundary and requires a separately tested event transaction. | A synchronous item-table observer can see the refreshed WeenieDesc while the same object's physics/parent/state still reflects the prior snapshot; retail observers see the completed physics tail first. | `SmartBox::HandleCreateObject @ 0x00454C80`; `ACCObjectMaint::CreateObject @ 0x00558870` |
|
||||
| AP-121 | The `/framerate` command persists acdream's live ShowFps value in the modern `settings.json` Display bag after performing retail's live toggle/notice behavior. The named retail `DoFrameRate` body itself only flips `fShowFramerate` and sends `SetFramerateDisplay`; no equivalent persistence write occurs in that command body. | `src/AcDream.App/Settings/RuntimeSettingsController.cs` (`ToggleFrameRate`) | This preserves acdream's shipped L.0 cross-launch display preference while keeping the live retail mechanism and SmartBox visibility result exact; it changes no gameplay or wire state | A relaunch can retain FPS visibility where the named retail command body alone would not; a future port of retail's wider preference owner must avoid double-persisting or applying conflicting startup state | `ClientCommunicationSystem::DoFrameRate @ 0x005707D0`; `CM_UI::SendNotice_SetFramerateDisplay @ 0x0047A050`; `gmSmartBoxUI::RecvNotice_SetFramerateDisplay @ 0x004D65E0` |
|
||||
| AP-122 | The toolbar Use hand is Ghosted when selection is empty. Retail sets state 1 (Normal) and an empty-selection click enters generic `TARGET_MODE_USE`; selected-object enablement and activation remain retail-faithful. | `src/AcDream.App/UI/Layout/ToolbarController.cs` (`RefreshUseButton`) | User explicitly requires no selection to present as unavailable. The disabled retained button consumes the click without opening a cursor; every nonempty selection still uses the exact retail CombatUse/type/ItemUses predicate and the shared activation owner. | Generic pick-then-use mode cannot be entered from the empty toolbar hand; an object must first be selected, after which the same Use command is available. | `gmToolbarUI::HandleSelectionChanged @ 0x004BF380`; `gmToolbarUI::ListenToElementMessage @ 0x004BEE90` |
|
||||
|
||||
## 4. Temporary stopgap (TS) — 38 active rows + TS-37 historical note (TS-20 retired 2026-07-16 — the later named-retail audit disproved the proposed DrawingBSP polygon filter; TS-37 is a retired-row historical note, not an active count; TS-39 retired R5-V3 — sticky seams bound to the ported PositionManager/StickyManager, radii threaded; TS-45 retired 2026-07-07 — hand-rolled `SphereCollision` replaced by the faithful CSphere family port, fixing the player-vs-monster crowd wedge; TS-3 retired 2026-07-07 — `frames_stationary_fall` accounting ported in the #182 verbatim UpdateObjectInternal rebuild, fixing the airborne falling-animation wedge; TS-41 retired 2026-07-07 — SERVERVEL synth-velocity remote body-drive replaced by the retail interp catch-up + unconditional MovementManager::UseTime, the remote-creature de-overlap #184; TS-42 retired 2026-07-19 — semantic animation completion now precedes the ordered Target/Movement/PartArray/Position tail; TS-44 narrowed again 2026-07-19 — complete orientation joined interpolation, only during-stick enqueue suppression remains)
|
||||
|
||||
|
|
|
|||
|
|
@ -12,14 +12,14 @@ The active work order is
|
|||
It closes six remaining retail interaction surfaces before larger M4 subsystem
|
||||
bodies begin: spell-bar overflow controls, status-bar Use/Assess commands,
|
||||
assessment information, equipped-child picking, vendor browsing, and
|
||||
server-authoritative vendor transactions. Slice 1 is active and ports the
|
||||
DAT-authored arrow-only favorite-spell scrollbar through the shared retained
|
||||
scrollbar/list model. The corrected import preserves authored arrow sides and
|
||||
rollover/pressed media, passive refresh preserves manual offsets, and the
|
||||
mixed-parent DAT anchor chain produces the fixed retail capacity of 18 visible
|
||||
favorite cells (nine numbered plus nine unnumbered). Its
|
||||
implementation and complete Release suite pass; connected overflow-arrow
|
||||
visual acceptance is pending.
|
||||
server-authoritative vendor transactions. Slice 1's DAT-authored arrow-only
|
||||
favorite-spell scrollbar, fixed 18-cell capacity, authored arrow sides/media,
|
||||
and persistent manual offset passed the connected gate. Slice 2 is active: the
|
||||
status hand now follows canonical selection/object notices, ghosts empty and
|
||||
unusable selections, and routes enabled weapons, direct-use items, and
|
||||
use-on-target tools through the existing interaction transaction and cursor
|
||||
owners. Its connected hand-state gate is pending; the magnifying-glass Assess
|
||||
command follows in the same slice.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -89,6 +89,11 @@ transactions. This is deliberately using the extracted interaction owners and
|
|||
canonical shared main-panel host before quest/emote/character-creation bodies
|
||||
broaden the feature surface.
|
||||
|
||||
Slice 1's 18-cell favorite-spell overflow bar is user-accepted. Slice 2 is
|
||||
active: the status hand's selected-object availability and existing
|
||||
Use/AutoWield/use-on-target command routing are code-complete, with the
|
||||
connected state/cursor gate pending before the Assess half continues.
|
||||
|
||||
Carried:
|
||||
#153 far-teleport residual, #116 slide-response, the remaining R6 ownership cleanup plus
|
||||
registered TS-50/TS-51/TS-53 timing residuals (the complete-root-Frame/object-workset
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
# World interaction completion — pre-M4 program
|
||||
|
||||
**Status:** Slice 1 code-complete 2026-07-23; connected visual gate pending.
|
||||
**Status:** Slice 1 user-accepted 2026-07-23. Slice 2 Use-hand state and command
|
||||
path code-complete; connected visual gate pending before Assess continues.
|
||||
**Milestone:** M4 prerequisite/preamble.
|
||||
**Architecture:** retained gameplay UI over shared selection, object, and
|
||||
interaction state. `GameWindow` remains a composition/callback shell.
|
||||
|
|
@ -52,9 +53,21 @@ spells. Passive object/endowment refresh preserves a manual offset. The mixed
|
|||
producing exactly 18 visible 32-pixel favorite cells (nine numbered plus nine
|
||||
unnumbered) without consuming the overflow. The initial 57-test/full-suite gate passed;
|
||||
the corrective 104-test focus set, 3,472 App tests / 3 skips, Release solution
|
||||
build, and 7,845 complete-solution tests / 5 skips pass. The connected gate is
|
||||
pending after user feedback on arrow sides, rollover, width, and offset
|
||||
persistence.
|
||||
build, and 7,845 complete-solution tests / 5 skips pass. The corrected
|
||||
18-cell bar and both overflow directions passed the connected gate.
|
||||
|
||||
Slice 2 now drives the toolbar hand from canonical `SelectionState` changes and
|
||||
live `ClientObjectTable` updates. The pure Core predicate ports
|
||||
`gmToolbarUI::HandleSelectionChanged`: combat-use items, armor/clothing/jewelry,
|
||||
and `ItemUses` values without `USEABLE_NO` remain active. Clicking still enters
|
||||
the existing `ItemInteractionController` command path, so weapons use the
|
||||
server-confirmed AutoWield transaction and targeted tools enter the existing
|
||||
use-on-target cursor. Empty selection and explicitly unusable spell components
|
||||
are ghosted and ignore clicks. Empty-selection ghosting is the user's explicit
|
||||
choice over retail's generic `TARGET_MODE_USE` entry and is registered as
|
||||
AP-122. The 14-test Core interaction focus, 49-test toolbar focus, Release
|
||||
solution build, 3,474 App tests / 3 skips, and 7,848 complete-solution tests /
|
||||
5 skips pass.
|
||||
|
||||
## Slice 1 — spell-bar overflow arrows
|
||||
|
||||
|
|
@ -115,3 +128,27 @@ Named retail references and executable pseudocode are recorded in
|
|||
- Existing stack, combat-power, chat, inventory, spellbook, and external
|
||||
container scrollbars retain their current behavior.
|
||||
- No substantial feature body enters `GameWindow`.
|
||||
|
||||
## Slice 2 — status Use/Assess commands
|
||||
|
||||
### Use-hand implementation
|
||||
|
||||
1. `ItemInteractionPolicy.IsToolbarUseEnabled` is the pure named-retail
|
||||
selection predicate.
|
||||
2. `ItemInteractionController.IsToolbarUseEnabled` adapts the selected live
|
||||
object without triggering a request or consuming the use throttle.
|
||||
3. `ToolbarController` subscribes to canonical `SelectionState.Changed` and
|
||||
selected-object add/update/remove notices, then sets the imported button's
|
||||
normal or ghosted state through `UiButton.Enabled`.
|
||||
4. Enabled clicks retain the one existing activation path:
|
||||
equipment enters `AutoWieldController`, ordinary use enters the normal Use
|
||||
request owner, and targeted items enter `UseItemOnTarget`, which already
|
||||
owns the retail target cursor.
|
||||
|
||||
### Remaining Slice 2 work
|
||||
|
||||
- Port the magnifying-glass Assess command state and activation.
|
||||
- Run the connected Use-hand visual gate: no selection, spell component,
|
||||
weapon, and health kit.
|
||||
- Continue directly into the Assess information slice after the combined
|
||||
status-control gate.
|
||||
|
|
|
|||
|
|
@ -579,8 +579,15 @@ HandleSelectionChanged(): // 0x004BF380
|
|||
```
|
||||
|
||||
The exact item-type mask in the use-button ghost test is visible as separate
|
||||
`InqType() & 4`, `& 2`, and `& 8` checks. Preserve those numeric tests until the
|
||||
retail `ITEM_TYPE` names are pinned.
|
||||
`InqType() & 4`, `& 2`, and `& 8` checks. The verbatim `ITEM_TYPE` enum now pins
|
||||
those values as Clothing, Armor, and Jewelry respectively.
|
||||
|
||||
**2026-07-23 connected requirement:** acdream intentionally ghosts the hand
|
||||
when the selection is empty. Retail instead sets state 1 and lets a click enter
|
||||
generic `TARGET_MODE_USE` (see `ListenToElementMessage @ 0x004BEE90`). Selected
|
||||
objects still use the exact predicate above and still activate through the
|
||||
ordinary `ItemHolder::UseObject` path. The empty-selection difference is
|
||||
registered as AP-122.
|
||||
|
||||
### 5.3 Meter notices
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ using AcDream.App.UI.Layout;
|
|||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using AcDream.Core.Selection;
|
||||
using Xunit;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
|
@ -515,6 +516,123 @@ public class ToolbarControllerTests
|
|||
Assert.Equal(InteractionModeKind.Examine, interaction.InteractionState.Current.Kind);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseButton_tracksEmptyUnusableAndTargetedSelection()
|
||||
{
|
||||
const uint player = 0x50000001u;
|
||||
const uint pack = 0x50000002u;
|
||||
const uint component = 0x50000003u;
|
||||
const uint healthKit = 0x50000004u;
|
||||
var (layout, _, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature });
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = pack, Type = ItemType.Container });
|
||||
repo.MoveItem(pack, player, 0);
|
||||
repo.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = component,
|
||||
Type = ItemType.SpellComponents,
|
||||
Useability = ItemUseability.No,
|
||||
});
|
||||
repo.MoveItem(component, pack, 0);
|
||||
repo.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = healthKit,
|
||||
Type = ItemType.Misc,
|
||||
Useability = 0x00220008u,
|
||||
TargetType = (uint)ItemType.Creature,
|
||||
});
|
||||
repo.MoveItem(healthKit, pack, 1);
|
||||
|
||||
var selection = new SelectionState();
|
||||
using var interaction = new ItemInteractionController(
|
||||
repo,
|
||||
() => player,
|
||||
sendUse: null,
|
||||
sendUseWithTarget: null,
|
||||
sendWield: null,
|
||||
sendDrop: null);
|
||||
using var controller = ToolbarController.Bind(
|
||||
layout,
|
||||
repo,
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_, _, _, _, _) => 0u,
|
||||
useItem: _ => { },
|
||||
itemInteraction: interaction,
|
||||
selectedObjectId: () => selection.SelectedObjectId ?? 0u,
|
||||
selection: selection);
|
||||
var use = Assert.IsType<UiButton>(layout.FindElement(UseButtonId));
|
||||
|
||||
Assert.False(use.Enabled);
|
||||
Assert.Equal("Ghosted", use.ActiveState);
|
||||
use.OnEvent(new UiEvent(0u, use, UiEventType.Click));
|
||||
Assert.Equal(InteractionModeKind.None, interaction.InteractionState.Current.Kind);
|
||||
|
||||
selection.Select(component, SelectionChangeSource.Inventory);
|
||||
Assert.False(use.Enabled);
|
||||
Assert.Equal("Ghosted", use.ActiveState);
|
||||
|
||||
selection.Select(healthKit, SelectionChangeSource.Inventory);
|
||||
Assert.True(use.Enabled);
|
||||
Assert.Equal("Normal", use.ActiveState);
|
||||
use.OnEvent(new UiEvent(0u, use, UiEventType.Click));
|
||||
Assert.Equal(
|
||||
InteractionModeKind.UseItemOnTarget,
|
||||
interaction.InteractionState.Current.Kind);
|
||||
|
||||
selection.Clear(SelectionChangeSource.Inventory);
|
||||
Assert.False(use.Enabled);
|
||||
Assert.Equal("Ghosted", use.ActiveState);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void UseButton_selectedCombatItem_routesExistingWieldPath()
|
||||
{
|
||||
const uint player = 0x50000001u;
|
||||
const uint pack = 0x50000002u;
|
||||
const uint sword = 0x50000003u;
|
||||
var (layout, _, _) = FakeToolbar();
|
||||
var repo = new ClientObjectTable();
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = player, Type = ItemType.Creature });
|
||||
repo.AddOrUpdate(new ClientObject { ObjectId = pack, Type = ItemType.Container });
|
||||
repo.MoveItem(pack, player, 0);
|
||||
repo.AddOrUpdate(new ClientObject
|
||||
{
|
||||
ObjectId = sword,
|
||||
Type = ItemType.MeleeWeapon,
|
||||
CombatUse = 1,
|
||||
Useability = ItemUseability.No,
|
||||
ValidLocations = EquipMask.MeleeWeapon,
|
||||
});
|
||||
repo.MoveItem(sword, pack, 0);
|
||||
|
||||
var selection = new SelectionState();
|
||||
selection.Select(sword, SelectionChangeSource.Inventory);
|
||||
var wields = new List<(uint Item, uint Location)>();
|
||||
using var interaction = new ItemInteractionController(
|
||||
repo,
|
||||
() => player,
|
||||
sendUse: null,
|
||||
sendUseWithTarget: null,
|
||||
sendWield: (item, location) => wields.Add((item, location)),
|
||||
sendDrop: null);
|
||||
using var controller = ToolbarController.Bind(
|
||||
layout,
|
||||
repo,
|
||||
() => Array.Empty<ShortcutEntry>(),
|
||||
iconIds: (_, _, _, _, _) => 0u,
|
||||
useItem: _ => { },
|
||||
itemInteraction: interaction,
|
||||
selectedObjectId: () => selection.SelectedObjectId ?? 0u,
|
||||
selection: selection);
|
||||
var use = Assert.IsType<UiButton>(layout.FindElement(UseButtonId));
|
||||
|
||||
Assert.True(use.Enabled);
|
||||
use.OnEvent(new UiEvent(0u, use, UiEventType.Click));
|
||||
|
||||
Assert.Equal(new[] { (sword, (uint)EquipMask.MeleeWeapon) }, wields);
|
||||
}
|
||||
|
||||
// ── C1: combat-mode indicator tests ─────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -7,6 +7,28 @@ public sealed class ItemInteractionPolicyTests
|
|||
private const uint Player = 0x50000001u;
|
||||
private const uint GroundContainer = 0x70000001u;
|
||||
|
||||
[Fact]
|
||||
public void ToolbarUseEnabled_matchesRetailSelectionPredicate()
|
||||
{
|
||||
Assert.True(ItemInteractionPolicy.IsToolbarUseEnabled(
|
||||
ItemType.MeleeWeapon,
|
||||
combatUse: 1,
|
||||
useability: ItemUseability.No));
|
||||
Assert.True(ItemInteractionPolicy.IsToolbarUseEnabled(
|
||||
ItemType.Armor | ItemType.Clothing,
|
||||
combatUse: 0,
|
||||
useability: ItemUseability.No));
|
||||
Assert.True(ItemInteractionPolicy.IsToolbarUseEnabled(
|
||||
ItemType.Misc,
|
||||
combatUse: 0,
|
||||
useability: 0x00220008u));
|
||||
|
||||
Assert.False(ItemInteractionPolicy.IsToolbarUseEnabled(
|
||||
ItemType.SpellComponents,
|
||||
combatUse: 0,
|
||||
useability: ItemUseability.No));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void DetermineUseResult_looseItemAndComponentPack_matchRetailPickupBranch()
|
||||
{
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue