feat(studio): Character panel pilot — gmCharacterInfoUI report text + menu-bar picker
Task A — CharacterController (LayoutDesc 0x2100002E)
- CharacterSheet.cs: data record for all fields used by the report sections,
with retail property-id citations per field (DateOfBirth 0x62, TotalPlayTime
0x7d, NumDeaths 0x2b, SkillCredits 0xb5/0xc0, AugmentationStat 0x162,
EncumbranceVal 0x5/0xe6).
- CharacterController.cs: static Bind(layout, data, datFont) wires element
0x1000011d (m_pMainText, confirmed gmCharacterInfoUI::PostInit 0x004b86f0) as
a UiText; LinesProvider builds the report. Report section order mirrors
gmCharacterInfoUI::Update (0x004ba790):
1. UpdatePlayerBirthAgeDeaths 0x004b8cb0 — birth/age/deaths
2. UpdateEnduranceInfo 0x004b8eb0 — vitals (H/S/M cur/max)
3. UpdateInnateAttributeInfo 0x004b87e0 — 6 attrs InqAttribute 1,2,4,3,5,6
= Strength, Endurance, Quickness, Coordination, Focus, Self
4. UpdateFakeSkills 0x004b8930 — skill credits (InqInt 0xb5 / 0xc0)
5. UpdateAugmentations 0x004b9000 — aug name (InqInt 0x162 switch 1..0xb)
6. UpdateLoad 0x004b8a20 — burden cur/max/pct
Element 0x1000011d imports as UiText (Type 12); multi-line text set via
LinesProvider — same pattern as ChatWindowController transcript.
- SampleData.SampleCharacter(): plausible retail-scale CharacterSheet fixture.
- FixtureProvider.Populate case 0x2100002Eu: CharacterController.Bind with
SampleData.SampleCharacter + VitalsDatFont.
- CharacterControllerTests.cs: 10 pure data-wiring + content tests (no GL/dats).
Task B — Studio panel picker → main menu bar
- StudioWindow.OnRender: replaced the floating "Studio" toolbar window (kToolbarH
40px) with ImGui.BeginMainMenuBar / EndMainMenuBar (kMenuBarH 22px). Panel
picker combo now lives in the always-on-top menu bar and cannot be occluded by
Tree/Canvas/Props panes. paneY reduced from 40→22, freeing ~18px for the canvas.
Build: dotnet build green (0 CS errors; DLL-lock warnings are AcDream.App being
live — not compile errors). Full suite: 619 passed, 2 skipped.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
c03a241884
commit
4988dd4dfe
6 changed files with 515 additions and 8 deletions
184
tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs
Normal file
184
tests/AcDream.App.Tests/UI/Layout/CharacterControllerTests.cs
Normal file
|
|
@ -0,0 +1,184 @@
|
|||
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="CharacterController"/> and <see cref="SampleData.SampleCharacter"/>.
|
||||
/// No dats, no GL — pure data-wiring + report-content tests.
|
||||
/// </summary>
|
||||
public class CharacterControllerTests
|
||||
{
|
||||
// ── Test 1: Bind finds the main text element and sets LinesProvider ───────
|
||||
|
||||
[Fact]
|
||||
public void Bind_SetsLinesProviderOnMainText()
|
||||
{
|
||||
var text = new UiText();
|
||||
var layout = FakeLayout((CharacterController.MainTextId, text));
|
||||
|
||||
CharacterController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
// LinesProvider must be replaced (default returns empty).
|
||||
var lines = text.LinesProvider();
|
||||
Assert.True(lines.Count > 0, "Expected non-empty report after Bind");
|
||||
}
|
||||
|
||||
// ── Test 2: Report contains all six attribute names (decomp order 1,2,4,3,5,6) ───
|
||||
|
||||
[Fact]
|
||||
public void Bind_ReportContainsAllSixAttributes()
|
||||
{
|
||||
var text = new UiText();
|
||||
var layout = FakeLayout((CharacterController.MainTextId, text));
|
||||
|
||||
CharacterController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
var allText = string.Join("\n", text.LinesProvider().Select(l => l.Text));
|
||||
|
||||
Assert.Contains("Strength", allText);
|
||||
Assert.Contains("Endurance", allText);
|
||||
Assert.Contains("Quickness", allText);
|
||||
Assert.Contains("Coordination", allText);
|
||||
Assert.Contains("Focus", allText);
|
||||
Assert.Contains("Self", allText);
|
||||
}
|
||||
|
||||
// ── Test 3: Report contains vitals section ────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Bind_ReportContainsVitals()
|
||||
{
|
||||
var text = new UiText();
|
||||
var layout = FakeLayout((CharacterController.MainTextId, text));
|
||||
|
||||
CharacterController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
var allText = string.Join("\n", text.LinesProvider().Select(l => l.Text));
|
||||
|
||||
Assert.Contains("Health", allText);
|
||||
Assert.Contains("Stamina", allText);
|
||||
Assert.Contains("Mana", allText);
|
||||
}
|
||||
|
||||
// ── Test 4: Report contains encumbrance / burden section ─────────────────
|
||||
|
||||
[Fact]
|
||||
public void Bind_ReportContainsBurden()
|
||||
{
|
||||
var text = new UiText();
|
||||
var layout = FakeLayout((CharacterController.MainTextId, text));
|
||||
|
||||
CharacterController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
var allText = string.Join("\n", text.LinesProvider().Select(l => l.Text));
|
||||
|
||||
Assert.Contains("Burden", allText);
|
||||
Assert.Contains("Capacity", allText);
|
||||
}
|
||||
|
||||
// ── Test 5: Report contains the sample character's name ──────────────────
|
||||
|
||||
[Fact]
|
||||
public void Bind_ReportContainsSampleName()
|
||||
{
|
||||
var text = new UiText();
|
||||
var layout = FakeLayout((CharacterController.MainTextId, text));
|
||||
|
||||
CharacterController.Bind(layout, SampleData.SampleCharacter);
|
||||
|
||||
var allText = string.Join("\n", text.LinesProvider().Select(l => l.Text));
|
||||
|
||||
// SampleData.SampleCharacter().Name == "Studio Player"
|
||||
Assert.Contains("Studio Player", allText);
|
||||
}
|
||||
|
||||
// ── Test 6: Missing element id is a no-op (no throw) ─────────────────────
|
||||
|
||||
[Fact]
|
||||
public void Bind_MissingElement_DoesNotThrow()
|
||||
{
|
||||
// Layout does not contain MainTextId — Bind must silently skip.
|
||||
var layout = FakeLayout(); // empty layout
|
||||
|
||||
// Must not throw.
|
||||
CharacterController.Bind(layout, SampleData.SampleCharacter);
|
||||
}
|
||||
|
||||
// ── Test 7: Element that is NOT a UiText is silently skipped ─────────────
|
||||
|
||||
[Fact]
|
||||
public void Bind_WrongElementType_DoesNotThrow()
|
||||
{
|
||||
// Place a UiMeter at the MainTextId — Bind must skip (not crash on wrong type).
|
||||
var meter = new UiMeter();
|
||||
var layout = FakeLayout((CharacterController.MainTextId, meter));
|
||||
|
||||
CharacterController.Bind(layout, SampleData.SampleCharacter);
|
||||
// No exception; meter is unchanged.
|
||||
}
|
||||
|
||||
// ── Test 8: SampleCharacter fixture has non-zero attribute values ─────────
|
||||
|
||||
[Fact]
|
||||
public void SampleCharacter_AttributesAreNonZero()
|
||||
{
|
||||
var s = SampleData.SampleCharacter();
|
||||
|
||||
Assert.True(s.Strength > 0, "Strength must be > 0");
|
||||
Assert.True(s.Endurance > 0, "Endurance must be > 0");
|
||||
Assert.True(s.Quickness > 0, "Quickness must be > 0");
|
||||
Assert.True(s.Coordination > 0, "Coordination must be > 0");
|
||||
Assert.True(s.Focus > 0, "Focus must be > 0");
|
||||
Assert.True(s.Self > 0, "Self must be > 0");
|
||||
}
|
||||
|
||||
// ── Test 9: SampleCharacter fixture has coherent vitals ──────────────────
|
||||
|
||||
[Fact]
|
||||
public void SampleCharacter_VitalsAreCoherent()
|
||||
{
|
||||
var s = SampleData.SampleCharacter();
|
||||
|
||||
Assert.True(s.HealthCurrent > 0 && s.HealthCurrent <= s.HealthMax, "Health cur must be in [1, max]");
|
||||
Assert.True(s.StaminaCurrent > 0 && s.StaminaCurrent <= s.StaminaMax, "Stamina cur must be in [1, max]");
|
||||
Assert.True(s.ManaCurrent > 0 && s.ManaCurrent <= s.ManaMax, "Mana cur must be in [1, max]");
|
||||
}
|
||||
|
||||
// ── Test 10: Report contains attribute VALUES from the fixture ────────────
|
||||
|
||||
[Fact]
|
||||
public void Bind_ReportContainsSampleAttributeValues()
|
||||
{
|
||||
var text = new UiText();
|
||||
var layout = FakeLayout((CharacterController.MainTextId, text));
|
||||
|
||||
var sheet = SampleData.SampleCharacter();
|
||||
CharacterController.Bind(layout, () => sheet);
|
||||
|
||||
var allText = string.Join("\n", text.LinesProvider().Select(l => l.Text));
|
||||
|
||||
// All six attribute values should appear as strings in the report.
|
||||
Assert.Contains(sheet.Strength.ToString(), allText);
|
||||
Assert.Contains(sheet.Endurance.ToString(), allText);
|
||||
Assert.Contains(sheet.Quickness.ToString(), allText);
|
||||
Assert.Contains(sheet.Coordination.ToString(), allText);
|
||||
Assert.Contains(sheet.Focus.ToString(), allText);
|
||||
Assert.Contains(sheet.Self.ToString(), allText);
|
||||
}
|
||||
|
||||
// ── Helpers ───────────────────────────────────────────────────────────────
|
||||
|
||||
private static ImportedLayout FakeLayout(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);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue