feat(ui): port favorite spell bar overflow
Import the retail arrow-only spell bar scrollbar from LayoutDesc, preserve authored end-button extents and HideDisabled behavior in the shared retained widget, and bind each favorite list to its sole horizontal pixel-scroll model. Match retail selection exposure for off-screen spells and pin the behavior with real-fixture conformance tests. Document the six-slice world-interaction completion program as the active pre-M4 work order. Co-authored-by: Codex <codex@openai.com>
This commit is contained in:
parent
20e2998adb
commit
02c29e67c8
12 changed files with 582 additions and 45 deletions
|
|
@ -5,7 +5,21 @@
|
|||
|
||||
---
|
||||
|
||||
## Current program: `GameWindow` structural stabilization (M4 prerequisite)
|
||||
## Current program: world interaction completion (M4 prelude)
|
||||
|
||||
The active work order is
|
||||
[`2026-07-23-world-interaction-completion.md`](2026-07-23-world-interaction-completion.md).
|
||||
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. Its implementation and complete Release suite pass;
|
||||
connected overflow-arrow visual acceptance is pending.
|
||||
|
||||
---
|
||||
|
||||
## Completed program: `GameWindow` structural stabilization (M4 prerequisite)
|
||||
|
||||
**Outcome:** `GameWindow` becomes a thin Silk.NET composition/lifecycle shell
|
||||
without changing accepted gameplay or rendering behavior. Focused App owners
|
||||
|
|
|
|||
|
|
@ -81,6 +81,14 @@ recall-tail regression gates also pass. The capped/RDP jump presentation
|
|||
cadence alias remains non-blocking issue #235; it is smooth uncapped and does
|
||||
not alter the retail 30 Hz object clock.
|
||||
|
||||
The immediate M4 prelude is the six-slice
|
||||
[`world interaction completion`](2026-07-23-world-interaction-completion.md)
|
||||
program: spell-bar overflow, status Use/Assess, assessment information,
|
||||
equipped-child picking, vendor browsing, and authoritative vendor
|
||||
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.
|
||||
|
||||
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
|
||||
|
|
|
|||
106
docs/plans/2026-07-23-world-interaction-completion.md
Normal file
106
docs/plans/2026-07-23-world-interaction-completion.md
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
# World interaction completion — pre-M4 program
|
||||
|
||||
**Status:** Slice 1 code-complete 2026-07-23; connected visual gate pending.
|
||||
**Milestone:** M4 prerequisite/preamble.
|
||||
**Architecture:** retained gameplay UI over shared selection, object, and
|
||||
interaction state. `GameWindow` remains a composition/callback shell.
|
||||
|
||||
## Outcome
|
||||
|
||||
Close the remaining retail interaction surfaces before the larger M4
|
||||
quest/emote/character-creation bodies begin:
|
||||
|
||||
1. Favorite spell bars expose their DAT-authored overflow arrows and scroll
|
||||
through every server-persisted favorite.
|
||||
2. The status bar's hand and magnifying-glass controls invoke the same Use and
|
||||
Assess commands as their keyboard paths.
|
||||
3. Assessing a creature, player, NPC, or object opens its retail information in
|
||||
the canonical shared main-panel host.
|
||||
4. World picking can resolve visible equipped children, such as a character's
|
||||
wielded weapon, while selection markers remain anchored to the picked child.
|
||||
5. Vendor use opens the authored vendor surface, publishes its inventory, and
|
||||
supports retail selection/browsing.
|
||||
6. Vendor buy/sell transactions, quantities, pending-state ownership, and
|
||||
authoritative inventory reconciliation complete the loop.
|
||||
|
||||
The program reuses the existing retained-window host, `SelectionState`,
|
||||
`ClientObjectTable`, interaction transaction owner, and server-authoritative
|
||||
inventory updates. It does not create parallel panel positions, item tables,
|
||||
selection state, or optimistic inventory outcomes.
|
||||
|
||||
## Ordered slices
|
||||
|
||||
| Slice | Deliverable | Principal owner |
|
||||
|---|---|---|
|
||||
| 1 | Spell-bar overflow arrows | `SpellcastingUiController` + generic retained scrollbar/list |
|
||||
| 2 | Status Use/Assess commands | focused status-bar controller binding to the existing action router |
|
||||
| 3 | Assessment information panel | new retained panel controller mounted by the canonical main-panel host |
|
||||
| 4 | Equipped-child world picking | pure world-query/picking policy plus presentation anchor |
|
||||
| 5 | Vendor browse lifecycle | vendor session/controller plus authored panel |
|
||||
| 6 | Vendor transactions | server-authoritative buy/sell command and reconciliation owner |
|
||||
|
||||
Each slice begins with named-retail research, produces pseudocode and
|
||||
conformance tests, updates the divergence register if required, and lands as a
|
||||
separate bisectable commit. A visual gate follows each UI-bearing slice.
|
||||
|
||||
Slice 1 now imports both 23-pixel arrow buttons and HideDisabled from the real
|
||||
combat fixture, shares the list's single pixel-scroll model, advances one
|
||||
32-pixel cell per press, and exposes programmatically selected spells. The
|
||||
focused 57-test set, all 3,466 App tests / 3 skips, the Release solution build,
|
||||
and all 7,839 solution tests / 5 skips pass. Only the connected visual gate
|
||||
remains before Slice 2 starts.
|
||||
|
||||
## Slice 1 — spell-bar overflow arrows
|
||||
|
||||
### Retail oracle
|
||||
|
||||
The authored Magic combat layout contains, inside every favorite-tab group:
|
||||
|
||||
- horizontal scrollbar `0x100000B5`, 685×36 at the 800-pixel design width;
|
||||
- decrement button `0x10000071`, 23×36;
|
||||
- increment button `0x10000072`, 23×36;
|
||||
- item list `0x100000B6`, inset by 23 pixels on both sides and 32 pixels high.
|
||||
|
||||
The list references the scrollbar through `UIElement_Scrollable` property
|
||||
`0x71`. The scrollbar references increment/decrement buttons through
|
||||
properties `0x77`/`0x78`, and property `0x79` enables HideDisabled. There is no
|
||||
track or thumb in this specific control: the two authored arrows are the whole
|
||||
visible scrollbar.
|
||||
|
||||
Named retail references and executable pseudocode are recorded in
|
||||
[`../research/2026-07-23-retail-spellbar-overflow-pseudocode.md`](../research/2026-07-23-retail-spellbar-overflow-pseudocode.md).
|
||||
|
||||
### Implementation plan
|
||||
|
||||
1. Generalize `DatWidgetFactory.BuildScrollbar` so horizontal and vertical
|
||||
scrollbars both import the referenced decrement/increment children,
|
||||
including their authored dimensions and HideDisabled property.
|
||||
2. Generalize `UiScrollbar` to use distinct authored decrement/increment
|
||||
extents for rendering, hit-testing, track geometry, and dragging. Preserve
|
||||
the existing 16-pixel default for layouts whose button children are absent.
|
||||
3. Reproduce retail disabled presentation: a model without overflow rejects
|
||||
pointer input; when HideDisabled is authored, the scrollbar also draws
|
||||
nothing and does not claim hit tests.
|
||||
4. In `SpellcastingUiController`, bind each group's scrollbar to its
|
||||
`UiItemList.Scroll`, enable horizontal scrolling, and retain one independent
|
||||
pixel offset per favorite tab.
|
||||
5. Match `SpellCastSubMenu::SetSelected`: selecting a spell through keyboard,
|
||||
shortcut, or code scrolls that item into view.
|
||||
6. Pin the importer, arrow hit extents, 32-pixel step, no-overflow behavior,
|
||||
controller binding, and selection exposure with focused App tests.
|
||||
7. Run the App Release suite, solution Release build, and complete Release
|
||||
suite. Then update the roadmap/memory and request the connected visual gate:
|
||||
place more favorites than fit, scroll both directions, change tabs, and
|
||||
verify arrows disappear on a non-overflowing tab.
|
||||
|
||||
### Invariants
|
||||
|
||||
- DAT supplies the controls and their artwork; no new spell-bar texture or
|
||||
overlay is invented.
|
||||
- The scrollbar and list share one `UiScrollable`; there is no second offset.
|
||||
- One arrow press moves one 32-pixel favorite cell, matching
|
||||
`UIElement_ListBox::InqScrollDelta`.
|
||||
- Hidden disabled arrows cannot intercept combat-page dragging or clicks.
|
||||
- Existing stack, combat-power, chat, inventory, spellbook, and external
|
||||
container scrollbars retain their current behavior.
|
||||
- No substantial feature body enters `GameWindow`.
|
||||
141
docs/research/2026-07-23-retail-spellbar-overflow-pseudocode.md
Normal file
141
docs/research/2026-07-23-retail-spellbar-overflow-pseudocode.md
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
# Retail favorite spell-bar overflow pseudocode
|
||||
|
||||
This note is the implementation oracle for world-interaction completion Slice
|
||||
1. Addresses refer to the September 2013 EoR named retail client. The exact
|
||||
element graph and inherited properties were verified against combat LayoutDesc
|
||||
`0x21000073` and the existing imported fixture
|
||||
`combat_21000073.json`.
|
||||
|
||||
## Authored element graph
|
||||
|
||||
Each of the eight `SpellCastSubMenu` groups contains:
|
||||
|
||||
```text
|
||||
favorite group
|
||||
scrollbar 0x100000B5, 685x36
|
||||
decrement button 0x10000071, 23x36
|
||||
Normal 0x06004CDC
|
||||
Rollover 0x06004CDD
|
||||
increment button 0x10000072, 23x36
|
||||
Normal 0x06004CDE
|
||||
Rollover 0x06004CDF
|
||||
item list 0x100000B6, x=23, y=2, 639x32
|
||||
|
||||
scrollbar property 0x77 = increment button 0x10000072
|
||||
scrollbar property 0x78 = decrement button 0x10000071
|
||||
scrollbar property 0x79 = HideDisabled true
|
||||
item-list property 0x71 = horizontal scrollbar 0x100000B5
|
||||
```
|
||||
|
||||
The spell-bar scrollbar has no authored track or thumb. Its 23-pixel buttons
|
||||
occupy the space immediately outside the 639-pixel item-list viewport.
|
||||
|
||||
## Initialization and ownership
|
||||
|
||||
Retail:
|
||||
|
||||
- `SpellCastSubMenu::Init @ 0x004C5A90`
|
||||
- `UIElement_Scrollable::PostInit @ 0x004744B0`
|
||||
- `UIElement_Scrollable::OnSetAttribute @ 0x00474630`
|
||||
|
||||
```text
|
||||
SpellCastSubMenu.Init(parent, groupId, tabId, tabIndex):
|
||||
group = parent.GetChildRecursive(groupId)
|
||||
spellItemList = group.GetChildRecursive(0x100000B6)
|
||||
register this as the item-list drag handler
|
||||
spellTabElement = parent.GetChildRecursive(tabId)
|
||||
|
||||
UIElement_Scrollable.OnSetAttribute(property):
|
||||
if property id == 0x71:
|
||||
horizontalScrollbarId = property element id
|
||||
|
||||
UIElement_Scrollable.PostInit():
|
||||
horizontalScrollbar = resolve horizontalScrollbarId
|
||||
register for scrollbar messages
|
||||
UpdateScrollbarSize(horizontal=true)
|
||||
```
|
||||
|
||||
The controller does not synthesize arrow buttons. LayoutDesc owns them; the
|
||||
ordinary scrollable/listbox relationship owns the offset.
|
||||
|
||||
## Size, disabled state, and visibility
|
||||
|
||||
Retail:
|
||||
|
||||
- `UIElement_Scrollable::UpdateScrollbarSize_ @ 0x004741A0`
|
||||
- `UIElement_Scrollbar::SetDisabled @ 0x004703D0`
|
||||
- `UIElement_Scrollbar::SetHideDisabled @ 0x00470400`
|
||||
- `UIElement_Scrollbar::UpdateLayout @ 0x004710D0`
|
||||
|
||||
```text
|
||||
UpdateScrollbarSize(horizontal):
|
||||
content = scrollableWidth
|
||||
viewport = element.Width
|
||||
|
||||
if content <= viewport:
|
||||
content = viewport
|
||||
scrollOffsetX = 0
|
||||
disabled = true
|
||||
else:
|
||||
disabled = false
|
||||
|
||||
scrollbar.Horizontal = true
|
||||
scrollbar.Disabled = disabled
|
||||
scrollbar.Proportion = min(1, viewport / content)
|
||||
|
||||
Scrollbar.UpdateLayout():
|
||||
scrollingArea = own rectangle
|
||||
position decrement button at the start and subtract its authored width
|
||||
position increment button at the end and subtract its authored width
|
||||
update thumb/track geometry, when those children exist
|
||||
Visible = NOT (Disabled AND HideDisabled)
|
||||
```
|
||||
|
||||
For the favorite bar, disabled plus HideDisabled removes both arrows when every
|
||||
favorite/empty cell fits. With overflow, the arrows remain visible and the
|
||||
item list clips the horizontally translated cells.
|
||||
|
||||
## Scroll delta and selection exposure
|
||||
|
||||
Retail:
|
||||
|
||||
- `UIElement_Scrollable::HandleScrollbarMessage_ @ 0x00474370`
|
||||
- `UIElement_ListBox::InqScrollDelta @ 0x0046DC10`
|
||||
- `SpellCastSubMenu::SetSelected @ 0x004C5B00`
|
||||
|
||||
```text
|
||||
on decrement:
|
||||
delta = -itemList.InqScrollDelta(horizontal, line)
|
||||
set horizontal pixel offset += delta
|
||||
|
||||
on increment:
|
||||
delta = +itemList.InqScrollDelta(horizontal, line)
|
||||
set horizontal pixel offset += delta
|
||||
|
||||
ItemList.InqScrollDelta(horizontal, line):
|
||||
if column count is nonzero:
|
||||
return min(viewportWidth, scrollableWidth / columnCount)
|
||||
return 0
|
||||
|
||||
SpellCastSubMenu.SetSelected(spellId):
|
||||
for each item in list order:
|
||||
item.Selected = item.spellId == spellId
|
||||
if selected:
|
||||
endowmentSelected = false
|
||||
itemList.ScrollToView(itemIndex)
|
||||
selectedSpellId = spellId
|
||||
```
|
||||
|
||||
The favorite cells are 32 pixels wide, so a line arrow moves exactly 32
|
||||
pixels. Programmatic or keyboard selection exposes the selected cell even when
|
||||
it was outside the prior viewport.
|
||||
|
||||
## Port constraints
|
||||
|
||||
- Preserve the imported `0x71`/`0x77`/`0x78` relationships.
|
||||
- Preserve each authored button's dimension; do not assume the 16-pixel chat
|
||||
scrollbar button size.
|
||||
- Do not fabricate a track/thumb for this arrow-only control.
|
||||
- Derive disabled state from the shared model's content and viewport extents.
|
||||
- Hide disabled controls from both drawing and hit-testing.
|
||||
- Keep every favorite tab's scroll offset independent.
|
||||
Loading…
Add table
Add a link
Reference in a new issue