fix(ui): match retail vitae and character info

Restore Vitae's omitted penalty paragraph, replace the invented character summary with gmCharacterInfoUI's ordered report and property meanings, preserve authored translucent body surfaces, and initialize the end-session button in its visible Normal DAT state.

Release build and all 5,830 tests pass with five intentional skips. Connected visual gate pending.

Co-authored-by: OpenAI Codex <codex@openai.com>
This commit is contained in:
Erik 2026-07-17 12:06:14 +02:00
parent d1d603105f
commit 16c21e299c
21 changed files with 771 additions and 303 deletions

View file

@ -31,12 +31,10 @@ public sealed class CharacterSheetProvider
private const uint UnassignedXpPropertyId = 2u;
/// <summary>
/// Skill-credit debit fallback chain, most-specific first. Retail reads
/// InqInt(0x18) for the Attributes-tab footer, InqInt(0xc0) for available
/// and InqInt(0xb5) for total skill credits (see CharacterSheet docs);
/// the live server population varies, so debit whichever is present.
/// Retail PropertyInt 0x18 = available skill credits. Properties 0xB5 and
/// 0xC0 are Chess Rank and Fishing Skill and must never be debited.
/// </summary>
private static readonly uint[] SkillCreditPropertyIds = { 0x18u, 0xC0u, 0xB5u };
private static readonly uint[] SkillCreditPropertyIds = { 0x18u };
private readonly ClientObjectTable _objects;
private readonly LocalPlayerState _localPlayer;
@ -109,7 +107,7 @@ public sealed class CharacterSheetProvider
long unassignedXp = props.GetInt64(UnassignedXpPropertyId);
var xp = ComputeLevelXp(level, totalXp);
int skillCredits = props.GetInt(0x18u, props.GetInt(0xC0u, props.GetInt(0xB5u)));
int skillCredits = props.GetInt(0x18u);
return new CharacterSheet
{
@ -138,8 +136,17 @@ public sealed class CharacterSheetProvider
Focus = AttrCurrent(LocalPlayerState.AttributeKind.Focus),
Self = AttrCurrent(LocalPlayerState.AttributeKind.Self),
UnspentSkillCredits = props.GetInt(0xB5u, skillCredits),
SpecializedSkillCredits = props.GetInt(0xC0u),
UnspentSkillCredits = skillCredits,
SpecializedSkillCredits = 0,
ChessRank = props.GetInt(0xB5u),
FishingSkill = props.GetInt(0xC0u),
BirthTimestamp = props.Ints.TryGetValue(0x62u, out int born)
? born
: null,
TotalPlayTimeSeconds = props.Ints.TryGetValue(0x7Du, out int played)
? played
: null,
Deaths = props.GetInt(0x2Bu),
SkillCredits = skillCredits,
UnassignedXp = unassignedXp,
AttributeRaiseCosts = BuildAttributeRaiseCosts(amount: 1),
@ -147,6 +154,8 @@ public sealed class CharacterSheetProvider
Skills = BuildLiveCharacterSkills(),
BurdenCurrent = props.GetInt(5u),
BurdenMax = props.GetInt(96u),
EncumbranceAugmentations = props.GetInt(0xE6u),
CharacterInfoProperties = new Dictionary<uint, int>(props.Ints),
};
}