fix(player): EnchantmentMask bit fix + Vitae key=0 + absolute Vitals overlay

Three fixes to the Vitals HUD path:

1. EnchantmentMask Vitae/Cooldown bit values (parser regression).
   ACE's enum at references/ACE/Source/ACE.Entity/Enum/EnchantmentCategory.cs
   has Vitae=0x4 and Cooldown=0x8. I had them swapped — when ACE wrote
   the Vitae singleton with mask bit 0x4 set, my parser read it as
   "Cooldown" and tried to consume a count-prefixed list (no count
   present), blowing up with FormatException, returning null from
   TryParse. PlayerDescription consequently failed to parse on every
   live login. Fix: swap the bit values + bucket constants to match ACE.

2. Vitae applies regardless of StatModKey. Live trace showed:
     vitals: PD-ench spell=666 layer=0 bucket=Vitae key=0 val=0.95
   ACE's Vitae enchantment serializes with key=0 (meaning "any vital")
   per retail. EnchantmentMath was filtering Vitae by key like other
   buffs, so the 5% death penalty never applied to Health/Stam/Mana
   max — the Vitals percent read 95% because current=276 / max=290
   (server already reduced current; our max didn't match). Fix:
   Vitae bucket short-circuits the per-key check and applies its
   multiplier to all vitals.

3. Absolute current/max in HUD overlay. VitalsVM exposes
   HealthCurrent/Max, StaminaCurrent/Max, ManaCurrent/Max from
   LocalPlayerState. VitalsPanel overlay format is now
   "current / max (percent%)" when absolutes are available; falls
   back to percent-only pre-PlayerDescription. Matches the retail
   look the user requested ("HP 400/400" style).

Test deltas (841 -> 842):
  - Existing Vitae test still passes (key matches statKey case).
  - New Vitae key=0 test pins the "any vital" semantics.
  - Existing PlayerDescription Vitae singleton test updated to
    write mask=0x4 (was 0x8 with the swapped enum).

Live verification: with +Acdream's Vitae-666 active and Endurance.current=290:
  HP   : current=138, max=145×0.95≈138 → bar 100% (was 95%)
  Stam : current=276, max=290×0.95≈276 → bar 100%
  Mana : current=190, max=200×0.95≈190 → bar 100%
Overlay reads e.g. "276 / 276 (100%)".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Erik 2026-04-25 18:15:20 +02:00
parent bb5003a849
commit 196f883c10
6 changed files with 99 additions and 19 deletions

View file

@ -181,6 +181,25 @@ public sealed class EnchantmentMathTests
Assert.Equal(10.0f, mod.Additive);
}
[Fact]
public void GetMod_Vitae_AppliesEvenWhenStatModKeyIsZero()
{
// Retail behaviour observed in live trace (2026-04-25): ACE's
// Vitae enchantment serializes with StatModKey = 0 (meaning
// "any vital"). The Vitae multiplier must apply regardless of
// the requested stat key — otherwise +Acdream's 5% death
// penalty wouldn't show up in the Vitals HUD percent.
var table = LoadTable((666u, "Vitae", 0u));
var enchantments = new[]
{
MakeVitaeRecord(spellId: 666, layer: 0, statKey: 0u /* "any" */, val: 0.95f),
};
// Query for MaxStamina; Vitae key=0 should still apply.
var mod = EnchantmentMath.GetMod(enchantments, table,
EnchantmentMath.StatKey.MaxStamina);
Assert.Equal(0.95f, mod.Multiplier, precision: 3);
}
[Fact]
public void GetMod_FamilyStacking_PicksHigherSpellId()
{
@ -207,7 +226,7 @@ public sealed class EnchantmentMathTests
new(spellId, layer, 60f, 0u, StatModType: 0, StatModKey: statKey, StatModValue: val, Bucket: 2u);
private static ActiveEnchantmentRecord MakeVitaeRecord(uint spellId, uint layer, uint statKey, float val) =>
new(spellId, layer, -1f, 0u, StatModType: 0, StatModKey: statKey, StatModValue: val, Bucket: 8u);
new(spellId, layer, -1f, 0u, StatModType: 0, StatModKey: statKey, StatModValue: val, Bucket: 4u);
private static SpellTable LoadTable(params (uint id, string name, uint family)[] rows)
{