feat(ui): port retail component book rows

Instantiate gmSpellComponentUI category and component entries from LayoutDesc 0x21000033 instead of synthesizing catalog rows. Preserve localized category titles, authored textures and geometry, live icon/count/desired bindings, selected-object synchronization, and retail's inclusive 0..5000 desired-level contract.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-15 17:38:24 +02:00
parent 15bb61e05d
commit a1175e6aac
13 changed files with 2395 additions and 60 deletions

View file

@ -2,9 +2,9 @@ using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Numerics;
using AcDream.App.Spells;
using AcDream.Core.Items;
using AcDream.Core.Selection;
using AcDream.Core.Spells;
namespace AcDream.App.UI.Layout;
@ -48,6 +48,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
private readonly ClientObjectTable _objects;
private readonly Func<uint> _playerGuid;
private readonly IReadOnlyDictionary<uint, SpellComponentDescriptor> _components;
private readonly SelectionState _selection;
private readonly Func<uint, uint> _resolveSpellIcon;
private readonly Func<uint, uint> _resolveComponentIcon;
private readonly Func<uint, int> _spellLevel;
@ -66,10 +67,12 @@ public sealed class SpellbookWindowController : IRetainedPanelController
private readonly UiButton _deleteButton;
private readonly UiItemList _spellList;
private readonly UiItemList _componentList;
private readonly ComponentBookTemplateFactory _componentTemplates;
private readonly List<(UiButton Button, uint Mask)> _filters = new();
private readonly SpellbookRowStyle _rowStyle;
private readonly UiDatFont? _rowFont;
private uint? _selectedSpell;
private uint? _selectedComponent;
private bool _spellsDirty;
private bool _componentsDirty;
private bool _componentEditActive;
@ -81,6 +84,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
ClientObjectTable objects,
Func<uint> playerGuid,
IReadOnlyDictionary<uint, SpellComponentDescriptor> components,
SelectionState selection,
Func<uint, uint> resolveSpellIcon,
Func<uint, uint> resolveComponentIcon,
Func<uint, int> spellLevel,
@ -99,6 +103,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
UiButton deleteButton,
UiItemList spellList,
UiItemList componentList,
ComponentBookTemplateFactory componentTemplates,
SpellbookRowStyle rowStyle,
UiDatFont? rowFont)
{
@ -106,6 +111,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
_objects = objects;
_playerGuid = playerGuid;
_components = components;
_selection = selection;
_resolveSpellIcon = resolveSpellIcon;
_resolveComponentIcon = resolveComponentIcon;
_spellLevel = spellLevel;
@ -124,6 +130,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
_deleteButton = deleteButton;
_spellList = spellList;
_componentList = componentList;
_componentTemplates = componentTemplates;
_rowStyle = rowStyle;
_rowFont = rowFont;
@ -148,6 +155,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
_objects.ObjectRemoved += OnObjectRemoved;
_objects.ObjectMoved += OnObjectMoved;
_objects.ContainerContentsReplaced += OnContainerContentsReplaced;
_selection.Changed += OnSelectionChanged;
ShowPage(SpellbookWindowPage.Spells);
RebuildAll();
}
@ -160,6 +168,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
ClientObjectTable objects,
Func<uint> playerGuid,
IReadOnlyDictionary<uint, SpellComponentDescriptor> components,
SelectionState selection,
Func<uint, uint> resolveSpellIcon,
Func<uint, uint> resolveComponentIcon,
Func<uint, int> spellLevel,
@ -170,6 +179,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
Action<string, Action<bool>> showConfirmation,
Action<uint, uint> setDesiredComponent,
Action close,
ComponentBookTemplateFactory componentTemplates,
SpellbookRowStyle rowStyle,
UiDatFont? rowFont)
{
@ -199,13 +209,14 @@ public sealed class SpellbookWindowController : IRetainedPanelController
}
return new SpellbookWindowController(
layout, spellbook, objects, playerGuid, components,
layout, spellbook, objects, playerGuid, components, selection,
resolveSpellIcon, resolveComponentIcon,
spellLevel, selectObject,
addFavorite, sendFilter, removeSpell, showConfirmation,
setDesiredComponent, close,
spellPage, componentPage, spellTab, componentTab, closeButton,
deleteButton, spellList, componentList, rowStyle, rowFont);
deleteButton, spellList, componentList, componentTemplates,
rowStyle, rowFont);
}
public void ShowPage(SpellbookWindowPage page)
@ -337,40 +348,29 @@ public sealed class SpellbookWindowController : IRetainedPanelController
if (category != nextCategory)
{
category = nextCategory;
_componentList.AddItem(new UiCatalogSlot
UiTemplateListSlot categoryRow =
_componentTemplates.CreateCategoryRow(nextCategory);
categoryRow.Clicked = () =>
{
EntryId = uint.MaxValue,
Label = ComponentCategoryName(nextCategory),
ShowLabel = true,
SpriteResolve = _componentList.SpriteResolve,
});
_selectedComponent = null;
SyncComponentSelection();
_selectObject(0u);
};
_componentList.AddItem(categoryRow);
}
uint desired = _spellbook.DesiredComponents.TryGetValue(componentId, out uint amount) ? amount : 0u;
var row = new UiCatalogSlot
{
EntryId = componentId,
CatalogIconTexture = _resolveComponentIcon(
pack.IconId != 0 ? pack.IconId : descriptor?.IconId ?? 0u),
Label = string.IsNullOrWhiteSpace(pack.Name) ? descriptor?.Name ?? $"Component {componentId}" : pack.Name,
Detail = packGroup.Quantity.ToString(CultureInfo.InvariantCulture),
ShowLabel = true,
SpriteResolve = _componentList.SpriteResolve,
};
var desiredField = new UiField
{
Left = width - 47f,
Top = 4f,
Width = 43f,
Height = 24f,
MaxCharacters = 4,
ClearOnSubmit = false,
RecordHistory = false,
SelectAllOnFocus = true,
RightAligned = true,
CharacterFilter = char.IsAsciiDigit,
BackgroundColor = new Vector4(0f, 0f, 0f, 0.65f),
};
desiredField.SetText(desired.ToString(CultureInfo.InvariantCulture));
string componentName = string.IsNullOrWhiteSpace(pack.Name)
? descriptor?.Name ?? $"Component {componentId}"
: pack.Name;
ComponentBookTemplateFactory.ComponentRow row =
_componentTemplates.CreateComponentRow(
componentId,
_resolveComponentIcon(
pack.IconId != 0 ? pack.IconId : descriptor?.IconId ?? 0u),
componentName,
packGroup.Quantity,
desired);
UiField desiredField = row.DesiredField;
desiredField.OnFocusGained = () => _componentEditActive = true;
uint committed = desired;
void Commit(string value)
@ -392,25 +392,18 @@ public sealed class SpellbookWindowController : IRetainedPanelController
Commit(value);
_componentEditActive = false;
};
row.AddChild(desiredField);
row.Clicked = () => _selectObject(pack.ObjectId);
_componentList.AddItem(row);
row.Slot.Clicked = () =>
{
_selectedComponent = componentId;
SyncComponentSelection();
_selectObject(pack.ObjectId);
};
_componentList.AddItem(row.Slot);
}
}
SyncComponentSelectionFromWorld();
}
private static string ComponentCategoryName(uint category) => category switch
{
0u => "Scarabs",
1u => "Herbs",
2u => "Powdered Gems",
3u => "Alchemical Substances",
4u => "Talismans",
5u => "Tapers",
6u => "Prismatic Components",
_ => "Other Components",
};
private bool IsOwnedByPlayer(ClientObject item, uint playerGuid)
{
if (playerGuid == 0) return item.ContainerId != 0 || item.WielderId != 0;
@ -479,6 +472,23 @@ public sealed class SpellbookWindowController : IRetainedPanelController
slot.Selected = slot.EntryId == _selectedSpell;
}
private void SyncComponentSelection()
{
for (int i = 0; i < _componentList.GetNumUIItems(); i++)
if (_componentList.GetItem(i) is UiTemplateListSlot slot)
slot.SetSelected(slot.EntryId == _selectedComponent);
}
private void SyncComponentSelectionFromWorld()
{
_selectedComponent = _selection.SelectedObjectId is uint selected
&& _objects.Get(selected) is { } item
&& IsComponent(item)
? item.WeenieClassId
: null;
SyncComponentSelection();
}
public void Tick()
{
if (_spellsDirty)
@ -505,6 +515,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
}
private void OnObjectMoved(ClientObjectMove _) => _componentsDirty = true;
private void OnContainerContentsReplaced(uint _) => _componentsDirty = true;
private void OnSelectionChanged(SelectionTransition _) => SyncComponentSelectionFromWorld();
private bool IsComponent(ClientObject item)
=> item.IsComponentPack || _components.ContainsKey(item.WeenieClassId);
public void OnShown() => RebuildAll();
@ -520,6 +531,7 @@ public sealed class SpellbookWindowController : IRetainedPanelController
_objects.ObjectRemoved -= OnObjectRemoved;
_objects.ObjectMoved -= OnObjectMoved;
_objects.ContainerContentsReplaced -= OnContainerContentsReplaced;
_selection.Changed -= OnSelectionChanged;
RetailTabBinding.SetClick(_spellTab, null);
RetailTabBinding.SetClick(_componentTab, null);
_closeButton.OnClick = null;