fix(studio): Character window — bind the REAL Attributes-tab elements (no guessing)
Redo of the character pilot per faithful-port rules. The previous pilot GUESSED a text report and put it on the wrong window. The decomp (verified by dumping acdream's importer-resolved tree) shows LayoutDesc 0x2100002E is the tabbed Attributes/Skills/Titles window: its tab-content slot 0x1000022B mounts sub-layout 0x2100002C (gmAttributeUI) which chains into the gmStatManagementUI header. The importer ALREADY mounts every content element (FindElement resolves name 0x10000231, heritage 0x10000232, PK 0x10000233, level 0x1000023B, total-XP 0x10000235, XP meter 0x10000236 → UiMeter, list box 0x1000023D) — EventId was a red herring (the dat id lives in _byId, not the widget's EventId field). CharacterStatController (port of gmStatManagementUI::UpdateCharacterInfo 0x004f0770 + UpdateExperience 0x004f0a70 + UpdatePKStatus 0x004f00a0) binds those real elements: name, heritage, PK status, level, total XP, the XP-to-level meter fill, and the six innate attributes in the list box. Studio (--layout 0x2100002E) now renders the actual panel content, not an overlay. 16 controller tests green; full App suite stays green. Refinements with known decomp sources (follow-ups): tab-button active/inactive state so the 3 tabs draw their sprites; exact label wording from the StringTable (table 0x10000001 → dat 0x31000001); the full AttributeInfoRegion row template (column-aligned values + raise buttons). CharacterController (text-report 0x2100001A) retained for that separate sub-panel. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
33693c6412
commit
0e644b5887
5 changed files with 250 additions and 2 deletions
|
|
@ -33,6 +33,25 @@ public sealed class CharacterSheet
|
|||
/// <summary>Title string, e.g. "the Adventurer". Null = omit.</summary>
|
||||
public string? Title { get; init; }
|
||||
|
||||
// ── Experience / PK (gmStatManagementUI::UpdateExperience 0x004f0a70,
|
||||
// UpdatePKStatus 0x004f00a0) — the Attributes-tab header strip ──────────
|
||||
|
||||
/// <summary>Total accrued experience (retail PropertyInt64 1). Header value
|
||||
/// element 0x10000235 (m_pTotalXPText).</summary>
|
||||
public long TotalXp { get; init; }
|
||||
|
||||
/// <summary>Experience remaining to the next level. Header value element
|
||||
/// 0x10000238 (m_pXPToLevelText).</summary>
|
||||
public long XpToNextLevel { get; init; }
|
||||
|
||||
/// <summary>XP-to-next-level meter fill, 0..1 (retail (cur−base)/(cap−base)).
|
||||
/// Drives the header meter 0x10000236 (m_pXPToLevelMeter).</summary>
|
||||
public float XpFraction { get; init; }
|
||||
|
||||
/// <summary>PK status display string, e.g. "Non-Player Killer". Header element
|
||||
/// 0x10000233 (m_pPKStatusText). Null = omit.</summary>
|
||||
public string? PkStatus { get; init; }
|
||||
|
||||
// ── Birth / age / deaths (UpdatePlayerBirthAgeDeaths 0x004b8cb0) ─────────
|
||||
|
||||
/// <summary>Formatted birth date string (retail InqInt(0x62) → strftime).
|
||||
|
|
|
|||
113
src/AcDream.App/UI/Layout/CharacterStatController.cs
Normal file
113
src/AcDream.App/UI/Layout/CharacterStatController.cs
Normal file
|
|
@ -0,0 +1,113 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Numerics;
|
||||
using AcDream.App.UI;
|
||||
|
||||
namespace AcDream.App.UI.Layout;
|
||||
|
||||
/// <summary>
|
||||
/// Controller for the Character window's <b>Attributes tab</b> — LayoutDesc 0x2100002E,
|
||||
/// whose tab-content slot 0x1000022B mounts sub-layout 0x2100002C (gmAttributeUI, root
|
||||
/// type 0x1000002A) which in turn chains into the gmStatManagementUI header content.
|
||||
///
|
||||
/// <para>Unlike <see cref="CharacterController"/> (which targets the SEPARATE text-report
|
||||
/// sub-panel 0x2100001A, gmCharacterInfoUI, by creating its runtime m_pMainText element),
|
||||
/// this controller binds the <b>real, statically-mounted</b> header + list elements that the
|
||||
/// importer already produces — every id below is confirmed present via
|
||||
/// <see cref="ImportedLayout.FindElement"/>.</para>
|
||||
///
|
||||
/// <para>Ported from <c>gmStatManagementUI::UpdateCharacterInfo</c> (0x004f0770) +
|
||||
/// <c>UpdateExperience</c> (0x004f0a70) + <c>UpdatePKStatus</c> (0x004f00a0): name, heritage,
|
||||
/// PK status, level, total XP and the XP-to-level meter. The attribute list is the
|
||||
/// <c>gmAttributeUI</c> list box (0x1000023D).</para>
|
||||
///
|
||||
/// <para><b>Refinement pending:</b> exact label wording comes from the dat StringTable
|
||||
/// (table enum 0x10000001 → dat 0x31000001) — not yet ported, so labels use the canonical
|
||||
/// AC text. Column alignment + per-row raise buttons (the full AttributeInfoRegion row
|
||||
/// template) are a later pass; this binds values into the real element rects first.</para>
|
||||
/// </summary>
|
||||
public static class CharacterStatController
|
||||
{
|
||||
// ── gmStatManagementUI header element ids (sub-layout 0x2100002C content) ──
|
||||
public const uint NameId = 0x10000231u; // m_pNameText
|
||||
public const uint HeritageId = 0x10000232u; // m_pHeritageText
|
||||
public const uint PkStatusId = 0x10000233u; // m_pPKStatusText
|
||||
public const uint LevelId = 0x1000023Bu; // m_pLevelText (right-side level area)
|
||||
public const uint TotalXpId = 0x10000235u; // m_pTotalXPText
|
||||
public const uint XpMeterId = 0x10000236u; // m_pXPToLevelMeter (UiMeter)
|
||||
public const uint ListBoxId = 0x1000023Du; // m_pListBox container
|
||||
|
||||
private static readonly Vector4 Body = new(0.92f, 0.90f, 0.82f, 1f); // parchment-white body text
|
||||
private static readonly Vector4 Gold = new(1f, 0.82f, 0.36f, 1f); // section / emphasis gold
|
||||
|
||||
/// <summary>
|
||||
/// Bind the Attributes-tab header + list elements in <paramref name="layout"/> to
|
||||
/// <paramref name="data"/>. Each element is the real importer-mounted widget; the
|
||||
/// providers are polled each frame so a live session or the Studio can swap the sheet
|
||||
/// without re-binding.
|
||||
/// </summary>
|
||||
public static void Bind(ImportedLayout layout, Func<CharacterSheet> data, UiDatFont? datFont = null)
|
||||
{
|
||||
Label(layout, NameId, datFont, Gold, () => data().Name);
|
||||
Label(layout, HeritageId, datFont, Body, () => data().Heritage ?? data().Race ?? string.Empty);
|
||||
Label(layout, PkStatusId, datFont, Body, () => data().PkStatus ?? string.Empty);
|
||||
Label(layout, LevelId, datFont, Gold, () => data().Level.ToString());
|
||||
Label(layout, TotalXpId, datFont, Body, () => data().TotalXp.ToString("N0"));
|
||||
|
||||
// XP-to-level meter fill (gmStatManagementUI::UpdateExperience 0x004f0a70).
|
||||
if (layout.FindElement(XpMeterId) is UiMeter meter)
|
||||
meter.Fill = () => data().XpFraction;
|
||||
|
||||
// Attribute list (gmAttributeUI list box 0x1000023D). The list box is a generic
|
||||
// container in the import, so attach a single text view sized to the rows and pinned
|
||||
// to the top of the list region. (The full per-row AttributeInfoRegion widget is a
|
||||
// later pass; this renders the six innate attributes + values faithfully.)
|
||||
if (layout.FindElement(ListBoxId) is { } list)
|
||||
{
|
||||
var rows = new UiText
|
||||
{
|
||||
DatFont = datFont,
|
||||
ClickThrough = true,
|
||||
Left = 12f,
|
||||
Top = 10f,
|
||||
Width = MathF.Max(60f, list.Width - 24f),
|
||||
Height = 9f * LineHeight, // sized to the 8 content lines so it top-aligns
|
||||
};
|
||||
rows.LinesProvider = () => AttributeRows(data());
|
||||
list.AddChild(rows);
|
||||
}
|
||||
}
|
||||
|
||||
private const float LineHeight = 18f;
|
||||
|
||||
private static void Label(ImportedLayout layout, uint id, UiDatFont? datFont, Vector4 color, Func<string> text)
|
||||
{
|
||||
if (layout.FindElement(id) is UiText t)
|
||||
{
|
||||
t.DatFont = datFont;
|
||||
t.Centered = true; // single-line centered label (retail UIElement_Text)
|
||||
t.ClickThrough = true;
|
||||
t.LinesProvider = () => new[] { new UiText.Line(text(), color) };
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The six innate attributes in canonical AC panel order
|
||||
/// (Strength, Endurance, Coordination, Quickness, Focus, Self), preceded by a gold header.
|
||||
/// </summary>
|
||||
private static IReadOnlyList<UiText.Line> AttributeRows(CharacterSheet s)
|
||||
=> new List<UiText.Line>
|
||||
{
|
||||
new("Attributes", Gold),
|
||||
new(string.Empty, Body),
|
||||
Row("Strength", s.Strength),
|
||||
Row("Endurance", s.Endurance),
|
||||
Row("Coordination", s.Coordination),
|
||||
Row("Quickness", s.Quickness),
|
||||
Row("Focus", s.Focus),
|
||||
Row("Self", s.Self),
|
||||
};
|
||||
|
||||
private static UiText.Line Row(string name, int value)
|
||||
=> new($"{name,-13}{value,4}", Body);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue