fix(ui): match retail vitae and character info

Restore Vitae's omitted penalty paragraph, replace the invented character summary with gmCharacterInfoUI's ordered report and property meanings, preserve authored translucent body surfaces, and initialize the end-session button in its visible Normal DAT state.

Release build and all 5,830 tests pass with five intentional skips. Connected visual gate pending.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 12:06:14 +02:00
parent d1d603105f
commit 16c21e299c
21 changed files with 771 additions and 303 deletions

View file

@ -4,16 +4,10 @@ using AcDream.App.UI.Layout;
namespace AcDream.App.Tests.UI.Layout;
/// <summary>
/// Unit tests for <see cref="CharacterController"/> and <see cref="SampleData.SampleCharacter"/>.
/// Production Character Information data authors m_pMainText (0x1000011d);
/// reduced unit-test layouts exercise the controller's fallback element.
/// No GL — pure data-wiring + report-content tests.
/// </summary>
public class CharacterControllerTests
public sealed class CharacterControllerTests
{
[Fact]
public void AuthoredCharacterInformationPanel_BindsTextScrollbarAndClose()
public void AuthoredPanel_BindsTextScrollbarAndClose()
{
ImportedLayout layout = FixtureLoader.LoadCharacterInformation();
int closes = 0;
@ -21,15 +15,13 @@ public class CharacterControllerTests
CharacterController.Bind(
layout, SampleData.SampleCharacter, close: () => closes++);
Assert.Equal(CharacterController.RootId, layout.Root.DatElementId);
Assert.Equal((300f, 362f), (layout.Root.Width, layout.Root.Height));
UiText text = Assert.IsType<UiText>(
layout.FindElement(CharacterController.MainTextId));
UiScrollbar scrollbar = Assert.IsType<UiScrollbar>(
layout.FindElement(CharacterController.ScrollbarId));
Assert.Same(text.Scroll, scrollbar.Model);
Assert.False(text.PreserveEndOnLayout);
Assert.Contains("Studio Player", text.LinesProvider().Select(line => line.Text));
Assert.Contains("You were born on", VisibleText(text));
UiButton close = Assert.IsType<UiButton>(
layout.FindElement(CharacterController.CloseId));
@ -37,136 +29,76 @@ public class CharacterControllerTests
Assert.Equal(1, closes);
}
/// <summary>Bind on an empty layout, then return the created m_pMainText element.</summary>
private static UiText BindAndGetText(System.Func<CharacterSheet> data)
[Fact]
public void Report_UsesRetailContentInsteadOfInventedCharacterSheetSummary()
{
var layout = FakeLayout();
CharacterController.Bind(layout, data);
return layout.Root.Children.OfType<UiText>()
.First(t => t.EventId == CharacterController.MainTextId);
string report = Report(SampleData.SampleCharacter());
Assert.DoesNotContain("Studio Player", report);
Assert.DoesNotContain("Level 126", report);
Assert.DoesNotContain("Health:", report);
Assert.DoesNotContain("Augmentations", report);
Assert.DoesNotContain("Encumbrance", report);
Assert.Contains("You have died 42 times.", report);
Assert.Contains("Natural Resistances:", report);
Assert.Contains("Drain Resistances:", report);
Assert.Contains("Regeneration Bonus:", report);
Assert.Contains("Innate Strength: 200", report);
Assert.Contains("Innate Coordination: 10", report);
Assert.Contains("Chess Rank: 12", report);
Assert.Contains("Fishing Skill: 4", report);
Assert.Contains("Your melee mastery is Swords.", report);
Assert.Contains("You are not overburdened at this time.", report);
}
private static string Report(System.Func<CharacterSheet> data)
=> string.Join("\n", BindAndGetText(data).LinesProvider().Select(l => l.Text));
[Theory]
[InlineData(200, "None")]
[InlineData(201, "Poor")]
[InlineData(260, "Poor")]
[InlineData(261, "Mediocre")]
[InlineData(321, "Hardy")]
[InlineData(381, "Resilient")]
[InlineData(441, "Indomitable")]
public void ResistanceGrade_UsesRetailInclusiveBoundaries(
int value, string expected)
=> Assert.Equal(expected, CharacterController.ResistanceGrade(
value, 200, 260, 320, 380, 440));
// ── Test 1: Bind creates m_pMainText with a non-empty report ──────────────
[Theory]
[InlineData(0, "")]
[InlineData(1, "1 second")]
[InlineData(61, "1 minute 1 second")]
[InlineData(3_196_800, "1 month 1 week")]
[InlineData(31_536_000, "1 year")]
public void Duration_UsesRetailUnitDecomposition(int seconds, string expected)
=> Assert.Equal(expected, CharacterController.FormatRetailDuration(seconds));
[Fact]
public void Bind_CreatesMainTextWithReport()
public void DeathCount_UsesRetailSpecialCases()
{
var text = BindAndGetText(SampleData.SampleCharacter);
Assert.True(text.LinesProvider().Count > 0, "Expected non-empty report after Bind");
Assert.Contains("never died", Report(new CharacterSheet { Deaths = 0 }));
Assert.Contains("only once", Report(new CharacterSheet { Deaths = 1 }));
Assert.Contains("died twice", Report(new CharacterSheet { Deaths = 2 }));
Assert.Contains("died 3 times", Report(new CharacterSheet { Deaths = 3 }));
}
// ── Test 2: Report contains all six attribute names (decomp order 1,2,4,3,5,6) ───
[Fact]
public void Bind_ReportContainsAllSixAttributes()
public void EmptyLayout_GetsFallbackMainText()
{
var allText = Report(SampleData.SampleCharacter);
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 allText = Report(SampleData.SampleCharacter);
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 allText = Report(SampleData.SampleCharacter);
Assert.Contains("Burden", allText);
Assert.Contains("Capacity", allText);
}
// ── Test 5: Report contains the sample character's name ──────────────────
[Fact]
public void Bind_ReportContainsSampleName()
=> Assert.Contains("Studio Player", Report(SampleData.SampleCharacter));
// ── Test 6: Bind on a layout WITHOUT the element creates it (no throw) ────
[Fact]
public void Bind_OnEmptyLayout_CreatesElement()
{
var layout = FakeLayout(); // no MainTextId present
ImportedLayout layout = FakeLayout();
CharacterController.Bind(layout, SampleData.SampleCharacter);
Assert.Contains(layout.Root.Children.OfType<UiText>(),
t => t.EventId == CharacterController.MainTextId);
text => text.EventId == CharacterController.MainTextId);
}
// ── Test 7: The created element carries the m_pMainText id ───────────────
private static string Report(CharacterSheet sheet)
=> CharacterController.BuildReport(sheet, CharacterInfoStrings.English);
[Fact]
public void Bind_CreatedElementHasMainTextId()
=> Assert.Equal(CharacterController.MainTextId, BindAndGetText(SampleData.SampleCharacter).EventId);
private static string VisibleText(UiText text)
=> string.Join('\n', text.LinesProvider().Select(line => line.Text));
// ── 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 sheet = SampleData.SampleCharacter();
var allText = Report(() => sheet);
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);
}
private static ImportedLayout FakeLayout()
=> new(new UiPanel { Width = 300f, Height = 362f },
new Dictionary<uint, UiElement>());
}