fix(ui): restore retail spell tabs and spellbook
Bind the spellbook and component-book tabs as their authored stateful text controls, propagate Closed/Open state into the retained DAT child tree, and keep authored label colors live across state changes. Match retail UIElement_Text zero-margin construction so the Magic favorite captions I through VIII are no longer clipped. Add a real portal.dat spellbook fixture plus controller, state-propagation, and text-layout regression coverage. Co-Authored-By: Codex <noreply@openai.com>
This commit is contained in:
parent
07be994d97
commit
ad30c37a48
15 changed files with 13492 additions and 38 deletions
|
|
@ -482,6 +482,50 @@ public class DatWidgetFactoryTests
|
|||
Assert.Equal(gold, t.DefaultColor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildText_RetailConstructorDefaultsToZeroMargins()
|
||||
{
|
||||
var info = new ElementInfo { Type = 12, Width = 100, Height = 20 };
|
||||
var text = Assert.IsType<UiText>(DatWidgetFactory.Create(info, NoTex, null));
|
||||
|
||||
Assert.Equal(0f, text.Padding);
|
||||
Assert.False(text.OneLine);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildText_AuthoredLineTracksStateFontColor()
|
||||
{
|
||||
var info = new ElementInfo
|
||||
{
|
||||
Type = 12,
|
||||
Width = 100,
|
||||
Height = 20,
|
||||
DefaultStateId = RetailUiStateIds.Closed,
|
||||
DefaultStateName = "Closed",
|
||||
};
|
||||
var direct = new UiStateInfo { Id = UiStateInfo.DirectStateId };
|
||||
direct.Properties.Values[0x17u] = new UiPropertyValue
|
||||
{
|
||||
Kind = UiPropertyKind.StringInfo,
|
||||
StringInfoValue = new UiStringInfoValue(0, 1, 2, 0, 1, 0),
|
||||
};
|
||||
var closed = new UiStateInfo { Id = RetailUiStateIds.Closed, Name = "Closed" };
|
||||
closed.Properties.Values[0x1Bu] = Color(127, 127, 127);
|
||||
var open = new UiStateInfo { Id = RetailUiStateIds.Open, Name = "Open" };
|
||||
open.Properties.Values[0x1Bu] = Color(204, 204, 204);
|
||||
info.States[UiStateInfo.DirectStateId] = direct;
|
||||
info.States[RetailUiStateIds.Closed] = closed;
|
||||
info.States[RetailUiStateIds.Open] = open;
|
||||
|
||||
var text = Assert.IsType<UiText>(DatWidgetFactory.Create(
|
||||
info, NoTex, null, stringResolve: _ => "Spells"));
|
||||
Assert.Equal(127f / 255f, text.LinesProvider()[0].Color.X, 5);
|
||||
|
||||
Assert.True(text.TrySetRetailState(RetailUiStateIds.Open));
|
||||
|
||||
Assert.Equal(204f / 255f, text.LinesProvider()[0].Color.X, 5);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void HorizontalScrollbar_PreservesNestedCombatMeterFillSprite()
|
||||
{
|
||||
|
|
@ -560,6 +604,13 @@ public class DatWidgetFactoryTests
|
|||
private static UiPropertyValue Integer(int value)
|
||||
=> new() { Kind = UiPropertyKind.Integer, IntegerValue = value };
|
||||
|
||||
private static UiPropertyValue Color(byte red, byte green, byte blue)
|
||||
=> new()
|
||||
{
|
||||
Kind = UiPropertyKind.Color,
|
||||
ColorValue = new UiColorValue(blue, green, red, 255),
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// When ElementInfo.FontColor is null (dat carried no 0x1B), DefaultColor must
|
||||
/// stay at white (Vector4.One) — the backward-compatible default.
|
||||
|
|
|
|||
|
|
@ -95,6 +95,12 @@ public static class FixtureLoader
|
|||
public static ElementInfo LoadCombatInfos()
|
||||
=> LoadInfos("combat_21000073.json");
|
||||
|
||||
public static ImportedLayout LoadSpellbook()
|
||||
=> LayoutImporter.Build(LoadSpellbookInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
public static ElementInfo LoadSpellbookInfos()
|
||||
=> LoadInfos("spellbook_21000034.json");
|
||||
|
||||
public static ImportedLayout LoadPowerbar()
|
||||
=> LayoutImporter.Build(LoadPowerbarInfos(), _ => (0u, 0, 0), null);
|
||||
|
||||
|
|
|
|||
|
|
@ -71,6 +71,54 @@ public class LayoutImporterTests
|
|||
Assert.NotNull(tree.FindElement(0x20000002));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFromInfos_TextPassToChildren_PropagatesDefaultAndRuntimeState()
|
||||
{
|
||||
var tab = new ElementInfo
|
||||
{
|
||||
Id = 0x100002A9u,
|
||||
Type = 12,
|
||||
Width = 100,
|
||||
Height = 25,
|
||||
DefaultStateId = RetailUiStateIds.Closed,
|
||||
DefaultStateName = "Closed",
|
||||
};
|
||||
tab.States[RetailUiStateIds.Closed] = new UiStateInfo
|
||||
{ Id = RetailUiStateIds.Closed, Name = "Closed", PassToChildren = true };
|
||||
tab.States[RetailUiStateIds.Open] = new UiStateInfo
|
||||
{ Id = RetailUiStateIds.Open, Name = "Open", PassToChildren = true };
|
||||
|
||||
var cap = new ElementInfo { Id = 0x10000439u, Type = 3, Width = 17, Height = 25 };
|
||||
cap.States[RetailUiStateIds.Closed] = new UiStateInfo
|
||||
{
|
||||
Id = RetailUiStateIds.Closed,
|
||||
Name = "Closed",
|
||||
Image = new UiImageMedia(0x06005D93u, 3),
|
||||
};
|
||||
cap.States[RetailUiStateIds.Open] = new UiStateInfo
|
||||
{
|
||||
Id = RetailUiStateIds.Open,
|
||||
Name = "Open",
|
||||
Image = new UiImageMedia(0x06005D92u, 3),
|
||||
};
|
||||
cap.StateMedia["Closed"] = (0x06005D93u, 3);
|
||||
cap.StateMedia["Open"] = (0x06005D92u, 3);
|
||||
tab.Children.Add(cap);
|
||||
|
||||
var root = new ElementInfo { Id = 1u, Type = 3, Width = 100, Height = 25 };
|
||||
ImportedLayout layout = LayoutImporter.BuildFromInfos(root, [tab], NoTex, null);
|
||||
UiText text = Assert.IsType<UiText>(layout.FindElement(tab.Id));
|
||||
UiDatElement child = Assert.IsType<UiDatElement>(layout.FindElement(cap.Id));
|
||||
|
||||
Assert.Single(text.Children);
|
||||
Assert.Equal(RetailUiStateIds.Closed, child.ActiveRetailStateId);
|
||||
Assert.Equal(0x06005D93u, child.ActiveMedia().File);
|
||||
|
||||
Assert.True(text.TrySetRetailState(RetailUiStateIds.Open));
|
||||
Assert.Equal(RetailUiStateIds.Open, child.ActiveRetailStateId);
|
||||
Assert.Equal(0x06005D92u, child.ActiveMedia().File);
|
||||
}
|
||||
|
||||
// ── Test 3: Meter consumes Type-3 slice children — child ids not in byId ────
|
||||
|
||||
/// <summary>
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@ public sealed class RetailLayoutFixtureGenerator
|
|||
(0x21000023u, "inventory_21000023.json"),
|
||||
(0x21000024u, "paperdoll_21000024.json"),
|
||||
(0x2100002Eu, "character_2100002E.json"),
|
||||
(SpellbookWindowController.LayoutId, "spellbook_21000034.json"),
|
||||
(0x2100006Cu, "vitals_2100006C.json"),
|
||||
(EffectsIndicatorController.LayoutId, "indicators_21000071.json"),
|
||||
(0x21000072u, "powerbar_21000072.json"),
|
||||
|
|
@ -116,6 +117,21 @@ public sealed class RetailLayoutFixtureGenerator
|
|||
Assert.Equal("High", labels.High);
|
||||
Assert.Equal("Medium", labels.Medium);
|
||||
Assert.Equal("Low", labels.Low);
|
||||
var strings = new DatStringResolver(dats);
|
||||
uint[] tabIds =
|
||||
{
|
||||
0x100000A3u, 0x100000A4u, 0x100000A5u, 0x100000A6u,
|
||||
0x100000A7u, 0x100000A8u, 0x100000A9u, 0x100005C2u,
|
||||
};
|
||||
string[] expected = ["I", "II", "III", "IV", "V", "VI", "VII", "VIII"];
|
||||
for (int i = 0; i < tabIds.Length; i++)
|
||||
{
|
||||
uint id = tabIds[i];
|
||||
ElementInfo tab = FindInfo(info, id)!;
|
||||
Assert.NotNull(tab);
|
||||
Assert.True(tab.TryGetEffectiveProperty(0x17u, out UiPropertyValue label));
|
||||
Assert.Equal(expected[i], strings.Resolve(label.StringInfoValue));
|
||||
}
|
||||
}
|
||||
var json = JsonSerializer.Serialize(info, new JsonSerializerOptions
|
||||
{
|
||||
|
|
@ -129,4 +145,12 @@ public sealed class RetailLayoutFixtureGenerator
|
|||
|
||||
private static string FixtureDirectory([CallerFilePath] string thisFile = "")
|
||||
=> Path.Combine(Path.GetDirectoryName(thisFile)!, "fixtures");
|
||||
|
||||
private static ElementInfo? FindInfo(ElementInfo root, uint id)
|
||||
{
|
||||
if (root.Id == id) return root;
|
||||
foreach (ElementInfo child in root.Children)
|
||||
if (FindInfo(child, id) is { } match) return match;
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,56 @@
|
|||
using AcDream.App.Spells;
|
||||
using AcDream.App.UI;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Core.Items;
|
||||
using AcDream.Core.Spells;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
||||
public sealed class SpellbookWindowControllerTests
|
||||
{
|
||||
[Fact]
|
||||
public void RetailFixture_BindsTextTabs_AndPropagatesOpenClosedState()
|
||||
{
|
||||
ImportedLayout layout = FixtureLoader.LoadSpellbook();
|
||||
var closed = new List<uint>();
|
||||
using SpellbookWindowController? controller = SpellbookWindowController.Bind(
|
||||
layout,
|
||||
new Spellbook(),
|
||||
new ClientObjectTable(),
|
||||
() => 1u,
|
||||
new Dictionary<uint, SpellComponentDescriptor>(),
|
||||
spellId => spellId,
|
||||
iconId => iconId,
|
||||
_ => 1,
|
||||
_ => { },
|
||||
_ => { },
|
||||
_ => { },
|
||||
(_, _) => { },
|
||||
() => closed.Add(1u));
|
||||
|
||||
Assert.NotNull(controller);
|
||||
UiText spellTab = Assert.IsType<UiText>(layout.FindElement(SpellbookWindowController.SpellTabId));
|
||||
UiText componentTab = Assert.IsType<UiText>(layout.FindElement(SpellbookWindowController.ComponentTabId));
|
||||
UiElement spellPage = Assert.IsAssignableFrom<UiElement>(layout.FindElement(SpellbookWindowController.SpellPageId));
|
||||
UiElement componentPage = Assert.IsAssignableFrom<UiElement>(layout.FindElement(SpellbookWindowController.ComponentPageId));
|
||||
|
||||
Assert.Equal(RetailUiStateIds.Open, spellTab.ActiveRetailStateId);
|
||||
Assert.Equal(RetailUiStateIds.Closed, componentTab.ActiveRetailStateId);
|
||||
Assert.True(spellPage.Visible);
|
||||
Assert.False(componentPage.Visible);
|
||||
Assert.Equal(3, spellTab.Children.Count);
|
||||
Assert.All(spellTab.Children, child =>
|
||||
Assert.Equal(RetailUiStateIds.Open, Assert.IsAssignableFrom<IUiDatStateful>(child).ActiveRetailStateId));
|
||||
|
||||
componentTab.OnEvent(new UiEvent(0, componentTab, UiEventType.Click));
|
||||
|
||||
Assert.Equal(SpellbookWindowPage.Components, controller!.CurrentPage);
|
||||
Assert.Equal(RetailUiStateIds.Closed, spellTab.ActiveRetailStateId);
|
||||
Assert.Equal(RetailUiStateIds.Open, componentTab.ActiveRetailStateId);
|
||||
Assert.False(spellPage.Visible);
|
||||
Assert.True(componentPage.Visible);
|
||||
Assert.All(componentTab.Children, child =>
|
||||
Assert.Equal(RetailUiStateIds.Open, Assert.IsAssignableFrom<IUiDatStateful>(child).ActiveRetailStateId));
|
||||
Assert.Empty(closed);
|
||||
}
|
||||
}
|
||||
13086
tests/AcDream.App.Tests/UI/Layout/fixtures/spellbook_21000034.json
Normal file
13086
tests/AcDream.App.Tests/UI/Layout/fixtures/spellbook_21000034.json
Normal file
File diff suppressed because it is too large
Load diff
Loading…
Add table
Add a link
Reference in a new issue