diff --git a/docs/plans/2026-04-11-roadmap.md b/docs/plans/2026-04-11-roadmap.md index ad59ae63..50a686f6 100644 --- a/docs/plans/2026-04-11-roadmap.md +++ b/docs/plans/2026-04-11-roadmap.md @@ -532,6 +532,7 @@ behavior. Estimated 17–26 days focused work, 3–5 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 1–9 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. diff --git a/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md b/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md index 9669a5d0..ce0a3aa4 100644 --- a/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md +++ b/docs/research/2026-07-15-retail-magic-ui-and-casting-pseudocode.md @@ -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 diff --git a/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs b/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs index 5775611c..7f85228a 100644 --- a/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs +++ b/src/AcDream.App/UI/Layout/ItemListCellTemplate.cs @@ -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 /// /// 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); } + /// + /// Resolve from an already inherited 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 + /// has applied the + /// same inheritance that retail uses before InternalCreateItem queries + /// attribute 0x1000000E. + /// + 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); + } + /// /// Resolves ItemSlot_Empty directly from an authored UIItem prototype in /// . 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; + } } diff --git a/src/AcDream.App/UI/Layout/SpellcastingUiController.cs b/src/AcDream.App/UI/Layout/SpellcastingUiController.cs index 669f4a73..e19768f4 100644 --- a/src/AcDream.App/UI/Layout/SpellcastingUiController.cs +++ b/src/AcDream.App/UI/Layout/SpellcastingUiController.cs @@ -45,7 +45,8 @@ public sealed class SpellcastingUiController : IRetainedPanelController private readonly Action _useItem; private readonly Action? _addFavorite; private readonly Action? _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? addFavorite, Action? 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 diff --git a/src/AcDream.App/UI/RetailUiRuntime.cs b/src/AcDream.App/UI/RetailUiRuntime.cs index 9d98eadd..c1bf24e8 100644 --- a/src/AcDream.App/UI/RetailUiRuntime.cs +++ b/src/AcDream.App/UI/RetailUiRuntime.cs @@ -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(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( diff --git a/src/AcDream.App/UI/UiShortcutDigitGraphics.cs b/src/AcDream.App/UI/UiShortcutDigitGraphics.cs new file mode 100644 index 00000000..0e8aafbf --- /dev/null +++ b/src/AcDream.App/UI/UiShortcutDigitGraphics.cs @@ -0,0 +1,12 @@ +namespace AcDream.App.UI; + +/// +/// 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. +/// +public sealed record UiShortcutDigitGraphics( + uint[]? RegularDigits, + uint[]? GhostedDigits, + uint[]? EmptyDigits); diff --git a/src/AcDream.App/UI/UiShortcutSlotGraphics.cs b/src/AcDream.App/UI/UiShortcutSlotGraphics.cs deleted file mode 100644 index 7d047e57..00000000 --- a/src/AcDream.App/UI/UiShortcutSlotGraphics.cs +++ /dev/null @@ -1,12 +0,0 @@ -namespace AcDream.App.UI; - -/// -/// 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. -/// -public sealed record UiShortcutSlotGraphics( - uint[]? RegularDigits, - uint[]? GhostedDigits, - uint[]? EmptyDigits, - uint EmptySprite); diff --git a/tests/AcDream.App.Tests/UI/Layout/ItemListCellTemplateTests.cs b/tests/AcDream.App.Tests/UI/Layout/ItemListCellTemplateTests.cs index dc837367..82600811 100644 --- a/tests/AcDream.App.Tests/UI/Layout/ItemListCellTemplateTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/ItemListCellTemplateTests.cs @@ -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( + 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); + } } diff --git a/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs b/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs index 71e4658b..4f78cf54 100644 --- a/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs +++ b/tests/AcDream.App.Tests/UI/Layout/SpellcastingUiControllerTests.cs @@ -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().First(); @@ -46,11 +48,15 @@ public sealed class SpellcastingUiControllerTests { UiCatalogSlot slot = Assert.IsType(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( layout.FindElement(SpellcastingUiController.CastButtonId)); @@ -222,7 +228,8 @@ public sealed class SpellcastingUiControllerTests ClientObjectTable objects, Action useItem, Action? 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)