231 lines
10 KiB
C#
231 lines
10 KiB
C#
using System;
|
||
using System.Collections.Generic;
|
||
|
||
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>Gender display string, e.g. "Female". Null = omit.</summary>
|
||
public string? Gender { get; init; }
|
||
|
||
/// <summary>Race string, e.g. "Aluvian". Null = omit.</summary>
|
||
public string? Race { get; init; }
|
||
|
||
/// <summary>Heritage group display string, e.g. "Aluvian". Null = omit.</summary>
|
||
public string? Heritage { get; init; }
|
||
|
||
/// <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).
|
||
/// 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; }
|
||
|
||
/// <summary>Raw retail PropertyInt 0x62. Null means the quality was absent.</summary>
|
||
public int? BirthTimestamp { get; init; }
|
||
|
||
/// <summary>Raw retail PropertyInt 0x7D seconds. Null means the quality was absent.</summary>
|
||
public int? TotalPlayTimeSeconds { 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; }
|
||
|
||
/// <summary>
|
||
/// Unenchanted max Health/Stamina/Mana in that order.
|
||
/// </summary>
|
||
public int[] VitalBaseMaxValues { get; init; } = Array.Empty<int>();
|
||
|
||
/// <summary>
|
||
/// Isolated vitae contribution to max Health/Stamina/Mana, always
|
||
/// non-positive and ordered like <see cref="VitalBaseMaxValues"/>.
|
||
/// </summary>
|
||
public int[] VitalVitaeModifiers { get; init; } = Array.Empty<int>();
|
||
|
||
// ── Innate attributes (UpdateInnateAttributeInfo 0x004b87e0) ────────────
|
||
// InqAttribute order: 1,2,4,3,5,6 = Strength, Endurance, Quickness, Coordination, Focus, Self.
|
||
|
||
// Issue #267: these are the EFFECTIVE (post-buff) values — retail
|
||
// CACQualities::EnchantAttribute (0x00594570). Primary attributes are
|
||
// vitae-immune in retail; only active buffs move this number away from
|
||
// AttributeBaseValues below.
|
||
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; }
|
||
|
||
/// <summary>
|
||
/// Unenchanted base value for each of the 6 primary attributes, in
|
||
/// POSITIONAL order matching <c>CharacterStatController.AttrRows</c>
|
||
/// (Strength, Endurance, Coordination, Quickness, Focus, Self — note
|
||
/// this differs from the individual-property declaration order above).
|
||
/// Issue #267: paired with the effective values above to compute the
|
||
/// retail footer-title delta parenthetical
|
||
/// (<c>gmAttributeUI::DisplaySelectionFooter_Attribute</c> 0x0049d280).
|
||
/// </summary>
|
||
public int[] AttributeBaseValues { get; init; } = Array.Empty<int>();
|
||
|
||
// ── Skills (UpdateFakeSkills 0x004b8930) ────────────────────────────────
|
||
// Character Information uses 0xB5/0xC0 for Chess/Fishing; skill credits use 0x18.
|
||
// InqInt(0x18) = available skill credits — footer 0x10000245 in the Attributes tab.
|
||
|
||
public int UnspentSkillCredits { get; init; }
|
||
public int SpecializedSkillCredits { get; init; }
|
||
|
||
/// <summary>Retail Character Information PropertyInt 0xB5.</summary>
|
||
public int ChessRank { get; init; }
|
||
|
||
/// <summary>Retail Character Information PropertyInt 0xC0.</summary>
|
||
public int FishingSkill { get; init; }
|
||
|
||
/// <summary>
|
||
/// Available (unspent) skill credits shown in the Attributes tab footer State-A.
|
||
/// Retail InqInt(0x18) — gmStatManagementUI::DisplayDefaultFooter (0x0049cde0).
|
||
/// Element 0x10000243 (footer line-1 value in the studio's 3-line layout).
|
||
/// </summary>
|
||
public int SkillCredits { get; init; }
|
||
|
||
/// <summary>
|
||
/// Unassigned (banked) experience points.
|
||
/// Retail InqInt64(2) — shown in footer line-2 in State-A display.
|
||
/// Element 0x10000245 (footer line-2 value).
|
||
/// </summary>
|
||
public long UnassignedXp { get; init; }
|
||
|
||
// ── Attribute raise costs (ExperienceToAttributeLevel, gmAttributeUI::PostInit) ──
|
||
// Retail formula for x1: ExperienceToAttributeLevel(value + 1) − xpSpent.
|
||
// Retail formula for x10: ExperienceToAttributeLevel(value + min(10, remaining)) − xpSpent.
|
||
// Cost 0 means the attribute is at max or not trainable. Ordered to match AttrRows:
|
||
// Strength, Endurance, Coordination, Quickness, Focus, Self, Health, Stamina, Mana.
|
||
// Source: gmAttributeUI::GetCostToRaise/GetCostToRaise10 (0x0049cb80/0x0049cc70).
|
||
|
||
/// <summary>
|
||
/// XP cost to raise each of the 9 attributes/vitals by 1, in retail display order:
|
||
/// [0]=Strength, [1]=Endurance, [2]=Coordination, [3]=Quickness, [4]=Focus, [5]=Self,
|
||
/// [6]=Health, [7]=Stamina, [8]=Mana.
|
||
/// Cost 0 means the attribute is at max (raise button ghosted).
|
||
/// </summary>
|
||
public long[] AttributeRaiseCosts { get; init; } = Array.Empty<long>();
|
||
|
||
/// <summary>
|
||
/// XP cost to raise each of the 9 attributes/vitals by up to 10 retail steps,
|
||
/// in the same display order as <see cref="AttributeRaiseCosts"/>.
|
||
/// </summary>
|
||
public long[] AttributeRaise10Costs { get; init; } = Array.Empty<long>();
|
||
|
||
/// <summary>
|
||
/// Skills shown on the Character window Skills tab. Retail gmSkillUI groups these by
|
||
/// advancement class, then sorts each group alphabetically by skill name.
|
||
/// </summary>
|
||
public IReadOnlyList<CharacterSkill> Skills { get; init; } = Array.Empty<CharacterSkill>();
|
||
|
||
// ── 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; }
|
||
|
||
/// <summary>
|
||
/// Raw PropertyInt snapshot used by retail's data-driven mastery,
|
||
/// luminance, and augmentation report. The controller reads only the
|
||
/// quality ids cited by <c>gmCharacterInfoUI::UpdateAugmentations</c>.
|
||
/// </summary>
|
||
public IReadOnlyDictionary<uint, int> CharacterInfoProperties { get; init; }
|
||
= new Dictionary<uint, int>();
|
||
|
||
// ── Burden / load (UpdateLoad 0x004b8a20) ───────────────────────────────
|
||
// Retail InqLoad + EncumbranceCapacity(Strength, AugEncumbrance).
|
||
|
||
public int BurdenCurrent { get; init; }
|
||
public int BurdenMax { get; init; }
|
||
public int EncumbranceAugmentations { get; init; }
|
||
}
|
||
|
||
public enum CharacterSkillAdvancementClass
|
||
{
|
||
Inactive = 0,
|
||
Untrained = 1,
|
||
Trained = 2,
|
||
Specialized = 3,
|
||
}
|
||
|
||
public sealed record CharacterSkill(
|
||
uint Id,
|
||
string Name,
|
||
uint IconDid,
|
||
CharacterSkillAdvancementClass AdvancementClass,
|
||
int BaseLevel,
|
||
// BaseLevel is retail's pre-EnchantSkill value, including augmentation
|
||
// terms. CurrentLevel is the EFFECTIVE (vitae + buff) level —
|
||
// retail CACQualities::EnchantSkill (0x005947b0). Previously an alias of
|
||
// BaseLevel; this activates the existing CharacterStatController.
|
||
// SkillValueColor buffed/debuffed row coloring.
|
||
int CurrentLevel,
|
||
bool UsableUntrained,
|
||
int TrainedCost,
|
||
int SpecializedCost,
|
||
long RaiseCost,
|
||
long Raise10Cost = 0L,
|
||
// Issue #267: vitae's isolated contribution to CurrentLevel (always ≤ 0),
|
||
// retail SkillInfoRegion::GetVitaeModifier (0x004f0fa0). Used for the
|
||
// footer-title vitae-specific parenthetical, separate from the buff delta
|
||
// (CurrentLevel − VitaeModifier − BaseLevel).
|
||
int VitaeModifier = 0);
|