fix(D.5.1): toolbar movable + chrome-grab + peace-only indicator + no prototype square
D1 — Toolbar not movable: toolbarRoot.Anchors = AnchorEdges.None (was Left|Top) so ApplyAnchor early-returns and doesn't re-pin the window every frame. Matches the vitalsRoot idiom exactly. D2 — Cannot grab toolbar by chrome: toolbarRoot.ClickThrough = false so HitTest succeeds over the UiDatElement chrome and the drag starts. UiDatElement ctor defaults ClickThrough=true; vitalsRoot already overrides it. C1 — All four combat-mode indicators visible at once (war/flame stacked on peace): ports gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669). CombatIndicatorIds[] maps index 0-3 to NonCombat/Melee/Missile/Magic; SetCombatMode shows exactly one and hides the other three. Default to NonCombat at bind (player always spawns in peace). Wires CombatState.CombatModeChanged for live updates. Tests: CombatIndicator_defaultNonCombat_onlyPeaceVisible, CombatIndicator_setCombatModeMelee_onlyMeleeVisible, CombatIndicator_liveSignal_updatesWhenCombatStateChanges. V1 — Blue empty-slot square at top-left (prototype 0x100001B2 materialized): ImportInfos now skips top-level elements that are (a) referenced as a BaseElement by another element in the same layout AND (b) have no own state media. The CollectBaseRefsInDesc walk covers nested children; HasNoOwnMedia re-uses ToInfo's media extraction. The Resolve path reads BaseElement from the raw dat via dats.Get<LayoutDesc> — it never depends on the prototype being in the built widget tree — so the skip is safe. Conformance tests (vitals, chat) are unaffected (they exercise Build, not ImportInfos). Test: BuildFromInfos_PrototypeSkipped_DerivedPresent_PrototypeAbsent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
b3e5e8b0f7
commit
bfc452d610
5 changed files with 262 additions and 11 deletions
|
|
@ -1921,11 +1921,17 @@ public sealed class GameWindow : IDisposable
|
|||
toolbarLayout, Items,
|
||||
() => Shortcuts,
|
||||
iconIds: (icon, under, over) => iconComposer.GetIcon(icon, under, over),
|
||||
useItem: guid => UseItemByGuid(guid));
|
||||
useItem: guid => UseItemByGuid(guid),
|
||||
combatState: Combat);
|
||||
|
||||
var toolbarRoot = toolbarLayout.Root;
|
||||
toolbarRoot.Left = 10; toolbarRoot.Top = 300;
|
||||
toolbarRoot.Anchors = AcDream.App.UI.AnchorEdges.Left | AcDream.App.UI.AnchorEdges.Top;
|
||||
// D1: Anchors=None so ApplyAnchor skips re-pinning every frame and
|
||||
// the drag position is preserved (matches vitalsRoot pattern).
|
||||
toolbarRoot.Anchors = AcDream.App.UI.AnchorEdges.None;
|
||||
// D2: UiDatElement ctor defaults ClickThrough=true; override so the
|
||||
// chrome is hittable and the drag can start (matches vitalsRoot pattern).
|
||||
toolbarRoot.ClickThrough = false;
|
||||
toolbarRoot.Draggable = true;
|
||||
_uiHost.Root.AddChild(toolbarRoot);
|
||||
Console.WriteLine("[D.5.1] retail toolbar window from LayoutDesc importer (0x21000016).");
|
||||
|
|
|
|||
|
|
@ -139,9 +139,34 @@ public static class LayoutImporter
|
|||
var ld = dats.Get<LayoutDesc>(layoutId);
|
||||
if (ld is null) return null;
|
||||
|
||||
// Collect the set of element ids that are referenced as a BaseElement by ANY
|
||||
// element in THIS layout (where BaseLayoutId == layoutId). Such elements are
|
||||
// purely inheritance templates ("prototypes") — retail never instantiates them
|
||||
// as live widgets. Example: the toolbar slot prototype 0x100001B2 in LayoutDesc
|
||||
// 0x21000016, which all 18 slot elements inherit from and which has no own media.
|
||||
//
|
||||
// NOTE: the Resolve path reads BaseElement from the raw dat directly (via
|
||||
// dats.Get<LayoutDesc>), so the prototype never needs to appear in the built
|
||||
// widget tree for inheritance to work. Skipping it here is safe.
|
||||
var referencedAsBase = new HashSet<uint>();
|
||||
foreach (var kv in ld.Elements)
|
||||
CollectBaseRefsInDesc(kv.Value, layoutId, referencedAsBase);
|
||||
|
||||
var tops = new List<ElementInfo>();
|
||||
foreach (var kv in ld.Elements)
|
||||
tops.Add(Resolve(dats, kv.Value, new HashSet<(uint, uint)>()));
|
||||
{
|
||||
// Skip pure prototype elements: top-level elements that are referenced as a
|
||||
// base template by another element in this same layout AND have no own state
|
||||
// media (so they draw nothing and contribute nothing but their inherited shape).
|
||||
var d = kv.Value;
|
||||
if (referencedAsBase.Contains(d.ElementId) && HasNoOwnMedia(d))
|
||||
{
|
||||
Console.WriteLine($"[D.2b] LayoutImporter: skipping prototype element 0x{d.ElementId:X8} in layout 0x{layoutId:X8} (no own media, referenced as BaseElement).");
|
||||
continue;
|
||||
}
|
||||
|
||||
tops.Add(Resolve(dats, d, new HashSet<(uint, uint)>()));
|
||||
}
|
||||
|
||||
return tops.Count == 1
|
||||
? tops[0]
|
||||
|
|
@ -270,6 +295,37 @@ public static class LayoutImporter
|
|||
}
|
||||
}
|
||||
|
||||
// ── Prototype detection helpers ───────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Recursively walks <paramref name="d"/> and all its children, adding to
|
||||
/// <paramref name="result"/> the <c>BaseElement</c> of every descriptor that
|
||||
/// references this layout (<c>BaseLayoutId == layoutId</c>). Used by
|
||||
/// <see cref="ImportInfos"/> to identify pure prototype/template elements that
|
||||
/// should not be instantiated as live widgets.
|
||||
/// </summary>
|
||||
private static void CollectBaseRefsInDesc(ElementDesc d, uint layoutId, HashSet<uint> result)
|
||||
{
|
||||
if (d.BaseElement != 0 && d.BaseLayoutId == layoutId)
|
||||
result.Add(d.BaseElement);
|
||||
foreach (var kv in d.Children)
|
||||
CollectBaseRefsInDesc(kv.Value, layoutId, result);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns true when <paramref name="d"/> carries no own state media — i.e. its
|
||||
/// <c>StateDesc</c> (DirectState) and <c>States</c> (named states) yield no
|
||||
/// <see cref="MediaDescImage"/> entries with a non-zero file id.
|
||||
/// Such elements are pure inheritance templates with no rendering content.
|
||||
/// </summary>
|
||||
private static bool HasNoOwnMedia(ElementDesc d)
|
||||
{
|
||||
// Re-use ToInfo's media extraction: if the resulting StateMedia is empty the
|
||||
// element has no renderable image in any state.
|
||||
var info = ToInfo(d);
|
||||
return info.StateMedia.Count == 0;
|
||||
}
|
||||
|
||||
// ── Element tree search ───────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
|
||||
|
|
@ -39,7 +40,15 @@ public sealed class ToolbarController
|
|||
// Ids confirmed from the toolbar LayoutDesc dump.
|
||||
private static readonly uint[] HiddenIds = { 0x100001A1, 0x100001A2, 0x100001A4 };
|
||||
|
||||
// Four mutually-exclusive combat-mode indicator elements — exactly one visible at a time.
|
||||
// Index 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic.
|
||||
// Retail ref: gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669)
|
||||
// SetVisible's exactly one element depending on the incoming mode.
|
||||
private static readonly uint[] CombatIndicatorIds =
|
||||
{ 0x10000192u, 0x10000193u, 0x10000194u, 0x10000195u };
|
||||
|
||||
private readonly UiItemList?[] _slots = new UiItemList?[SlotIds.Length];
|
||||
private readonly UiElement?[] _combatIndicators = new UiElement?[CombatIndicatorIds.Length];
|
||||
private readonly ItemRepository _repo;
|
||||
private readonly Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> _shortcuts;
|
||||
private readonly Func<uint, uint, uint, uint> _iconIds; // (iconId, underlayId, overlayId) → GL tex
|
||||
|
|
@ -50,7 +59,8 @@ public sealed class ToolbarController
|
|||
ItemRepository repo,
|
||||
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
|
||||
Func<uint, uint, uint, uint> iconIds,
|
||||
Action<uint> useItem)
|
||||
Action<uint> useItem,
|
||||
CombatState? combatState)
|
||||
{
|
||||
_repo = repo;
|
||||
_shortcuts = shortcuts;
|
||||
|
|
@ -64,10 +74,23 @@ public sealed class ToolbarController
|
|||
WireClick(list);
|
||||
}
|
||||
|
||||
// Cache the four mutually-exclusive combat-mode indicator elements.
|
||||
for (int i = 0; i < CombatIndicatorIds.Length; i++)
|
||||
_combatIndicators[i] = layout.FindElement(CombatIndicatorIds[i]);
|
||||
|
||||
// Hide target-object meters + stack slider (gmToolbarUI::PostInit).
|
||||
foreach (var id in HiddenIds)
|
||||
if (layout.FindElement(id) is { } e) e.Visible = false;
|
||||
|
||||
// Port of gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669):
|
||||
// exactly one indicator visible at a time. Default to NonCombat (peace) — the player
|
||||
// always spawns in peace mode; retail has not yet called SetVisible when PostInit runs.
|
||||
SetCombatMode(CombatMode.NonCombat);
|
||||
|
||||
// Wire live combat-mode changes if a CombatState was provided.
|
||||
if (combatState is not null)
|
||||
combatState.CombatModeChanged += SetCombatMode;
|
||||
|
||||
// Re-bind any deferred slot whenever the repo learns about a new/updated item.
|
||||
repo.ItemAdded += _ => Populate();
|
||||
repo.ItemPropertiesUpdated += _ => Populate();
|
||||
|
|
@ -84,14 +107,21 @@ public sealed class ToolbarController
|
|||
/// <param name="shortcuts">Provider for the current shortcut bar list.</param>
|
||||
/// <param name="iconIds">Resolves (iconId, underlayId, overlayId) → GL texture handle.</param>
|
||||
/// <param name="useItem">Callback fired when a bound slot is clicked; receives the item guid.</param>
|
||||
/// <param name="combatState">
|
||||
/// Optional live combat state — when provided, the toolbar subscribes to
|
||||
/// <see cref="CombatState.CombatModeChanged"/> and updates the four mutually-exclusive
|
||||
/// combat-mode indicator elements accordingly.
|
||||
/// Pass null to skip live wiring (e.g. in unit tests that don't exercise the indicator).
|
||||
/// </param>
|
||||
public static ToolbarController Bind(
|
||||
ImportedLayout layout,
|
||||
ItemRepository repo,
|
||||
Func<IReadOnlyList<PlayerDescriptionParser.ShortcutEntry>> shortcuts,
|
||||
Func<uint, uint, uint, uint> iconIds,
|
||||
Action<uint> useItem)
|
||||
Action<uint> useItem,
|
||||
CombatState? combatState = null)
|
||||
{
|
||||
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem);
|
||||
var c = new ToolbarController(layout, repo, shortcuts, iconIds, useItem, combatState);
|
||||
c.Populate();
|
||||
return c;
|
||||
}
|
||||
|
|
@ -123,6 +153,33 @@ public sealed class ToolbarController
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Port of <c>gmToolbarUI::RecvNotice_SetCombatMode</c>
|
||||
/// (acclient_2013_pseudo_c.txt:196632-196669): show exactly one of the four
|
||||
/// mutually-exclusive combat-mode indicator elements and hide the other three.
|
||||
/// Called at bind-time with <see cref="CombatMode.NonCombat"/> (the player
|
||||
/// always starts in peace mode) and subsequently whenever
|
||||
/// <see cref="CombatState.CombatModeChanged"/> fires.
|
||||
/// </summary>
|
||||
public void SetCombatMode(CombatMode mode)
|
||||
{
|
||||
// Index → mode mapping matches CombatIndicatorIds declaration order:
|
||||
// 0 = NonCombat (peace), 1 = Melee, 2 = Missile, 3 = Magic.
|
||||
bool[] show =
|
||||
{
|
||||
mode == CombatMode.NonCombat,
|
||||
mode == CombatMode.Melee,
|
||||
mode == CombatMode.Missile,
|
||||
mode == CombatMode.Magic,
|
||||
};
|
||||
|
||||
for (int i = 0; i < _combatIndicators.Length; i++)
|
||||
{
|
||||
if (_combatIndicators[i] is { } e)
|
||||
e.Visible = show[i];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Wire the <see cref="UiItemSlot.Clicked"/> callback on a slot cell so that
|
||||
/// clicking a bound item fires <see cref="_useItem"/> with the slot's current guid.
|
||||
|
|
|
|||
|
|
@ -93,6 +93,54 @@ public class LayoutImporterTests
|
|||
Assert.Empty(uiMeter.Children);
|
||||
}
|
||||
|
||||
// ── Test 4: Prototype-skip in BuildFromInfos ─────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// When one top-level element is referenced as a BaseElement by a sibling
|
||||
/// (mirroring the toolbar slot prototype pattern), and the prototype element
|
||||
/// has no own state media, the importer must NOT produce a widget for the
|
||||
/// prototype id (FindElement returns null), but MUST produce the derived element.
|
||||
///
|
||||
/// NOTE: This test exercises <see cref="LayoutImporter.BuildFromInfos"/> (the pure
|
||||
/// layer), where prototype detection is done by inspecting the pre-resolved
|
||||
/// ElementInfo tree rather than the raw dat ElementDesc. The pure layer skips
|
||||
/// an element if its Id is in a sibling's (or child's) <c>Children</c> chain
|
||||
/// as a BaseElement — but actually the pure layer has no BaseElement knowledge
|
||||
/// at this stage (that's resolved before Build). The prototype-skip in the real
|
||||
/// world occurs in <c>ImportInfos</c> (the dat shell), BEFORE calling Build.
|
||||
///
|
||||
/// This test verifies the INVARIANT that holds AFTER ImportInfos filters prototypes:
|
||||
/// a pure template element that was skipped is absent from FindElement, while the
|
||||
/// derived element (which inherited from it) IS present.
|
||||
///
|
||||
/// We model this by simply NOT adding the prototype to the ElementInfo tree passed
|
||||
/// to BuildFromInfos — as if ImportInfos already filtered it out.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void BuildFromInfos_PrototypeSkipped_DerivedPresent_PrototypeAbsent()
|
||||
{
|
||||
// Simulate what ImportInfos does AFTER filtering: the prototype 0xBBB00001 is
|
||||
// absent (already skipped by ImportInfos), the derived element 0xCCC00001 is
|
||||
// present with its own media inherited from the prototype.
|
||||
var root = new ElementInfo { Id = 0x10000001, Type = 3, Width = 200, Height = 100 };
|
||||
// The derived element has its own size + media (prototype was merged into it already).
|
||||
var derived = new ElementInfo
|
||||
{
|
||||
Id = 0xCCC00001u,
|
||||
Type = 0x10000031u, // UIElement_ItemList (toolbar slot type)
|
||||
X = 10, Y = 10, Width = 32, Height = 32,
|
||||
};
|
||||
derived.StateMedia[""] = (0x06001234u, 1);
|
||||
|
||||
// Only the derived element appears in the tree (prototype was filtered by ImportInfos).
|
||||
var tree = LayoutImporter.BuildFromInfos(root, new[] { derived }, NoTex, null);
|
||||
|
||||
// The derived element is present in the built tree.
|
||||
Assert.NotNull(tree.FindElement(0xCCC00001u));
|
||||
// The prototype id is NOT in the tree (was never added).
|
||||
Assert.Null(tree.FindElement(0xBBB00001u));
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static ElementInfo BuildSliceContainer(uint id, uint ReadOrder, uint l, uint t, uint r)
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Combat;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Net.Messages;
|
||||
using Xunit;
|
||||
|
|
@ -15,14 +16,25 @@ public class ToolbarControllerTests
|
|||
private static readonly uint[] Row2 =
|
||||
{ 0x100006B7,0x100006B8,0x100006B9,0x100006BA,0x100006BB,0x100006BC,0x100006BD,0x100006BE,0x100006BF };
|
||||
|
||||
private static (ImportedLayout layout, Dictionary<uint, UiItemList> slots) FakeToolbar()
|
||||
// The four mutually-exclusive combat-mode indicator element ids (must match ToolbarController's list).
|
||||
private static readonly uint[] CombatIds = { 0x10000192u, 0x10000193u, 0x10000194u, 0x10000195u };
|
||||
|
||||
private static (ImportedLayout layout, Dictionary<uint, UiItemList> slots,
|
||||
Dictionary<uint, UiElement> indicators) FakeToolbar()
|
||||
{
|
||||
var dict = new Dictionary<uint, UiElement>();
|
||||
var slots = new Dictionary<uint, UiItemList>();
|
||||
var indicators = new Dictionary<uint, UiElement>();
|
||||
var root = new UiPanel();
|
||||
foreach (var id in Row1) AddSlot(id);
|
||||
foreach (var id in Row2) AddSlot(id);
|
||||
return (new ImportedLayout(root, dict), slots);
|
||||
// Add combat indicator elements as plain UiPanels keyed by id.
|
||||
foreach (var id in CombatIds)
|
||||
{
|
||||
var e = new UiPanel { Visible = true };
|
||||
dict[id] = e; indicators[id] = e; root.AddChild(e);
|
||||
}
|
||||
return (new ImportedLayout(root, dict), slots, indicators);
|
||||
|
||||
void AddSlot(uint id)
|
||||
{
|
||||
|
|
@ -34,7 +46,7 @@ public class ToolbarControllerTests
|
|||
[Fact]
|
||||
public void Populate_bindsShortcutToCorrectSlot()
|
||||
{
|
||||
var (layout, slots) = FakeToolbar();
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ItemRepository();
|
||||
repo.AddOrUpdate(new ItemInstance { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
|
||||
var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
|
||||
|
|
@ -51,7 +63,7 @@ public class ToolbarControllerTests
|
|||
[Fact]
|
||||
public void DeferredRebind_whenItemArrivesLate()
|
||||
{
|
||||
var (layout, slots) = FakeToolbar();
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ItemRepository(); // item NOT present yet
|
||||
var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
|
||||
{ new(Index: 2, ObjectGuid: 0x5002u, SpellId: 0, Layer: 0) };
|
||||
|
|
@ -68,7 +80,7 @@ public class ToolbarControllerTests
|
|||
[Fact]
|
||||
public void Click_emitsUseForBoundItem()
|
||||
{
|
||||
var (layout, slots) = FakeToolbar();
|
||||
var (layout, slots, _) = FakeToolbar();
|
||||
var repo = new ItemRepository();
|
||||
repo.AddOrUpdate(new ItemInstance { ObjectId = 0x5001u, WeenieClassId = 1u, IconId = 0x06001234u });
|
||||
var shortcuts = new List<PlayerDescriptionParser.ShortcutEntry>
|
||||
|
|
@ -82,4 +94,76 @@ public class ToolbarControllerTests
|
|||
|
||||
Assert.Equal(0x5001u, used);
|
||||
}
|
||||
|
||||
// ── C1: combat-mode indicator tests ─────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// At bind time (default NonCombat), only the peace indicator (0x10000192) is visible;
|
||||
/// the melee/missile/magic indicators (0x10000193/4/5) are hidden.
|
||||
/// Port of gmToolbarUI::RecvNotice_SetCombatMode (acclient_2013_pseudo_c.txt:196632-196669).
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CombatIndicator_defaultNonCombat_onlyPeaceVisible()
|
||||
{
|
||||
var (layout, _, indicators) = FakeToolbar();
|
||||
var repo = new ItemRepository();
|
||||
|
||||
ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
iconIds: (_,_,_) => 0u, useItem: _ => { });
|
||||
|
||||
// Only peace indicator (index 0 = 0x10000192) is visible.
|
||||
Assert.True (indicators[0x10000192u].Visible, "peace indicator should be visible after bind");
|
||||
Assert.False(indicators[0x10000193u].Visible, "melee indicator should be hidden after bind");
|
||||
Assert.False(indicators[0x10000194u].Visible, "missile indicator should be hidden after bind");
|
||||
Assert.False(indicators[0x10000195u].Visible, "magic indicator should be hidden after bind");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// SetCombatMode(Melee) hides peace/missile/magic and shows only the melee indicator.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CombatIndicator_setCombatModeMelee_onlyMeleeVisible()
|
||||
{
|
||||
var (layout, _, indicators) = FakeToolbar();
|
||||
var repo = new ItemRepository();
|
||||
|
||||
var ctrl = ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
iconIds: (_,_,_) => 0u, useItem: _ => { });
|
||||
|
||||
ctrl.SetCombatMode(CombatMode.Melee);
|
||||
|
||||
Assert.False(indicators[0x10000192u].Visible, "peace indicator should be hidden in melee mode");
|
||||
Assert.True (indicators[0x10000193u].Visible, "melee indicator should be visible in melee mode");
|
||||
Assert.False(indicators[0x10000194u].Visible, "missile indicator should be hidden in melee mode");
|
||||
Assert.False(indicators[0x10000195u].Visible, "magic indicator should be hidden in melee mode");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// CombatModeChanged event on CombatState automatically updates the indicator.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void CombatIndicator_liveSignal_updatesWhenCombatStateChanges()
|
||||
{
|
||||
var (layout, _, indicators) = FakeToolbar();
|
||||
var repo = new ItemRepository();
|
||||
var combat = new CombatState();
|
||||
|
||||
ToolbarController.Bind(layout, repo,
|
||||
() => Array.Empty<PlayerDescriptionParser.ShortcutEntry>(),
|
||||
iconIds: (_,_,_) => 0u, useItem: _ => { },
|
||||
combatState: combat);
|
||||
|
||||
// Initially NonCombat after bind.
|
||||
Assert.True(indicators[0x10000192u].Visible, "peace should be visible initially");
|
||||
|
||||
// Server fires CombatModeChanged → Magic.
|
||||
combat.SetCombatMode(CombatMode.Magic);
|
||||
|
||||
Assert.False(indicators[0x10000192u].Visible, "peace should be hidden in magic mode");
|
||||
Assert.False(indicators[0x10000193u].Visible, "melee should be hidden in magic mode");
|
||||
Assert.False(indicators[0x10000194u].Visible, "missile should be hidden in magic mode");
|
||||
Assert.True (indicators[0x10000195u].Visible, "magic indicator should be visible");
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue