fix(ui): resolve retail spell slot background

Keep shared shortcut digit overlays separate from the per-ItemList background. Resolve the magic favorite list through its cross-layout inherited cell prototype and use the pinned brown/gold ItemSlot_Empty surface instead of the blue toolbar slot.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-15 19:23:46 +02:00
parent e3605672bb
commit 09612f9981
9 changed files with 128 additions and 53 deletions

View file

@ -532,6 +532,7 @@ behavior. Estimated 1726 days focused work, 35 weeks calendar.
- **✓ M2 local attack receive funnel (live-gated 2026-07-15).** 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 (live-gated 2026-07-15).** 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.
- **M3 cast lifecycle + retained magic UI implemented 2026-07-15; visual gates pending.** `SpellCastingController` ports retail's immediate targeted/untargeted request boundary, component preflight, target eligibility, movement stop, and shared UI-busy lifetime while leaving turn/cast/fizzle/impact outcomes authoritative to ACE. Formula preflight reads decrypted portal.dat formulas, the SCID↔WCID map, canonical account randomization, exact school-focus map, infused-magic properties, and scarab-only substitution. Complete live enchantment packets preserve spell/layer identity, all StatMod fields, client-clock normalization, and exact purge buckets. Authored retained surfaces now include the Magic page of `gmCombatUI`, all eight favorite tabs, equipped caster endowment, `gmSpellbookUI`, `gmSpellComponentUI`, both `gmEffectsUI` windows, and the floating Helpful/Harmful effect indicators. The favorite bar now ports retail horizontal empty-cell padding, shared UIItem 19 shortcut overlays, and the Cast button's three independently reflowed DAT face segments. Spell rows resolve UIItem prototype `0x10000343`; the component page now instantiates LayoutDesc `0x21000033` category/component templates `0x10000466/67`, including localized headings, exact row textures and columns, owned/desired counts, selection, and the 0..5000 desired-value contract. The authored scrollbars, exact filters/display ordering, learned-spell drag, and shared server-authoritative delete dialog are connected. `RetailPanelUiController` ports the shared one-active-panel lifecycle so toolbar and indicator launchers converge on one owner. #L.2/#L.3 close and #L.4/AP-110 narrow. Research: `docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md`.
- **M3 favorite-bar empty-state correction 2026-07-15; visual gate pending.** Shortcut-number arrays remain shared with the status toolbar, but the cell background is now resolved independently from the favorite ItemList's inherited `0x1000000E` cell prototype. LayoutDesc `0x21000010` selects prototype `0x10000341`, whose exact brown/gold `ItemSlot_Empty` surface is `0x06001A97`; the blue toolbar-style `0x060074CF` substitution is removed. The cross-layout resolver and controller presentation are pinned by live-DAT and retained-layout conformance tests.
- **Retail client command families implemented 2026-07-13; corrective live gate pending.** One shared typed catalog now separates retail client actions from ACE administrator commands for both chat backends. Named-decomp ports cover recall/house/PK travel; age/birth; framerate, lock, version, location, corpse, and die confirmation; clear plus named/automatic UI layouts; AFK/consent; emotes; friends; squelch/filter/message types; and fill-components. App owns execution, Core owns authoritative friends/squelch state, Core.Net owns exact UIQueue/ControlQueue packets. Confirmation reuse now ports retail `DialogFactory` contexts, queue groups/priority, fresh DAT roots, property results, callbacks/close notices, and server abort handling; `/die`, gameplay request tuples, and guarded item-use prompts share its type-1 LayoutDesc `0x2100003C` presenter. The first live gate passed the family except for raw suicide-success code `0x004A` and the title-bar-only FPS presentation; the correction maps retail's text and mounts SmartBox element `0x10000047` with live two-decimal `FPS`/`DEG`. #L.6 is closed; TS-31/TS-47 are narrowed. Research: `docs/research/2026-07-13-retail-client-command-families-pseudocode.md`, `docs/research/2026-07-13-retail-dialog-factory-pseudocode.md`.
- **✓ SHIPPED — Character window** (`LayoutDesc 0x2100002E`, `CharacterStatController`, 2026-06-26, same branch). **Visually user-confirmed 2026-06-26 — Attributes tab reads as retail.** Three tabs, header (name/heritage/PK), large-gold level number (dat font, `largeDatFont` 18px), "Total Experience (XP):" + "XP for next level:" captions, 9-row attribute list (icons + right-aligned values + Health/Stamina/Mana vitals), click-to-select (top/bottom selection bars + footer State-B "{Attr}: {value}" / "Experience To Raise: Infinity!" + affordability-gated raise triangles), centered footer. User noted "still needs some polish for later" — deferred to Issue #158.
- **✓ CORRECTIVE PORT — live character experience qualities** (2026-07-13, #217; connected gate passed). Added retail `PrivateUpdatePropertyInt64 (0x02CF)` so Total Experience, the level-progress meter, and Skills' Unassigned Experience refresh from the same authoritative server update; Total XP value alignment now matches the authored right-justified layout.

View file

@ -194,6 +194,14 @@ authored `ItemSlot_Empty` background. The count is based on the reflowed list
width (639 pixels at the 800-pixel design parent, 439 pixels inside the live
600-pixel combat page, yielding 13 whole 32-pixel cells).
The two layers must not be conflated. The favorite ItemList is inherited through
LayoutDesc `0x21000010`; its attribute `0x1000000E` selects UIItem prototype
`0x10000341` in catalog `0x21000037`. That prototype's `m_elem_Icon`
(`0x1000033B`) maps `ItemSlot_Empty` to the brown/gold surface `0x06001A97`.
The blue toolbar-style `0x060074CF` surface is not the unnumbered favorite-cell
background. Resolve the list prototype after cross-layout inheritance, then add
the independent shortcut-number overlay only for indices zero through eight.
The Cast button is likewise fully DAT-authored rather than a synthesized red
rectangle. Element `0x100000B2` is 75 by 32 pixels and its face is three stateful
children authored against a 79-pixel design parent: left `0..31`, middle

View file

@ -21,8 +21,8 @@ public static class ItemListCellTemplate
public const uint CatalogLayoutId = 0x21000037u;
private const uint CellTemplateAttr = 0x1000000eu; // UIElement attribute: cell-template element id
private const uint IconChildId = 0x1000033Bu; // m_elem_Icon sub-element (carries ItemSlot_Empty)
private const string ItemSlotEmpty = "ItemSlot_Empty"; // UIStateId.ToString() for state 0x1000001c
private const uint IconChildId = 0x1000033Bu; // m_elem_Icon sub-element (carries ItemSlot_Empty)
private const string ItemSlotEmpty = "ItemSlot_Empty"; // UIStateId.ToString() for state 0x1000001c
/// <summary>
/// Resolve the empty-slot sprite (a 0x06xxxxxx RenderSurface id) for the cells of item-list
@ -43,6 +43,29 @@ public static class ItemListCellTemplate
return ResolvePrototypeEmptySprite(dats, protoId);
}
/// <summary>
/// Resolve from an already inherited <see cref="ElementInfo"/> tree. Use this
/// overload when the ItemList is contributed by a cross-layout BaseElement:
/// the raw outer LayoutDesc does not physically contain that descendant, but
/// <see cref="LayoutImporter.ImportInfos(DatCollection,uint)"/> has applied the
/// same inheritance that retail uses before <c>InternalCreateItem</c> queries
/// attribute <c>0x1000000E</c>.
/// </summary>
public static uint ResolveEmptySprite(
DatCollection dats,
ElementInfo resolvedRoot,
uint listElementId)
{
ElementInfo? list = FindInfo(resolvedRoot, listElementId);
if (list is null
|| !list.TryGetEffectiveProperty(CellTemplateAttr, out UiPropertyValue property)
|| property.Kind is not (UiPropertyKind.Enum or UiPropertyKind.DataId)
|| property.UnsignedValue is 0 or > uint.MaxValue)
return 0;
return ResolvePrototypeEmptySprite(dats, (uint)property.UnsignedValue);
}
/// <summary>
/// Resolves <c>ItemSlot_Empty</c> directly from an authored UIItem prototype in
/// <see cref="CatalogLayoutId"/>. Paperdoll lists select distinct prototypes for their
@ -168,4 +191,15 @@ public static class ItemListCellTemplate
}
return null;
}
private static ElementInfo? FindInfo(ElementInfo element, uint id)
{
if (element.Id == id) return element;
foreach (ElementInfo child in element.Children)
{
ElementInfo? found = FindInfo(child, id);
if (found is not null) return found;
}
return null;
}
}

View file

@ -45,7 +45,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
private readonly Action<uint> _useItem;
private readonly Action<int, int, uint>? _addFavorite;
private readonly Action<int, uint>? _removeFavorite;
private readonly UiShortcutSlotGraphics? _shortcutGraphics;
private readonly UiShortcutDigitGraphics? _shortcutDigits;
private readonly uint _emptySlotSprite;
private readonly UiElement[] _tabs;
private readonly UiElement[] _groups;
private readonly UiItemList?[] _lists;
@ -79,7 +80,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
UiItemList?[] lists,
UiButton cast,
UiElement endowmentHost,
UiShortcutSlotGraphics? shortcutGraphics)
UiShortcutDigitGraphics? shortcutDigits,
uint emptySlotSprite)
{
_spellbook = spellbook;
_casting = casting;
@ -91,7 +93,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
_useItem = useItem;
_addFavorite = addFavorite;
_removeFavorite = removeFavorite;
_shortcutGraphics = shortcutGraphics;
_shortcutDigits = shortcutDigits;
_emptySlotSprite = emptySlotSprite;
_tabs = tabs;
_groups = groups;
_lists = lists;
@ -143,7 +146,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
SelectionState selection,
Action<int, int, uint>? addFavorite,
Action<int, uint>? removeFavorite,
UiShortcutSlotGraphics? shortcutGraphics = null)
UiShortcutDigitGraphics? shortcutDigits = null,
uint emptySlotSprite = 0u)
{
if (layout.FindElement(CastButtonId) is not UiButton cast
|| layout.FindElement(EndowmentId) is not { } endowmentHost)
@ -166,7 +170,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController
layout, spellbook, casting, objects, playerGuid, resolveSpellIcon,
resolveItemDragIcon, useItem, selection,
addFavorite, removeFavorite,
tabs, groups, lists, cast, endowmentHost, shortcutGraphics);
tabs, groups, lists, cast, endowmentHost,
shortcutDigits, emptySlotSprite);
}
public void AddFavorite(uint spellId)
@ -291,6 +296,7 @@ public sealed class SpellcastingUiController : IRetainedPanelController
list.SingleRow = true;
list.CellWidth = 32f;
list.CellHeight = 32f;
list.CellEmptySprite = _emptySlotSprite;
list.EmptySlotFactory = () => CreateEmptyFavoriteSlot(list, targetTab);
list.FillVisibleEmptySlots = true;
foreach (uint spellId in favorites)
@ -332,9 +338,6 @@ public sealed class SpellcastingUiController : IRetainedPanelController
slot = new UiCatalogSlot
{
SpriteResolve = list.SpriteResolve,
EmptySprite = _shortcutGraphics is { EmptySprite: > 0u } graphics
? graphics.EmptySprite
: 0x060074CFu,
Dropped = payload =>
{
int position = Math.Max(0, list.IndexOf(slot!));
@ -362,11 +365,9 @@ public sealed class SpellcastingUiController : IRetainedPanelController
private void ConfigureShortcutOverlay(UiItemSlot slot, int index)
{
slot.RegularDigits = _shortcutGraphics?.RegularDigits;
slot.GhostedDigits = _shortcutGraphics?.GhostedDigits;
slot.EmptyDigits = _shortcutGraphics?.EmptyDigits;
if (_shortcutGraphics is { EmptySprite: > 0u } graphics)
slot.EmptySprite = graphics.EmptySprite;
slot.RegularDigits = _shortcutDigits?.RegularDigits;
slot.GhostedDigits = _shortcutDigits?.GhostedDigits;
slot.EmptyDigits = _shortcutDigits?.EmptyDigits;
if (index < 9)
slot.SetShortcutNum(index, ghosted: false);
else

View file

@ -167,7 +167,7 @@ public sealed class RetailUiRuntime : IDisposable
private GameplayConfirmationController? _gameplayConfirmationController;
private RetailItemConfirmationController? _itemConfirmationController;
private RetailSkillTrainingConfirmationController? _skillTrainingConfirmationController;
private UiShortcutSlotGraphics? _shortcutSlotGraphics;
private UiShortcutDigitGraphics? _shortcutDigitGraphics;
private bool _disposed;
private RetailUiRuntime(RetailUiRuntimeBindings bindings)
@ -559,12 +559,12 @@ public sealed class RetailUiRuntime : IDisposable
return;
}
UiShortcutSlotGraphics shortcutGraphics = LoadShortcutSlotGraphics();
UiShortcutDigitGraphics shortcutDigits = LoadShortcutDigitGraphics();
ToolbarRuntimeBindings b = _bindings.Toolbar;
ToolbarController = Layout.ToolbarController.Bind(
layout, b.Objects, b.Shortcuts, b.ResolveIcon, b.UseItem, b.Combat,
shortcutGraphics.RegularDigits, shortcutGraphics.GhostedDigits,
shortcutGraphics.EmptyDigits, b.ItemInteraction,
shortcutDigits.RegularDigits, shortcutDigits.GhostedDigits,
shortcutDigits.EmptyDigits, b.ItemInteraction,
b.SendAddShortcut, b.SendRemoveShortcut,
toggleCombat: b.ToggleCombat,
selectItem: guid => b.Selection.Select(guid, SelectionChangeSource.Toolbar),
@ -625,9 +625,16 @@ public sealed class RetailUiRuntime : IDisposable
ElementInfo? info;
ImportedLayout? layout;
CombatUiLabels? labels;
uint favoriteEmptySprite;
lock (_bindings.Assets.DatLock)
{
info = LayoutImporter.ImportInfos(_bindings.Assets.Dats, CombatUiController.LayoutId);
favoriteEmptySprite = info is null
? 0u
: ItemListCellTemplate.ResolveEmptySprite(
_bindings.Assets.Dats,
info,
SpellcastingUiController.FavoriteListId);
var strings = new DatStringResolver(_bindings.Assets.Dats);
layout = info is null ? null : LayoutImporter.Build(
info,
@ -675,7 +682,8 @@ public sealed class RetailUiRuntime : IDisposable
_bindings.Magic.Selection,
_bindings.Magic.AddFavorite,
_bindings.Magic.RemoveFavorite,
LoadShortcutSlotGraphics());
LoadShortcutDigitGraphics(),
favoriteEmptySprite);
if (spellcasting is null)
Console.WriteLine("[M3] spellcasting: required controls missing in LayoutDesc 0x21000073.");
@ -706,7 +714,8 @@ public sealed class RetailUiRuntime : IDisposable
controller.SyncVisibility();
Console.WriteLine(spellcasting is null
? "[M2] retail combat from LayoutDesc 0x21000073; magic binding unavailable."
: "[M3] retail combat + spell bar from LayoutDesc 0x21000073.");
: $"[M3] retail combat + spell bar from LayoutDesc 0x21000073; " +
$"favorite empty=0x{favoriteEmptySprite:X8}.");
}
private void MountSpellbook()
@ -1016,13 +1025,12 @@ public sealed class RetailUiRuntime : IDisposable
$"[UI] retail DialogFactory from LayoutDesc 0x{layoutId:X8}; confirmation root 0x15.");
}
private UiShortcutSlotGraphics LoadShortcutSlotGraphics()
private UiShortcutDigitGraphics LoadShortcutDigitGraphics()
{
if (_shortcutSlotGraphics is not null)
return _shortcutSlotGraphics;
if (_shortcutDigitGraphics is not null)
return _shortcutDigitGraphics;
uint[]? regular = null, ghosted = null, empty = null;
uint emptySprite = 0u;
lock (_bindings.Assets.DatLock)
{
var layout = _bindings.Assets.Dats.Get<DatReaderWriter.DBObjs.LayoutDesc>(0x21000037u);
@ -1039,10 +1047,6 @@ public sealed class RetailUiRuntime : IDisposable
&& emptyComposite.Children.TryGetValue(0x1000034Au, out var emptyNumber)
&& emptyNumber.StateDesc?.Properties is { } emptyProps)
empty = ReadDataIds(emptyProps, 0x1000005Eu);
emptySprite = ItemListCellTemplate.ResolveEmptySprite(
_bindings.Assets.Dats,
CombatUiController.LayoutId,
SpellcastingUiController.FavoriteListId);
}
regular ??=
@ -1055,13 +1059,12 @@ public sealed class RetailUiRuntime : IDisposable
0x06001ACCu, 0x06001ACDu, 0x06001ACEu, 0x06001ACFu, 0x06001AD0u,
0x06001AD1u, 0x06001AD2u, 0x06001AD3u, 0x06001AD4u,
];
_shortcutSlotGraphics = new UiShortcutSlotGraphics(
regular, ghosted, empty, emptySprite);
_shortcutDigitGraphics = new UiShortcutDigitGraphics(
regular, ghosted, empty);
Console.WriteLine(
$"[D.5.1] shared shortcut graphics ready: regular={regular.Length}, " +
$"ghosted={ghosted.Length}, empty={empty?.Length ?? 0}, " +
$"slot=0x{emptySprite:X8}.");
return _shortcutSlotGraphics;
$"[D.5.1] shared shortcut digits ready: regular={regular.Length}, " +
$"ghosted={ghosted.Length}, empty={empty?.Length ?? 0}.");
return _shortcutDigitGraphics;
}
private static uint[]? ReadDataIds(

View file

@ -0,0 +1,12 @@
namespace AcDream.App.UI;
/// <summary>
/// Shared DAT digit overlays used by UIItem shortcut cells. Both gmToolbarUI and
/// gmSpellcastingUI call UIElement_UIItem::SetShortcutNum @ 0x004E1590, so they
/// share these number arrays. The underlying ItemSlot_Empty surface remains a
/// per-ItemList asset selected by that list's cell prototype.
/// </summary>
public sealed record UiShortcutDigitGraphics(
uint[]? RegularDigits,
uint[]? GhostedDigits,
uint[]? EmptyDigits);

View file

@ -1,12 +0,0 @@
namespace AcDream.App.UI;
/// <summary>
/// Shared DAT presentation assets for UIItem shortcut cells. Both gmToolbarUI and
/// gmSpellcastingUI use UIElement_UIItem::SetShortcutNum @ 0x004E1590, so their
/// digit arrays and empty-cell prototype must come from one resolved asset set.
/// </summary>
public sealed record UiShortcutSlotGraphics(
uint[]? RegularDigits,
uint[]? GhostedDigits,
uint[]? EmptyDigits,
uint EmptySprite);

View file

@ -51,8 +51,8 @@ public class ItemListCellTemplateTests
{
uint sprite = ItemListCellTemplate.ResolveEmptySprite(dats, layout, elem);
_out.WriteLine($"{name}: list 0x{elem:X8} (layout 0x{layout:X8}) -> empty sprite 0x{sprite:X8}");
Assert.True(sprite != 0u, $"{name}: resolved 0 (no 0x1000000e attr / prototype / media)");
Assert.True(sprite != Generic, $"{name}: resolved the generic 0x060074CF — the bug, not the fix");
Assert.True(sprite != 0u, $"{name}: resolved 0 (no 0x1000000e attr / prototype / media)");
Assert.True(sprite != Generic, $"{name}: resolved the generic 0x060074CF — the bug, not the fix");
Assert.True((sprite & 0xFF000000u) == 0x06000000u, $"{name}: 0x{sprite:X8} is not a 0x06 RenderSurface id");
}
}
@ -72,4 +72,24 @@ public class ItemListCellTemplateTests
Assert.Equal(0x06000F6Eu, ItemListCellTemplate.ResolveEmptySprite(dats, BackpackLayout, SideBag));
Assert.Equal(0x06000F6Eu, ItemListCellTemplate.ResolveEmptySprite(dats, BackpackLayout, MainPack));
}
[Fact]
public void Spell_favorite_list_resolves_its_cross_layout_pinned_empty_sprite()
{
var datDir = DatDir();
if (datDir is null) return; // CI: no live dat — skip
using var dats = new DatCollection(datDir, DatAccessType.Read);
ElementInfo infos = Assert.IsType<ElementInfo>(
LayoutImporter.ImportInfos(dats, CombatUiController.LayoutId));
uint sprite = ItemListCellTemplate.ResolveEmptySprite(
dats, infos, SpellcastingUiController.FavoriteListId);
_out.WriteLine($"spell favorites -> empty sprite 0x{sprite:X8}");
// gmSpellcastingUI's ItemList is inherited from LayoutDesc 0x21000010.
// Its cell-template attr selects catalog prototype 0x10000341, whose
// m_elem_Icon ItemSlot_Empty state is the brown/gold 0x06001A97 surface.
Assert.Equal(0x06001A97u, sprite);
}
}

View file

@ -25,10 +25,12 @@ public sealed class SpellcastingUiControllerTests
uint[] regular = [101u, 102u, 103u, 104u, 105u, 106u, 107u, 108u, 109u];
uint[] ghosted = [201u, 202u, 203u, 204u, 205u, 206u, 207u, 208u, 209u];
uint[] empty = [301u, 302u, 303u, 304u, 305u, 306u, 307u, 308u, 309u];
var graphics = new UiShortcutSlotGraphics(regular, ghosted, empty, 0x060074CFu);
var digits = new UiShortcutDigitGraphics(regular, ghosted, empty);
using SpellcastingUiController controller = Bind(
layout, spellbook, objects, _ => { }, shortcutGraphics: graphics)!;
layout, spellbook, objects, _ => { },
shortcutDigits: digits,
emptySlotSprite: 0x06001A97u)!;
UiElement group = layout.FindElement(0x100000AAu)!;
UiItemList list = Descendants(group).OfType<UiItemList>().First();
@ -46,11 +48,15 @@ public sealed class SpellcastingUiControllerTests
{
UiCatalogSlot slot = Assert.IsType<UiCatalogSlot>(list.GetItem(i));
Assert.True(slot.IsEmptySlot);
Assert.Equal(0x06001A97u, slot.EmptySprite);
Assert.Equal(i, slot.ShortcutNum);
Assert.Same(empty, slot.ActiveDigitArray());
}
for (int i = 9; i < 13; i++)
{
Assert.Equal(0x06001A97u, list.GetItem(i)!.EmptySprite);
Assert.Equal(-1, list.GetItem(i)!.ShortcutNum);
}
var cast = Assert.IsType<UiButton>(
layout.FindElement(SpellcastingUiController.CastButtonId));
@ -222,7 +228,8 @@ public sealed class SpellcastingUiControllerTests
ClientObjectTable objects,
Action<uint> useItem,
Action<int, int, uint>? addFavorite = null,
UiShortcutSlotGraphics? shortcutGraphics = null)
UiShortcutDigitGraphics? shortcutDigits = null,
uint emptySlotSprite = 0u)
{
var casting = new SpellCastingController(
spellbook, () => null, () => 1u, () => { }, _ => { }, (_, _) => { }, _ => { });
@ -234,7 +241,8 @@ public sealed class SpellcastingUiControllerTests
new SelectionState(),
addFavorite ?? ((_, _, _) => { }),
(_, _) => { },
shortcutGraphics);
shortcutDigits,
emptySlotSprite);
}
private static void ApplyAnchors(UiElement parent)