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:
Erik 2026-06-25 18:37:30 +02:00
parent c03a241884
commit 4988dd4dfe
6 changed files with 515 additions and 8 deletions

View file

@ -0,0 +1,86 @@
namespace AcDream.App.UI.Layout;
/// <summary>
/// Snapshot of all character data the Character window report uses.
/// Passed to <see cref="CharacterController.Bind"/> as a provider delegate
/// so the window can be rebound to live data in GameWindow or a static
/// fixture in the UI Studio.
///
/// <para>Field names and retail property ids confirmed from
/// <c>gmCharacterInfoUI::UpdatePlayerBirthAgeDeaths</c> (0x004b8cb0),
/// <c>UpdateEnduranceInfo</c> (0x004b8eb0),
/// <c>UpdateInnateAttributeInfo</c> (0x004b87e0),
/// <c>UpdateFakeSkills</c> (0x004b8930),
/// <c>UpdateAugmentations</c> (0x004b9000), and
/// <c>UpdateLoad</c> (0x004b8a20).</para>
/// </summary>
public sealed class CharacterSheet
{
// ── Identity ──────────────────────────────────────────────────────────────
/// <summary>Character name (first line of the report).</summary>
public string Name { get; init; } = string.Empty;
/// <summary>Character level.</summary>
public int Level { get; init; }
/// <summary>Race string, e.g. "Aluvian". Null = omit.</summary>
public string? Race { get; init; }
/// <summary>Heritage string, e.g. "Aluvian Heritage". Null = omit.</summary>
public string? Heritage { get; init; }
/// <summary>Title string, e.g. "the Adventurer". Null = omit.</summary>
public string? Title { get; init; }
// ── Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) ─────────
/// <summary>Formatted birth date string (retail InqInt(0x62) → strftime).
/// Null = omit the birth line.</summary>
public string? BirthDate { get; init; }
/// <summary>Formatted play-time duration (retail InqInt(0x7d) → QueryDuration).
/// Null = omit the age line.</summary>
public string? PlayTime { get; init; }
/// <summary>Total deaths (retail InqInt(0x2b) = NumDeaths).</summary>
public int Deaths { get; init; }
// ── Vitals (UpdateEnduranceInfo 0x004b8eb0) ─────────────────────────────
public int HealthCurrent { get; init; }
public int HealthMax { get; init; }
public int StaminaCurrent { get; init; }
public int StaminaMax { get; init; }
public int ManaCurrent { get; init; }
public int ManaMax { get; init; }
// ── Innate attributes (UpdateInnateAttributeInfo 0x004b87e0) ────────────
// InqAttribute order: 1,2,4,3,5,6 = Strength, Endurance, Quickness, Coordination, Focus, Self.
public int Strength { get; init; }
public int Endurance { get; init; }
public int Quickness { get; init; }
public int Coordination { get; init; }
public int Focus { get; init; }
public int Self { get; init; }
// ── Skills (UpdateFakeSkills 0x004b8930) ────────────────────────────────
// Retail InqInt(0xb5) = SkillCredits; InqInt(0xc0) = AvailableSkillCredits.
public int UnspentSkillCredits { get; init; }
public int SpecializedSkillCredits { get; init; }
// ── Augmentations (UpdateAugmentations 0x004b9000) ─────────────────────
// Retail InqInt(0x162) = AugmentationStat; string-switch 1..0xb.
/// <summary>Augmentation name from the switch in UpdateAugmentations (0x004b9000),
/// e.g. "Swords", "Two Handed Weapons". Null = "None".</summary>
public string? AugmentationName { get; init; }
// ── Burden / load (UpdateLoad 0x004b8a20) ───────────────────────────────
// Retail InqLoad + EncumbranceCapacity(Strength, AugEncumbrance).
public int BurdenCurrent { get; init; }
public int BurdenMax { get; init; }
}