using System;
using System.Collections.Generic;
using System.Numerics;
using AcDream.App.UI;
namespace AcDream.App.UI.Layout;
///
/// Per-window controller for the Character window (LayoutDesc 0x2100002E, gmCharacterInfoUI).
///
/// Retail fills a single scrollable UIElement_Text element (id 0x1000011d,
/// m_pMainText) by calling a sequence of Update* methods that each
/// AppendStringInfo into the same element. This controller replicates that report
/// structure using acdream's .
///
/// Section order (ported from gmCharacterInfoUI::Update 0x004ba790):
///
/// - Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0)
/// - Vitals / endurance (UpdateEnduranceInfo 0x004b8eb0)
/// - Innate attributes (UpdateInnateAttributeInfo 0x004b87e0):
/// InqAttribute 1,2,4,3,5,6 = Strength, Endurance, Quickness, Coordination, Focus, Self
/// - Skills summary (UpdateFakeSkills 0x004b8930)
/// - Augmentations (UpdateAugmentations 0x004b9000)
/// - Burden / load (UpdateLoad 0x004b8a20)
///
/// Each section is separated by a blank line (retail: AppendStringInfo(m_pMainText, &var_120)
/// with an empty StringInfo between sections).
///
/// StringInfo resolution is NOT ported — the full dat string-table lookup is a
/// later refinement. For this pilot, labels are composed directly from the well-known
/// AC attribute / stat names confirmed in the decomp string literals and data identifiers.
///
public static class CharacterController
{
/// Dat element id for the main report text (m_pMainText). Confirmed in
/// gmCharacterInfoUI::PostInit 0x004b86f0: GetChildRecursive(this, 0x1000011d).
public const uint MainTextId = 0x1000011du;
/// White body text — matches retail's default UIElement_Text foreground.
private static readonly Vector4 BodyColor = new(1f, 1f, 1f, 1f);
/// Yellow section header — retail uses a different StringInfo color per section header.
private static readonly Vector4 HeaderColor = new(1f, 0.85f, 0.35f, 1f);
///
/// Bind the character report text element found in to
/// . The report is regenerated each frame via
/// so the Studio or a live session can push a fresh
/// without re-binding.
///
/// The element must resolve as a (Type 12 in the dat).
/// If 0x1000011d is absent from the layout the bind is a no-op — partial layouts
/// (e.g. test fakes) don't cause errors.
///
/// Imported 0x2100002E layout tree.
/// Provider returning the current .
/// Retail dat font forwarded from the render stack. May be null
/// (falls back to the BitmapFont debug path).
public static void Bind(
ImportedLayout layout,
Func data,
UiDatFont? datFont = null)
{
// Retail's gmCharacterInfoUI CREATES m_pMainText (0x1000011d) at RUNTIME — it is NOT a static
// element in any LayoutDesc (confirmed: acdream's dat-import of 0x2100002E AND 0x2100006E both
// lack it; only a runtime UI-tree capture has it). So replicate that: build the report text
// element ourselves and place it in the panel body, exactly as the gm*UI does at runtime.
var root = layout.Root;
if (root is null) return;
var text = new UiText
{
EventId = MainTextId,
Name = "m_pMainText",
Left = 12f,
Top = 44f, // below the window header row
Width = System.Math.Max(40f, root.Width - 24f),
Height = System.Math.Max(40f, root.Height - 56f),
Anchors = AnchorEdges.None,
ZOrder = 1_000_000, // draw above the static chrome
DatFont = datFont,
ClickThrough = false,
};
text.LinesProvider = () => BuildReport(data());
root.AddChild(text);
}
// ── Report builder ────────────────────────────────────────────────────────
///
/// Build the complete character report as a line list.
/// Order mirrors gmCharacterInfoUI::Update (0x004ba790).
///
private static IReadOnlyList BuildReport(CharacterSheet s)
{
var lines = new List();
// ── Section 1: Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) ──
// Retail InqInt(0x62) = DateOfBirth (unix timestamp); InqInt(0x7d) = TotalPlayTime;
// InqInt(0x2b) = NumDeaths.
AppendHeader(lines, s.Name);
Append(lines, $"Level {s.Level}");
if (s.Race is not null)
Append(lines, s.Race);
if (s.Heritage is not null)
Append(lines, s.Heritage);
if (s.Title is not null)
Append(lines, s.Title);
AppendBlank(lines);
if (s.BirthDate is not null)
Append(lines, $"Birth: {s.BirthDate}");
if (s.PlayTime is not null)
Append(lines, $"Age: {s.PlayTime}");
Append(lines, $"Deaths: {s.Deaths}");
AppendBlank(lines);
// ── Section 2: Vitals / endurance (UpdateEnduranceInfo 0x004b8eb0) ──
// Retail InqAttribute(1) = Strength base, InqAttribute(2) = Endurance base.
// Computes max-stamina tier and max-health tier from (Str+End) and (End+2*End) sums.
AppendHeader(lines, "Vitals");
Append(lines, $"Health: {s.HealthCurrent} / {s.HealthMax}");
Append(lines, $"Stamina: {s.StaminaCurrent} / {s.StaminaMax}");
Append(lines, $"Mana: {s.ManaCurrent} / {s.ManaMax}");
AppendBlank(lines);
// ── Section 3: Innate attributes (UpdateInnateAttributeInfo 0x004b87e0) ──
// InqAttribute order: 1,2,4,3,5,6 = Strength, Endurance, Quickness, Coordination,
// Focus, Self (confirmed from the AddVariable_Int sequence in the decomp).
AppendHeader(lines, "Attributes");
Append(lines, $"Strength: {s.Strength}");
Append(lines, $"Endurance: {s.Endurance}");
Append(lines, $"Quickness: {s.Quickness}");
Append(lines, $"Coordination: {s.Coordination}");
Append(lines, $"Focus: {s.Focus}");
Append(lines, $"Self: {s.Self}");
AppendBlank(lines);
// ── Section 4: Skills summary (UpdateFakeSkills 0x004b8930) ──
// Retail InqInt(0xb5) = SkillCredits remaining; InqInt(0xc0) = AvailableSkillCredits.
AppendHeader(lines, "Skills");
Append(lines, $"Unspent skill credits: {s.UnspentSkillCredits}");
if (s.SpecializedSkillCredits > 0)
Append(lines, $"Specialized credits: {s.SpecializedSkillCredits}");
AppendBlank(lines);
// ── Section 5: Augmentations (UpdateAugmentations 0x004b9000) ──
// Retail InqInt(0x162) = AugmentationStat; string-switch on value 1..0xb + Unknown.
AppendHeader(lines, "Augmentations");
if (s.AugmentationName is not null)
Append(lines, s.AugmentationName);
else
Append(lines, "None");
AppendBlank(lines);
// ── Section 6: Burden / load (UpdateLoad 0x004b8a20) ──
// Retail InqLoad → EncumbranceCapacity(Strength, AugEncumbrance).
// When load >= 1.0 (overloaded): shows "X burden over capacity; Y% speed penalty".
// When aug > 0: shows "N augmentations (X.X% bonus)".
AppendHeader(lines, "Encumbrance");
Append(lines, $"Burden: {s.BurdenCurrent}");
Append(lines, $"Capacity: {s.BurdenMax}");
if (s.BurdenMax > 0)
{
int pct = (int)Math.Round(100.0 * s.BurdenCurrent / s.BurdenMax);
Append(lines, $"Load: {pct}%");
}
return lines;
}
// ── Line helpers ──────────────────────────────────────────────────────────
private static void AppendHeader(List lines, string text)
=> lines.Add(new UiText.Line(text, HeaderColor));
private static void Append(List lines, string text)
=> lines.Add(new UiText.Line(text, BodyColor));
private static void AppendBlank(List lines)
=> lines.Add(new UiText.Line(string.Empty, BodyColor));
}