Port the authored effect row template, remaining-time and selection details, synchronize the full gmPanelUI child geometry, and route the burden indicator to Character Information panel 3. Co-authored-by: OpenAI Codex <codex@openai.com>
197 lines
9.5 KiB
C#
197 lines
9.5 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Numerics;
|
|
using AcDream.App.UI;
|
|
|
|
namespace AcDream.App.UI.Layout;
|
|
|
|
/// <summary>
|
|
/// Per-window controller for the Character Information child in the production
|
|
/// gmPanelUI layout (LayoutDesc 0x2100006E, root 0x10000183).
|
|
///
|
|
/// <para>Retail fills a single scrollable <c>UIElement_Text</c> element (id 0x1000011d,
|
|
/// <c>m_pMainText</c>) by calling a sequence of Update* methods that each
|
|
/// <c>AppendStringInfo</c> into the same element. This controller replicates that report
|
|
/// structure using acdream's <see cref="UiText.LinesProvider"/>.</para>
|
|
///
|
|
/// <para>Section order (ported from <c>gmCharacterInfoUI::Update</c> 0x004ba790):
|
|
/// <list type="number">
|
|
/// <item>Birth / age / deaths (<c>UpdatePlayerBirthAgeDeaths</c> 0x004b8cb0)</item>
|
|
/// <item>Vitals / endurance (<c>UpdateEnduranceInfo</c> 0x004b8eb0)</item>
|
|
/// <item>Innate attributes (<c>UpdateInnateAttributeInfo</c> 0x004b87e0):
|
|
/// InqAttribute 1,2,4,3,5,6 = Strength, Endurance, Quickness, Coordination, Focus, Self</item>
|
|
/// <item>Skills summary (<c>UpdateFakeSkills</c> 0x004b8930)</item>
|
|
/// <item>Augmentations (<c>UpdateAugmentations</c> 0x004b9000)</item>
|
|
/// <item>Burden / load (<c>UpdateLoad</c> 0x004b8a20)</item>
|
|
/// </list>
|
|
/// Each section is separated by a blank line (retail: <c>AppendStringInfo(m_pMainText, &var_120)</c>
|
|
/// with an empty StringInfo between sections).</para>
|
|
///
|
|
/// <para>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.</para>
|
|
/// </summary>
|
|
public static class CharacterController
|
|
{
|
|
public const uint LayoutId = 0x2100006Eu;
|
|
public const uint RootId = 0x10000183u;
|
|
|
|
/// <summary>Dat element id for the main report text (m_pMainText). Confirmed in
|
|
/// <c>gmCharacterInfoUI::PostInit</c> 0x004b86f0: <c>GetChildRecursive(this, 0x1000011d)</c>.</summary>
|
|
public const uint MainTextId = 0x1000011du;
|
|
public const uint ScrollbarId = 0x1000011Eu;
|
|
public const uint CloseId = 0x100000FCu;
|
|
|
|
/// <summary>White body text — matches retail's default UIElement_Text foreground.</summary>
|
|
private static readonly Vector4 BodyColor = new(1f, 1f, 1f, 1f);
|
|
|
|
/// <summary>Yellow section header — retail uses a different StringInfo color per section header.</summary>
|
|
private static readonly Vector4 HeaderColor = new(1f, 0.85f, 0.35f, 1f);
|
|
|
|
/// <summary>
|
|
/// Bind the character report text element found in <paramref name="layout"/> to
|
|
/// <paramref name="data"/>. The report is regenerated each frame via
|
|
/// <see cref="UiText.LinesProvider"/> so the Studio or a live session can push a fresh
|
|
/// <see cref="CharacterSheet"/> without re-binding.
|
|
///
|
|
/// <para>Production data resolves the element as a <see cref="UiText"/>
|
|
/// (Type 12). A synthetic fallback is created only for deliberately reduced
|
|
/// test layouts.</para>
|
|
/// </summary>
|
|
/// <param name="layout">Imported 0x2100006E / 0x10000183 layout tree.</param>
|
|
/// <param name="data">Provider returning the current <see cref="CharacterSheet"/>.</param>
|
|
/// <param name="datFont">Retail dat font forwarded from the render stack. May be null
|
|
/// (falls back to the BitmapFont debug path).</param>
|
|
public static void Bind(
|
|
ImportedLayout layout,
|
|
Func<CharacterSheet> data,
|
|
UiDatFont? datFont = null,
|
|
Action? close = null)
|
|
{
|
|
// End-of-retail data authors m_pMainText under the Character Information
|
|
// child. The fallback exists only so focused controller tests can bind a
|
|
// deliberately reduced tree without fabricating production structure.
|
|
var root = layout.Root;
|
|
if (root is null) return;
|
|
|
|
UiText text = layout.FindElement(MainTextId) as UiText ?? 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,
|
|
};
|
|
if (text.Parent is null)
|
|
root.AddChild(text);
|
|
else if (text.DatFont is null && datFont is not null)
|
|
text.DatFont = datFont;
|
|
text.PreserveEndOnLayout = false;
|
|
if (layout.FindElement(ScrollbarId) is UiScrollbar scrollbar)
|
|
scrollbar.Model = text.Scroll;
|
|
text.LinesProvider = () => BuildReport(data());
|
|
|
|
if (layout.FindElement(CloseId) is UiButton closeButton)
|
|
closeButton.OnClick = close;
|
|
}
|
|
|
|
// ── Report builder ────────────────────────────────────────────────────────
|
|
|
|
/// <summary>
|
|
/// Build the complete character report as a line list.
|
|
/// Order mirrors <c>gmCharacterInfoUI::Update</c> (0x004ba790).
|
|
/// </summary>
|
|
private static IReadOnlyList<UiText.Line> BuildReport(CharacterSheet s)
|
|
{
|
|
var lines = new List<UiText.Line>();
|
|
|
|
// ── 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<UiText.Line> lines, string text)
|
|
=> lines.Add(new UiText.Line(text, HeaderColor));
|
|
|
|
private static void Append(List<UiText.Line> lines, string text)
|
|
=> lines.Add(new UiText.Line(text, BodyColor));
|
|
|
|
private static void AppendBlank(List<UiText.Line> lines)
|
|
=> lines.Add(new UiText.Line(string.Empty, BodyColor));
|
|
}
|