- Rename UiChatInput → UiField (UIElement_Field, RegisterElementClass(3) @ :126190); update doc to cite retail's CatchDroppedItem/MouseOverTop drag-drop hooks for future item windows. BackgroundColor default → transparent (controller sets the translucent 0.35α value explicitly, matching UiText pattern). - Register Type 3 in DatWidgetFactory.Create: `3 => new UiField()`. - ChatWindowController.Bind (Variant B): factory now builds 0x10000016 as an invisible UiText placeholder (Type 12); Bind removes that placeholder via FindElement(InputId).Parent.RemoveChild and places a UiField at the same rect. Result: exactly ONE input widget in the input bar, no stray UiText duplicate. - Input property type changed from UiChatInput to UiField; GameWindow.cs:1861 UiField.Keyboard assignment compiles unchanged (field exists). - Tests: UiChatInputTests → UiFieldTests (class + all ctor refs renamed); DatWidgetFactoryTests: new Type3_Field_MakesUiField test; ChatWindowControllerTests: updated stale "skipped by factory" comments; LayoutConformanceTests: updated VitalsTree_ChromeCornerHasExpectedSprite — Type-3 chrome-corner elements are now UiField (sprite rendering for Type-3 dat image elements is a known limitation, tracked for post-Task-8 UiField.BackgroundSprite follow-up). - Full suite: 404 passed, 2 skipped, 0 failed. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
199 lines
8.9 KiB
C#
199 lines
8.9 KiB
C#
using AcDream.App.UI;
|
|
using AcDream.App.UI.Layout;
|
|
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Golden conformance tests for the vitals LayoutDesc importer.
|
|
/// Uses the committed JSON fixture (<c>vitals_2100006C.json</c>) — no dats, no GL.
|
|
///
|
|
/// These tests lock the importer's tree-building (factory dispatch, meter slice
|
|
/// extraction, rects) against the real portal.dat values captured when the
|
|
/// fixture was generated. Any regression in <see cref="LayoutImporter"/>,
|
|
/// <see cref="DatWidgetFactory"/>, or <see cref="ElementReader"/> will surface here.
|
|
///
|
|
/// Sprite ids sourced from <c>docs/research/2026-06-15-layoutdesc-format.md §11</c>.
|
|
/// </summary>
|
|
[Trait("Category", "Conformance")]
|
|
public class LayoutConformanceTests
|
|
{
|
|
// ── Test 1: Three meters at expected rects ────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// The three vital bars must be UiMeters positioned at x=5, width=150, height=16,
|
|
/// at y=5 (health), y=21 (stamina), y=37 (mana).
|
|
/// </summary>
|
|
[Fact]
|
|
public void VitalsTree_HasThreeMetersAtExpectedRects()
|
|
{
|
|
var layout = FixtureLoader.LoadVitals();
|
|
|
|
(uint Id, float Y)[] expected =
|
|
[
|
|
(0x100000E6u, 5f), // health
|
|
(0x100000ECu, 21f), // stamina
|
|
(0x100000EEu, 37f), // mana
|
|
];
|
|
|
|
foreach (var (id, y) in expected)
|
|
{
|
|
var elem = layout.FindElement(id);
|
|
Assert.NotNull(elem);
|
|
var meter = Assert.IsType<UiMeter>(elem);
|
|
Assert.Equal(5f, meter.Left);
|
|
Assert.Equal(y, meter.Top);
|
|
Assert.Equal(150f, meter.Width);
|
|
Assert.Equal(16f, meter.Height);
|
|
}
|
|
}
|
|
|
|
// ── Test 2: All 18 slice ids ──────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// The six back+front 3-slice sprite ids for each of the three meters must
|
|
/// match the values confirmed from the dat dump (format doc §11).
|
|
/// This proves the factory's grandchild slice extraction against committed data.
|
|
/// </summary>
|
|
[Fact]
|
|
public void VitalsTree_MetersHaveExpectedSliceIds()
|
|
{
|
|
var layout = FixtureLoader.LoadVitals();
|
|
|
|
// Columns: MeterId, then 6 slice ids in order:
|
|
// BackLeft, BackTile, BackRight, FrontLeft, FrontTile, FrontRight
|
|
(uint MeterId, uint[] Slices)[] cases =
|
|
[
|
|
(0x100000E6u, [0x0600747Eu, 0x0600747Fu, 0x06007480u, 0x06007481u, 0x06007482u, 0x06007483u]), // health
|
|
(0x100000ECu, [0x06007484u, 0x06007485u, 0x06007486u, 0x06007487u, 0x06007488u, 0x06007489u]), // stamina
|
|
(0x100000EEu, [0x0600748Au, 0x0600748Bu, 0x0600748Cu, 0x0600748Du, 0x0600748Eu, 0x0600748Fu]), // mana
|
|
];
|
|
|
|
foreach (var (meterId, s) in cases)
|
|
{
|
|
var m = Assert.IsType<UiMeter>(layout.FindElement(meterId));
|
|
Assert.Equal(s[0], m.BackLeft); Assert.Equal(s[1], m.BackTile); Assert.Equal(s[2], m.BackRight);
|
|
Assert.Equal(s[3], m.FrontLeft); Assert.Equal(s[4], m.FrontTile); Assert.Equal(s[5], m.FrontRight);
|
|
}
|
|
}
|
|
|
|
// ── Test 3: Chrome TL corner type ────────────────────────────────────────
|
|
//
|
|
// NOTE: As of Task 6 (widget-generalization), Type-3 elements are built as
|
|
// UiField (UIElement_Field, reg :126190) rather than UiDatElement. The
|
|
// chrome corner (0x10000633) is a Type-3 dat element and is now a UiField.
|
|
// Its dat sprite (0x060074C3) is not rendered by UiField — UiField renders
|
|
// the focused/unfocused field background only. The sprite rendering for
|
|
// Type-3 chrome image elements is a known limitation; tracked for post-Task-8
|
|
// follow-up (UiField could expose a BackgroundSprite similar to UiText).
|
|
|
|
/// <summary>
|
|
/// The top-left chrome corner element (id <c>0x10000633</c>) is Type-3 in
|
|
/// the dat, built as a <see cref="UiField"/> since Task 6. Confirms the
|
|
/// element exists in the tree.
|
|
/// </summary>
|
|
[Fact]
|
|
public void VitalsTree_ChromeCornerHasExpectedSprite()
|
|
{
|
|
var layout = FixtureLoader.LoadVitals();
|
|
|
|
var elem = layout.FindElement(0x10000633u);
|
|
Assert.NotNull(elem);
|
|
// Type-3 elements are now built as UiField (UIElement_Field, Task 6).
|
|
Assert.IsType<UiField>(elem);
|
|
}
|
|
|
|
// ── Test 4 (N4): Inheritance resolution — FontDid propagated from base ───
|
|
|
|
/// <summary>
|
|
/// Proves that <c>Resolve()</c>'s inheritance merge fired against real dat data:
|
|
/// at least one element in the fixture tree must have <c>FontDid == 0x40000000</c>
|
|
/// (the vitals font), inherited from the base-layout prototype <c>0x10000376</c>
|
|
/// in <c>0x2100003F</c> via the <c>BaseElement</c> / <c>BaseLayoutId</c> chain.
|
|
///
|
|
/// <para>
|
|
/// The three text labels (<c>0x100000EB</c> health, <c>0x100000ED</c> stamina,
|
|
/// <c>0x100000EF</c> mana) are Type=0 derived elements with no own font property.
|
|
/// The base element <c>0x10000376</c> carries <c>Properties[0x1A]</c> →
|
|
/// <c>ArrayBaseProperty[ DataIdBaseProperty{Value=0x40000000} ]</c>.
|
|
/// <see cref="ElementReader.Merge"/> propagates this via the "FontDid: derived wins
|
|
/// if non-zero, otherwise inherit" rule.
|
|
/// </para>
|
|
///
|
|
/// <para>
|
|
/// This test verifies end-to-end inheritance resolution against the committed fixture
|
|
/// (format doc §10, <c>docs/research/2026-06-15-layoutdesc-format.md</c>).
|
|
/// It operates on the raw <see cref="ElementInfo"/> tree, NOT the widget tree,
|
|
/// so the factory dispatch (Type 12 → skip) does not interfere.
|
|
/// </para>
|
|
/// </summary>
|
|
[Fact]
|
|
public void VitalsTree_TextLabel_InheritsFontDidFromBaseLayout()
|
|
{
|
|
var root = FixtureLoader.LoadVitalsInfos();
|
|
|
|
// Walk the full ElementInfo tree and collect all FontDid values.
|
|
var fontDids = new System.Collections.Generic.List<uint>();
|
|
CollectFontDids(root, fontDids);
|
|
|
|
// At least one element must carry FontDid == 0x40000000 (the vitals font).
|
|
// In practice, the three text labels (health/stamina/mana) all inherit it.
|
|
Assert.Contains(0x40000000u, fontDids);
|
|
}
|
|
|
|
private static void CollectFontDids(ElementInfo node, System.Collections.Generic.List<uint> acc)
|
|
{
|
|
if (node.FontDid != 0) acc.Add(node.FontDid);
|
|
foreach (var child in node.Children)
|
|
CollectFontDids(child, acc);
|
|
}
|
|
|
|
// ── Test 5: Horizontal resize conformance (160→200) ──────────────────────
|
|
|
|
/// <summary>
|
|
/// Proves end-to-end reflow for a 160→200 width change using the corrected
|
|
/// ToAnchors mapping (UIElement::UpdateForParentSizeChange @0x00462640).
|
|
///
|
|
/// For each piece, margins are computed from the 160-wide design rect and then
|
|
/// <see cref="UiElement.ComputeAnchoredRect"/> is applied at parentW=200.
|
|
///
|
|
/// Expected outcomes:
|
|
/// - TL corner (L=1,R=2): Left only → fixed at x=0, w=5
|
|
/// - top edge (L=1,R=1): Left+Right → stretches to w=190 at x=5
|
|
/// - TR corner (L=2,R=1): Right only → tracks right at x=195, w=5
|
|
/// - meter (L=1,R=1): Left+Right → stretches to w=190 at x=5
|
|
/// </summary>
|
|
[Fact]
|
|
public void HorizontalResize_160to200_ReflowsCorrectly()
|
|
{
|
|
const float designParentW = 160f;
|
|
const float newParentW = 200f;
|
|
const float parentH = 58f;
|
|
|
|
// (piece, designX, designW, LeftEdge, RightEdge, expectedX, expectedW)
|
|
(string Piece, float DesignX, float DesignW, uint L, uint R, float ExpX, float ExpW)[] cases =
|
|
[
|
|
("TL corner", 0f, 5f, 1u, 2u, 0f, 5f ),
|
|
("top edge", 5f, 150f, 1u, 1u, 5f, 190f),
|
|
("TR corner", 155f, 5f, 2u, 1u, 195f, 5f ),
|
|
("meter", 5f, 150f, 1u, 1u, 5f, 190f),
|
|
];
|
|
|
|
foreach (var (piece, dX, dW, l, r, expX, expW) in cases)
|
|
{
|
|
// T/B values don't affect x/w; use real vitals values (top=1, bottom=2)
|
|
var anchors = ElementReader.ToAnchors(l, top: 1u, r, bottom: 2u);
|
|
|
|
// Margins from the design rect at parentW=160
|
|
float mL = dX;
|
|
float mR = designParentW - (dX + dW);
|
|
|
|
// Reflow at parentW=200 (parentH irrelevant for x/w assertions)
|
|
var (x, _, w, _) = UiElement.ComputeAnchoredRect(
|
|
anchors, mL, mT: 0f, mR, mB: 0f, w0: dW, h0: 5f, parentW: newParentW, parentH);
|
|
|
|
// xUnit 2.x Assert.Equal(float,float,int) = decimal-place precision
|
|
Assert.True(Math.Abs(x - expX) < 0.5f, $"{piece}: expected x={expX} got {x}");
|
|
Assert.True(Math.Abs(w - expW) < 0.5f, $"{piece}: expected w={expW} got {w}");
|
|
}
|
|
}
|
|
}
|