fix(inventory): correct drag overlay priority
Keep the persistent selection indicator above retail's waiting mesh, use the normal green accept circle for backpack-grid placement, and reserve the green drop-in arrow for bag-column containers as ItemList_DragOver does. Co-Authored-By: Codex <codex@openai.com>
This commit is contained in:
parent
ace5880fed
commit
64a056e656
6 changed files with 63 additions and 19 deletions
|
|
@ -496,7 +496,7 @@ behavior. Estimated 17–26 days focused work, 3–5 weeks calendar.
|
|||
- **✓ SHIPPED — Wave 4.4d toolbar launchers + Use/Examine.** All seven authored launchers are discovered through their DAT panel-id attribute; mounted Inventory/Character panels toggle through the registry and the other five buttons are ghosted. Typed window-visibility events own Highlight/Normal state. Use and Examine act on selection or enter their one-shot retail target modes, with exact Appraise wire dispatch and retail cursors. Warning-free App build and 4,747-pass / 5-skip Release suite are green; the connected live gate passed 2026-07-11. AP-101 is narrowed to the remaining ammo-number display.
|
||||
- **Wave 4.4e implemented — exact missile ammo number (live gate pending).** Pure Core reproduces retail's thrown-weapon-vs-separate-ammo resolution over the ordered player equipment list and its zero-to-one count normalization. Relevant equipment/stack events update authored missile indicator `0x10000194` with the DAT font. Warning-free Release build and 4,754-pass / 5-skip suite are green; AP-101 is retired.
|
||||
- **Wave 4.5 implemented — retail primary-weapon switching (corrective live gate pending).** Inventory double-click now routes through a focused `AutoWieldController`: the occupied `0x03500000` weapon-ready group is returned to the player container, followed when necessary by an incompatible shield or mismatched ammo; each blocker waits for authoritative `InventoryPutObjInContainer` before the next step, then `GetAndWieldItem` equips the requested bow/sword/caster/two-hander. The transaction remains inventory-busy through the exact authoritative `WieldObject` acknowledgement, independent of the object table's rollback counter, matching retail's waiting state and preventing rapid switches from overlapping or remaining stuck. `CreateObject` retains exact `AmmoType` for compatibility. The same transaction runs in peace and war; ACE owns the latter's old-stance → peace → new-stance motion chain. Player PropertyInt 40 updates `CombatState`, keeping toolbar/combat UI synchronized with ACE's selected Melee/Missile/Magic stance. Research: `docs/research/2026-07-12-retail-weapon-switch-pseudocode.md`.
|
||||
- **Wave 4 inventory drag visuals implemented — live visual gate pending.** `IconComposer` now preserves retail's separate underlay-free `m_pDragIcon` for cursor tracking across inventory, paperdoll, and toolbar bindings. Physical source cells keep their full icon in place and reveal authored ghost mesh `0x0600109A` from drag begin through every release/cancel path; shortcut aliases remain unghosted. AP-47 retired. Research: `docs/research/2026-07-13-retail-item-drag-visuals-pseudocode.md`.
|
||||
- **Wave 4 inventory drag visuals implemented — corrective live visual gate pending.** `IconComposer` now preserves retail's separate underlay-free `m_pDragIcon` for cursor tracking across inventory, paperdoll, and toolbar bindings. Physical source cells keep their full icon in place and reveal authored ghost mesh `0x0600109A` from drag begin through every release/cancel path, with the persistent selection square above the mesh; shortcut aliases remain unghosted. `ItemList_DragOver` destination states are split exactly: contents-grid placement uses the green accept circle `0x060011F9`, while side-bag/main-pack container drop-in uses the green arrow `0x060011F7`. AP-47 retired. Research: `docs/research/2026-07-13-retail-item-drag-visuals-pseudocode.md`.
|
||||
- **M2 held-object parenting shipped and live-gated 2026-07-11.** The combat toggle now ports `GetDefaultCombatMode` over ordered equipped contents, so a bow requests Missile instead of the old hardcoded Melee. CreateObject preserves parent/placement/timestamp fields, `0xF749` ParentEvent is handled, and `EquippedChildRenderController` renders the weapon as a separate child composed from the animated hand part + holding frame + child placement frame. Live gate passed: bow selected missile stance, rendered in-hand, followed animation, unequipped cleanly, and melee remained correct. App Release builds with zero warnings; the full 4,765-pass / 5-skip suite is green. AP-111 is retired; research: `docs/research/2026-07-11-combat-default-and-parent-event-pseudocode.md`.
|
||||
- **M2 local attack receive funnel implemented 2026-07-11; live gate pending.** Retail `ExecuteAttack` only sends the request; ACE chooses the concrete melee/missile action and returns it in a non-autonomous mt-0 `UpdateMotion`. The local branch now runs that state through the same constructor-defaulted `MoveToInterpretedState` funnel and 15-bit action-stamp gate as remotes, then applies sticky/long-jump tails. The old local-only direct `Commands[]` replay is deleted. Shared conversion lives in `InboundInterpretedMotionFactory`; research: `docs/research/2026-07-11-local-combat-motion-pseudocode.md`.
|
||||
- **M2 basic retail combat bar implemented 2026-07-11; corrective live visual gate pending.** Production mounts authored `gmCombatUI` LayoutDesc `0x21000073`, shows it only for Melee/Missile, and routes mouse plus keyboard through one `CombatAttackController`. Corrections include the authored wider centered dark-red child range (accepted IA-20), full-width left-to-right bright live attack-charge feedback, exact Speed-left/Power-right justification, silent control-only `AttackDone(ActionCancelled)`, target-frame Keep in View with manual orbit, and persistent corpse motion: the AP-80 velocity-only NPC adaptation can now replace only Ready/Walk/Run, never authoritative Dead/actions. `CombatTargetController` ports the selection-cleared AutoTarget consumer, so a selected creature's authoritative Dead motion clears it and selects the nearest eligible creature when enabled. The 2026-07-12 replacement-corpse correction unifies both multi-frame and static/reactive spawns behind retail's CreateObject lifecycle: apply the wire's Dead state while detached, then `MotionTableManager::HandleEnterWorld` strips Ready→Dead links before the first in-world tick; multiple corpses remaining fallen passed the live user gate that day. Follow-up #205 makes the toolbar read the final canonical selection after a reentrant Auto Target notice and restricts automatic candidates to hostile non-player monsters (intentional PK-edge divergence IA-19); that live gate also passed 2026-07-12. Research: `docs/research/2026-07-11-retail-combat-bar-pseudocode.md`, `docs/research/2026-07-11-combat-target-camera-pseudocode.md`, `docs/research/2026-07-12-death-and-auto-target-pseudocode.md`; AP-24/AP-95 retired, AP-80 narrowed, AP-110 narrowed, AP-112 records the remaining attack-start and exact trained-Recklessness seams.
|
||||
|
|
|
|||
|
|
@ -65,6 +65,30 @@ on drag release/cancel:
|
|||
hide m_elem_Icon_Ghosted
|
||||
```
|
||||
|
||||
## Destination feedback and source overlay order
|
||||
|
||||
`UIElement_ItemList::ItemList_DragOver @ 0x004e3400` distinguishes an ordinary
|
||||
slot insertion from dropping into a container:
|
||||
|
||||
```text
|
||||
if target list is a container selector
|
||||
and target cell is occupied by a container
|
||||
and that container has an empty item slot:
|
||||
target.SetDragAcceptState(0x10000046) # ItemSlot_DragOver_DropIn
|
||||
# 0x060011F7 green arrow
|
||||
else if target accepts an ordinary item-list placement:
|
||||
target.SetDragAcceptState(0x10000041) # ItemSlot_DragOver_Accept
|
||||
# 0x060011F9 green circle
|
||||
else:
|
||||
target.SetDragAcceptState(0x10000040) # 0x060011F8 reject
|
||||
```
|
||||
|
||||
Therefore the backpack contents grid uses the green circle; the side-bag column
|
||||
and main-pack container cell use the green drop-in arrow. The selected/open
|
||||
indicators remain visible while `m_elem_Icon_Ghosted` is active, so the
|
||||
procedural leaf draws the waiting mesh before those persistent indicators and
|
||||
keeps drag-accept feedback topmost.
|
||||
|
||||
## Port mapping
|
||||
|
||||
- `IconComposer` caches the underlay-free composite separately and exposes
|
||||
|
|
@ -85,6 +109,6 @@ on drag release/cancel:
|
|||
- `DragDropSpineTests.InventoryDrag_ghostsSourceUntilRelease`
|
||||
- `DragDropSpineTests.ShortcutDrag_doesNotGhostSource`
|
||||
- `UiItemSlotTests.DefaultWaitingSprite_isRetailGhostMesh`
|
||||
- `InventoryControllerTests.Empty_sprites_and_drop_feedback_are_applied_per_list_role`
|
||||
- Existing `IconComposerTests.TwoStageWithEffect_copiesTilePixelBeforeUnderlay`
|
||||
locks the two-stage composition order.
|
||||
|
||||
|
|
|
|||
|
|
@ -365,8 +365,7 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
var cell = new UiItemSlot { SpriteResolve = list.SpriteResolve };
|
||||
cell.SetItem(guid, tex, dragIconTexture: dragTex);
|
||||
cell.SlotIndex = list.GetNumUIItems(); // index it will occupy (== its slot in a packed list)
|
||||
cell.DragAcceptSprite = 0x060011F7u; // green insert arrow (export-confirmed)
|
||||
cell.DragRejectSprite = 0x060011F8u; // red circle
|
||||
ConfigureDropFeedback(list, cell);
|
||||
cell.DoubleClicked = () => _itemInteraction?.ActivateItem(guid);
|
||||
if (isContainer)
|
||||
{
|
||||
|
|
@ -388,14 +387,27 @@ public sealed class InventoryController : IItemListDragHandler, IRetainedPanelCo
|
|||
fallback();
|
||||
}
|
||||
|
||||
private static void AddEmptyCell(UiItemList list)
|
||||
private void AddEmptyCell(UiItemList list)
|
||||
{
|
||||
var cell = new UiItemSlot { SpriteResolve = list.SpriteResolve, SlotIndex = list.GetNumUIItems() };
|
||||
cell.DragAcceptSprite = 0x060011F7u;
|
||||
cell.DragRejectSprite = 0x060011F8u;
|
||||
ConfigureDropFeedback(list, cell);
|
||||
list.AddItem(cell);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Bind the exact ItemList_DragOver state for this destination. A normal contents-grid
|
||||
/// insertion uses ItemSlot_DragOver_Accept (0x10000041 → green circle 0x060011F9).
|
||||
/// An occupied container selector uses ItemSlot_DragOver_DropIn
|
||||
/// (0x10000046 → green arrow 0x060011F7). Retail: 0x004e3400.
|
||||
/// </summary>
|
||||
private void ConfigureDropFeedback(UiItemList list, UiItemSlot cell)
|
||||
{
|
||||
cell.DragAcceptSprite = ReferenceEquals(list, _contentsGrid)
|
||||
? 0x060011F9u
|
||||
: 0x060011F7u;
|
||||
cell.DragRejectSprite = 0x060011F8u;
|
||||
}
|
||||
|
||||
/// <summary>Set the per-cell container capacity bar — retail UIElement_UIItem::UpdateCapacityDisplay
|
||||
/// (0x004e16e0): visible only for a container with itemsCapacity > 0; fill =
|
||||
/// GetNumContainedItems / itemsCapacity, clamped [0,1]. -1 hides the bar (non-container / unknown
|
||||
|
|
|
|||
|
|
@ -338,10 +338,22 @@ public sealed class UiItemSlot : UiElement
|
|||
}
|
||||
}
|
||||
|
||||
// Pending/drag-source mesh — retail UIElement_UIItem::SetWaitingState (0x004e11b0)
|
||||
// reveals m_elem_Icon_Ghosted (0x10000349). ItemList_BeginDrag (0x004e32d0)
|
||||
// sets it for physical inventory/equipment cells while leaving their icon in place.
|
||||
// Draw it before the persistent selected/open indicators: retail keeps selection visible
|
||||
// while the source is waiting, so the grey mesh must not erase the selected square.
|
||||
if (_waiting && SpriteResolve is not null && WaitingSprite != 0)
|
||||
{
|
||||
var (tex, _, _) = SpriteResolve(WaitingSprite);
|
||||
if (tex != 0)
|
||||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
// Open-container triangle (retail SetOpenContainerState 0x004e1200) + selected-item square
|
||||
// (retail SetSelectedState 0x004e1240). Drawn procedurally like the digit/drag overlays;
|
||||
// guard id != 0 BEFORE resolving (feedback_ui_resolve_zero_magenta). Triangle under the
|
||||
// square; both under the transient drag overlay below.
|
||||
// square; both remain above the transient waiting mesh and below drag-accept feedback.
|
||||
if (IsOpenContainer && SpriteResolve is not null && OpenContainerSprite != 0)
|
||||
{
|
||||
var (tex, _, _) = SpriteResolve(OpenContainerSprite);
|
||||
|
|
@ -355,16 +367,6 @@ public sealed class UiItemSlot : UiElement
|
|||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
// Pending/drag-source mesh — retail UIElement_UIItem::SetWaitingState (0x004e11b0)
|
||||
// reveals m_elem_Icon_Ghosted (0x10000349). ItemList_BeginDrag (0x004e32d0)
|
||||
// sets it for physical inventory/equipment cells while leaving their icon in place.
|
||||
if (_waiting && SpriteResolve is not null && WaitingSprite != 0)
|
||||
{
|
||||
var (tex, _, _) = SpriteResolve(WaitingSprite);
|
||||
if (tex != 0)
|
||||
ctx.DrawSprite(tex, 0f, 0f, Width, Height, 0f, 0f, 1f, 1f, Vector4.One);
|
||||
}
|
||||
|
||||
// Drag-rollover accept/reject frame (retail SetDragAcceptState 0x10000041/40).
|
||||
// Guard id != 0 BEFORE resolving — resolve(0) returns the 1×1 magenta placeholder
|
||||
// with a non-zero GL handle (feedback_ui_resolve_zero_magenta).
|
||||
|
|
|
|||
|
|
@ -202,13 +202,16 @@ public class DragDropSpineTests
|
|||
{
|
||||
var (root, _, cell) = RootWithBoundSlot(0x5001u);
|
||||
cell.SourceKind = ItemDragSource.Inventory;
|
||||
cell.Selected = true;
|
||||
|
||||
root.OnMouseDown(UiMouseButton.Left, 10, 10);
|
||||
root.OnMouseMove(20, 10);
|
||||
Assert.True(cell.WaitingVisual);
|
||||
Assert.True(cell.Selected); // selected indicator remains active above the waiting mesh
|
||||
|
||||
root.OnMouseUp(UiMouseButton.Left, 600, 500);
|
||||
Assert.False(cell.WaitingVisual);
|
||||
Assert.True(cell.Selected);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ public class InventoryControllerTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void Empty_sprites_are_applied_to_the_three_lists()
|
||||
public void Empty_sprites_and_drop_feedback_are_applied_per_list_role()
|
||||
{
|
||||
var (layout, grid, containers, top, _, _, _, _) = BuildLayout();
|
||||
InventoryController.Bind(layout, new ClientObjectTable(), () => Player,
|
||||
|
|
@ -351,6 +351,9 @@ public class InventoryControllerTests
|
|||
Assert.Equal(0x06004D20u, grid.GetItem(0)!.EmptySprite); // a padded empty contents cell
|
||||
Assert.Equal(0x06005D9Cu, containers.GetItem(0)!.EmptySprite); // a padded empty side-bag cell
|
||||
Assert.Equal(0x06005D9Cu, top.GetItem(0)!.EmptySprite); // the main-pack cell
|
||||
Assert.Equal(0x060011F9u, grid.GetItem(0)!.DragAcceptSprite); // normal slot accept = green circle
|
||||
Assert.Equal(0x060011F7u, containers.GetItem(0)!.DragAcceptSprite); // container drop-in = green arrow
|
||||
Assert.Equal(0x060011F7u, top.GetItem(0)!.DragAcceptSprite); // main container drop-in = green arrow
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue