Redo of the character pilot per faithful-port rules. The previous pilot GUESSED a text report and put it on the wrong window. The decomp (verified by dumping acdream's importer-resolved tree) shows LayoutDesc 0x2100002E is the tabbed Attributes/Skills/Titles window: its tab-content slot 0x1000022B mounts sub-layout 0x2100002C (gmAttributeUI) which chains into the gmStatManagementUI header. The importer ALREADY mounts every content element (FindElement resolves name 0x10000231, heritage 0x10000232, PK 0x10000233, level 0x1000023B, total-XP 0x10000235, XP meter 0x10000236 → UiMeter, list box 0x1000023D) — EventId was a red herring (the dat id lives in _byId, not the widget's EventId field). CharacterStatController (port of gmStatManagementUI::UpdateCharacterInfo 0x004f0770 + UpdateExperience 0x004f0a70 + UpdatePKStatus 0x004f00a0) binds those real elements: name, heritage, PK status, level, total XP, the XP-to-level meter fill, and the six innate attributes in the list box. Studio (--layout 0x2100002E) now renders the actual panel content, not an overlay. 16 controller tests green; full App suite stays green. Refinements with known decomp sources (follow-ups): tab-button active/inactive state so the 3 tabs draw their sprites; exact label wording from the StringTable (table 0x10000001 → dat 0x31000001); the full AttributeInfoRegion row template (column-aligned values + raise buttons). CharacterController (text-report 0x2100001A) retained for that separate sub-panel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
108 lines
3.9 KiB
C#
108 lines
3.9 KiB
C#
using AcDream.App.Studio;
|
|
using AcDream.App.UI;
|
|
using AcDream.App.UI.Layout;
|
|
|
|
namespace AcDream.App.Tests.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Unit tests for <see cref="CharacterStatController"/> — the Attributes-tab controller that
|
|
/// binds the REAL importer-mounted header + list elements (no created overlay). Pure data
|
|
/// wiring against fake layouts; no dats, no GL.
|
|
/// </summary>
|
|
public class CharacterStatControllerTests
|
|
{
|
|
// ── Header labels bind to the sheet ──────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Bind_SetsNameLabel()
|
|
{
|
|
var name = new UiText();
|
|
var layout = Fake((CharacterStatController.NameId, name));
|
|
|
|
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
|
|
|
Assert.Equal("Studio Player", name.LinesProvider()[0].Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bind_SetsLevelLabel()
|
|
{
|
|
var level = new UiText();
|
|
var layout = Fake((CharacterStatController.LevelId, level));
|
|
|
|
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
|
|
|
Assert.Equal("126", level.LinesProvider()[0].Text);
|
|
}
|
|
|
|
[Fact]
|
|
public void Bind_SetsHeritageAndPkLabels()
|
|
{
|
|
var heritage = new UiText();
|
|
var pk = new UiText();
|
|
var layout = Fake((CharacterStatController.HeritageId, heritage),
|
|
(CharacterStatController.PkStatusId, pk));
|
|
|
|
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
|
|
|
Assert.Equal("Aluvian Heritage", heritage.LinesProvider()[0].Text);
|
|
Assert.Equal("Non-Player Killer", pk.LinesProvider()[0].Text);
|
|
}
|
|
|
|
// ── XP meter fill ────────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Bind_SetsXpMeterFill()
|
|
{
|
|
var meter = new UiMeter();
|
|
var layout = Fake((CharacterStatController.XpMeterId, meter));
|
|
|
|
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
|
|
|
var fill = meter.Fill();
|
|
Assert.NotNull(fill);
|
|
Assert.True(System.MathF.Abs(fill!.Value - 0.63f) < 0.001f, $"expected ~0.63, got {fill}");
|
|
}
|
|
|
|
// ── Attribute list ───────────────────────────────────────────────────────
|
|
|
|
[Fact]
|
|
public void Bind_PopulatesAttributeList()
|
|
{
|
|
var list = new UiPanel();
|
|
var layout = Fake((CharacterStatController.ListBoxId, list));
|
|
|
|
CharacterStatController.Bind(layout, SampleData.SampleCharacter);
|
|
|
|
var rows = list.Children.OfType<UiText>().First();
|
|
var text = string.Join("\n", rows.LinesProvider().Select(l => l.Text));
|
|
|
|
Assert.Contains("Strength", text);
|
|
Assert.Contains("Endurance", text);
|
|
Assert.Contains("Coordination", text);
|
|
Assert.Contains("Quickness", text);
|
|
Assert.Contains("Focus", text);
|
|
Assert.Contains("Self", text);
|
|
Assert.Contains("100", text);
|
|
}
|
|
|
|
// ── Robustness: a partial layout (missing ids) must not throw ────────────
|
|
|
|
[Fact]
|
|
public void Bind_MissingElements_DoesNotThrow()
|
|
=> CharacterStatController.Bind(Fake(), SampleData.SampleCharacter);
|
|
|
|
// ── Helper ─────────────────────────────────────────────────────────────
|
|
|
|
private static ImportedLayout Fake(params (uint id, UiElement e)[] items)
|
|
{
|
|
var dict = new Dictionary<uint, UiElement>();
|
|
var root = new UiPanel();
|
|
foreach (var (id, e) in items)
|
|
{
|
|
root.AddChild(e);
|
|
dict[id] = e;
|
|
}
|
|
return new ImportedLayout(root, dict);
|
|
}
|
|
}
|