fix(ui): #267 character panel reflects vitae/buffed skills and attributes

Retail CACQualities::EnchantAttribute (0x00594570), EnchantAttribute2nd
(0x00594670, already ported for #6), and EnchantSkill (0x005947b0) are the
three enchantment-composition functions the Character window's Attributes
and Skills tabs depend on. Primary attributes never reference the vitae
singleton in retail (only Attribute2nd/Skill do) — confirmed directly from
the decompiled function bodies, not assumed.

EnchantmentMath.GetMod gains requiredType/includeVitae parameters (default
to the prior behavior) so a numeric StatMod key collision across domains
(e.g. key=1 is both Strength and MaxHealth) can't leak a buff into the
wrong computation. Spellbook.GetAttributeMod/GetSkillMod and
LocalPlayerState.GetEffectiveAttribute/GetEffectiveSkill/
GetSkillVitaeModifier wire the retail chain through to the panel.
CharacterSheetProvider now reports the effective value as the main number
and CharacterSkill.CurrentLevel is no longer an alias of BaseLevel (this
also activates the previously-dead SkillValueColor buffed/debuffed row
coloring). CharacterStatController's footer-title parenthetical is cited
from gmAttributeUI::DisplaySelectionFooter_Attribute (0x0049d280) and
gmSkillUI::DisplaySelectionFooter_Trained (0x0049b860) +
SkillInfoRegion::GetVitaeModifier (0x004f0fa0): skills show up to two
segments (vitae's own contribution, then the buff-only residual), while
vitae-immune attributes show at most one; no parenthetical when the delta
is zero. The panel now refreshes on Spellbook.EnchantmentsChanged, not only
raw property/attribute updates.

Core goldens cover the user-reported 33% vitae example (303->203, "(-100)"
exactly), buff+vitae composition, and the attribute vitae-immunity finding.
Provider/controller tests cover the full row-click -> footer-title path and
live refresh. Full solution suite passes with zero failures.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Erik 2026-07-30 18:08:55 +02:00
parent f6275f4501
commit cf2605fa4a
11 changed files with 1089 additions and 53 deletions

View file

@ -92,6 +92,105 @@ Copy this block when adding a new issue:
---
## #267 — Vitae does not update the character panel's skills/attributes display
**Status:** DONE (implementation) — 2026-07-30; visual confirmation with the user pending
**Severity:** MEDIUM
**Filed:** 2026-07-30
**Component:** retained UI / character window / vitae
**Symptom (user report):** with 5% vitae active, the skills and attributes
values in the character window did not change; retail shows the CURRENT
(vitae-reduced) value and, on click/detail, the current level with the
vitae reduction in parentheses (e.g. "(-100)"). The P1 movement chain
consumes vitae correctly (EnchantSkill for run/jump); the character
window presentation did not.
**Scoping (2026-07-30):** confirmed — `CharacterSheetProvider`/
`CharacterStatController` contained zero vitae/enchantment references; the
panel rendered base property values only. `LocalPlayerState.AttributeSnapshot.Current`
(Ranks+Start) and `SkillSnapshot.CurrentLevel` (an alias of `BaseLevel`) never
consulted the wired `Spellbook`.
**Root cause / resolution:** Ported retail's three enchantment-composition
functions from the named decomp:
`CEnchantmentRegistry::EnchantAttribute` (0x00594570 — primary attributes;
mult/add buff lists only, floor at 1/10 by base<10, truncate),
`CEnchantmentRegistry::EnchantAttribute2nd` (0x00594670 — vitals, already
ported for issue #6), and `CEnchantmentRegistry::EnchantSkill` (0x005947b0 —
skills; vitae singleton applied first, then mult/add lists, zero-floor at
<0.5, truncate). **Primary attributes are vitae-immune in retail**
`EnchantAttribute` never references the `_vitae` singleton, unlike its
Attribute2nd/Skill siblings; this is confirmed directly from the decompiled
function bodies, not assumed.
`EnchantmentMath.GetMod` gained a `requiredType` (`EnchantmentTypeFlag`:
Attribute=1, SecondAtt=2, Skill=0x10, matching ACE's
`EnchantmentTypeFlags`) and `includeVitae` parameter so a numeric key
collision across domains (e.g. key=1 is both Strength and MaxHealth) can't
leak a buff across domains; both default to the prior no-filter/vitae-always
behavior so the existing vitals path (#6) is untouched.
`Spellbook.GetAttributeMod`/`GetSkillMod` are new cached accessors mirroring
`GetVitalMod`. `LocalPlayerState.GetEffectiveAttribute`/`GetEffectiveSkill`/
`GetSkillVitaeModifier` compose the final int, falling back to the
unenchanted base when no spellbook is wired.
`CharacterSheetProvider.BuildSheet()` now reports the effective (buffed)
value as the panel's main number for all 6 attributes and every skill's
`CharacterSkill.CurrentLevel` (previously an alias of `BaseLevel` — this also
activates the pre-existing but previously-dead
`CharacterStatController.SkillValueColor` buffed/debuffed row coloring).
`CharacterSheet.AttributeBaseValues` and `CharacterSkill.VitaeModifier` carry
the extra state the footer-title delta needs.
The click/detail parenthetical format is cited from
`gmAttributeUI::DisplaySelectionFooter_Attribute` (0x0049d280, format
`"%s: %d"` + `" (%s%d)"`) and `gmSkillUI::DisplaySelectionFooter_Trained`
(0x0049b860, plus `SkillInfoRegion::GetVitaeModifier` 0x004f0fa0 for the
isolated vitae segment `" (%d)"`). Retail skills show up to TWO
parentheticals — vitae's own contribution (always ≤0, e.g. "(-100)") and the
buff-only residual excluding vitae — while attributes (vitae-immune) show at
most one. No parenthetical appears when the relevant delta is zero; an
increase gets an explicit "+" the natural `%d` doesn't provide, a decrease
uses the natural minus sign. Retail colors each segment independently via
`AppendTextWithFont` state 1/2/3; the current `UiText` footer-title element is
`OneLine` (single color, single run), so segments share the title's white
color rather than retail's per-run tint — a presentation simplification, not
a value/format deviation.
Live refresh: `CharacterSheetProvider`'s existing `SubscribeChanged` binding
now also subscribes to `LocalPlayerState.Spellbook.EnchantmentsChanged` (a
newly-exposed read-only property mirroring the private reference
`LocalPlayerState` already held for issue #6), so the panel repaints when
vitae is applied/removed or a buff is cast/expires, not only on raw
property/attribute updates.
**Files:** `src/AcDream.Core/Spells/EnchantmentMath.cs`;
`src/AcDream.Core/Spells/Spellbook.cs`;
`src/AcDream.Core/Player/LocalPlayerState.cs`;
`src/AcDream.App/UI/Layout/CharacterSheet.cs`;
`src/AcDream.App/UI/Layout/CharacterSheetProvider.cs`;
`src/AcDream.App/UI/Layout/CharacterStatController.cs`.
**Research:** `docs/research/named-retail/acclient_2013_pseudo_c.txt` lines
416110 (`EnchantAttribute`), 416169 (`EnchantAttribute2nd`), 416240
(`EnchantSkill`), 167258 (`gmSkillUI::DisplaySelectionFooter_Trained`),
168660 (`gmAttributeUI::DisplaySelectionFooter_Attribute`), 244033
(`SkillInfoRegion::GetVitaeModifier`);
`references/ACE/Source/ACE.Entity/Enum/EnchantmentTypeFlags.cs`.
**Acceptance:** Core golden tests cover 33% vitae on a known skill base
(303→203, matching the user's exact "(-100)" example), buff+vitae
composition, zero-delta no-parenthetical, and the attribute vitae-immunity
finding. `CharacterStatControllerTests` covers the full row-click →
footer-title text through `CharacterStatController.Bind` for both domains.
`CharacterSheetProviderTests` covers `BuildSheet()` effective-value output
and `SubscribeChanged` firing (and rebuilding to the new value) on
`EnchantmentsChanged`. `dotnet build AcDream.slnx -c Release` and the
complete solution test suite pass with zero failures. **Final closure pends
the user's live visual check** (5%/33% vitae against a live ACE session) per
the standard M4 UI-accuracy gate.
---
## #247 — Loot ordering and local dropped-item projection regressed
**Status:** DONE — 2026-07-26; connected loot/drop/selection gate passed