feat(ui): port retail spellbook interactions

Resolve the authored spell shortcut row prototype so the spellbook presents the retail icon, name, separator, selected overlay, and scrollbar geometry. Port exact school and level filters, stable display ordering, selection exposure, and learned-spell drags into the open favorite bar.

Route deletion through the shared retail confirmation dialog and send CM_Magic::Event_RemoveSpell only after an affirmative answer, leaving the inbound server notice authoritative for list state.

Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
Erik 2026-07-15 16:26:30 +02:00
parent ad30c37a48
commit ac2ca8f965
17 changed files with 727 additions and 47 deletions

View file

@ -240,6 +240,105 @@ sorts by `CSpellBase::_display_order`; school and level buttons apply the
server-persisted filter bitfield. Component rows use the installed component
table and the server-persisted desired purchase amounts.
### Spellbook row, selection, filtering, dragging, and deletion
Named-retail anchors:
- `gmSpellbookUI::IsFilteredOut @ 0x0048AEB0`
- `gmSpellbookUI::GetSortedInsertionPlace @ 0x0048B440`
- `gmSpellbookUI::SetSelected @ 0x0048B540`
- `gmSpellbookUI::UpdateFilter @ 0x0048B5E0`
- `gmSpellbookUI::AddSpell @ 0x0048B7A0`
- `gmSpellbookUI::DeleteSpellDialogCallback @ 0x0048BC10`
- `gmSpellbookUI::DeleteSpell @ 0x0048BD50`
- `gmSpellbookUI::ListenToElementMessage @ 0x0048BFD0`
- `UIElement_UIItem::Init_UIItem_Spell_Shortcut @ 0x004E1140`
- `UIElement_ItemList::ItemList_AddSpellShortcut @ 0x004E40A0`
- `UIElement_ItemList::ItemList_InsertSpellShortcut @ 0x004E41B0`
- `CM_Magic::Event_RemoveSpell @ 0x006A3220`
DAT structure:
```text
spell list 0x10000295 (280 x 224)
spell scrollbar 0x10000296 (16 x 224)
spell-row prototype property 0x1000000E -> 0x10000343
shared UIItem catalog 0x21000037
row prototype 0x10000343 (280 x 32)
row background 0x06001396
selected overlay child 0x10000342 -> 0x06001397
label child 0x10000344 (x=42, width=230)
icon child 0x1000033B (x=0, 32 x 32)
delete button 0x100002A5
```
The row's authored background contains the narrow horizontal separator visible
between entries. It is not an invented procedural line. The selection surface
is a separate overlay, so selecting a spell does not replace its icon or label.
```text
AddSpell(spellID):
if spell metadata is missing: return
if IsFilteredOut(spell): return
insertion = first existing row whose displayOrder > spell.displayOrder
list.InsertSpellShortcut(spellID, insertion)
SetSelected(spellID):
for each spell shortcut row:
row.selectedOverlay = (row.spellID == spellID)
if selected: list.ScrollItemIntoView(row)
selectedSpellID = spellID
ListenToElementMessage(message):
if message came from a spell shortcut row:
if message kind is click or double-click: SetSelected(row.spellID)
if message kind is AddSpellShortcut/drag notice:
CM_Magic.SendNotice_AddSpellShortcut(row.spellID)
if message is click on a school/level filter:
UpdateFilter(clickedFilter)
if message is click on 0x100002A5:
DeleteSpell()
UpdateFilter(button):
mask = exact school/level bit assigned to button
if button is currently Highlight: filters |= mask
else: filters &= ~mask
if filters changed:
CM_Character.Event_SpellbookFilterEvent(filters)
rebuild list from authoritative learned spells
scroll list to first row
IsFilteredOut(spell):
school bits: Creature=0x0001, Item=0x0002, Life=0x0004,
War=0x0008, Void=0x2000
level bits: I=0x0010 through VIII=0x0800
return school bit is disabled OR level bit is disabled
DeleteSpell():
if selectedSpellID == 0: return false
name = ClientMagicSystem.GetSpellName(selectedSpellID)
message = "Are you sure you want to remove {name} from your spellbook? "
"You will no longer be able to cast this spell unless you learn it again!"
dialog properties include selectedSpellID under 0x1000003F
DialogFactory.MakeCallbackDialogInCurrentUI(message, callback)
DeleteSpellDialogCallback(dialogProperties):
if confirmation result property 0x92 is true:
spellID = property 0x1000003F
CM_Magic.Event_RemoveSpell(spellID)
CM_Magic.Event_RemoveSpell(spellID):
send GameAction opcode 0x01A8 followed by spellID
RecvNotice_SpellRemoved(spellID):
rebuild from the authoritative player description
```
The client does not optimistically erase the row when Yes is clicked. It waits
for the server's `MagicRemoveSpell` notice. Dragging a row similarly carries a
spell-shortcut identity into the open spellcasting favorite list; it never
removes the spell from the learned-spell set.
## Active enchantment panels
Retail: `gmEffectsUI::PostInit` `0x004B7560` and