feat(ui): Campaign CT slice CT5 — attribute/skill row geometry + selection media
Aligns the hand-built attribute/skill rows in CharacterStatController with the authored shared row template 0x10000248 (LayoutDesc 0x21000045, InfoRegion::InfoRegion @0x004F1450 template index 0 — the same template gmAttributeUI and gmSkillUI both instantiate): - Row geometry replaced with AUTHORED PIXEL VALUES instead of derived fractions: icon flush left 20x20 (was 16x16 at X=4, vertically centered), name column X=25 W=150 fixed (was RowPadX+IconSize+IconGap offset with a width*0.60 fraction), value column X=175 W=100 right-justified (its right edge sits 7px short of the row's 282px right edge — the authored gutter the owner reported). Row width itself now clamps to the authored 282px template width (RowContentWidth) rather than the ListBox's raw 300px container width. Attribute-row height fixed at 20px (was 22px, no dat basis); SkillRowHeight folded into the same RowHeight constant since both row kinds share H=20. - RowHighlightSprite corrected from 0x06001397 to 0x06000F93 — CT1's ground-truth research sealed the verdict that gmAttributeUI:: UpdateSelection @0x0049DEE0 (SetState(6) -> InfoRegion::SetState @0x004F0EE0) swaps the row's Highlight-state media (0x06000F93), a full-row background swap. 0x06001397 belongs to a different mechanism entirely (the spellbook row's UIElement_UIItem::SetSelectedState overlay child) and SpellbookRowStyle.cs is untouched. - UiClickablePanel.UseSelectionBars/SelectionBarHeight retired outright (UiPanel.cs): they existed only to emulate 0x06001397's dark-bars art; the correct retail rendering is the full-panel sprite stretch the base UiPanel.OnDraw already performs, so the override is dead code once the correct sprite is used. No consumer existed outside CharacterStatController. - Per-attribute/per-vital icon DIDs now resolve through the live DBObj::GetDIDByEnum chain (RetailDataIdResolver.Resolve, AP-235's unification seam) when a resolver is supplied — RetailUiRuntime. MountCharacter wires one under the shared DatLock — falling back to the hardcoded AttrRows/VitalRows column otherwise (tests, no dat). gmAttributeUI::PostInit @0x0049DB70 read verbatim: attributes resolve via category 0x10000002 (statId order 1,2,4,3,5,6, matching AttrRows' authored display order exactly); vitals via category 0x10000003. Live-DAT-verified: every hardcoded fallback value already matched the resolved DID byte-exact (new InstalledDat pin AttributeAndVitalIconDids_MatchTheRetailEnumMapperChain). - RetailAppraisalNameResolver.ResolveHeritage's independent re-implementation of the 2/5/13 heritage overrides deleted; it now delegates straight to CharacterIdentityText.HeritageGroupDisplayName (which already bakes in the same overrides) — one owner, byte-identical behavior. AP-235's register row updated to reflect the single-owner fix (the underlying hardcoded-vs-live-DAT mechanism divergence itself stays open — out of CT5's scope). Hand-built-vs-template ruling: rows stay HAND-BUILT rather than converting to UiTemplateListBox instantiation. The hand-built path hits every authored number byte-exact (proven by the CT1 InstalledDat pin AttributeRowTemplate_IconIsFlushLeftTwentyPixels_NameAndValueAreFixedColumns), while conversion would touch ~15 call sites (raise-button affordability, footer State A/B, per-row tooltip, section bucketing, live-refresh, selection-highlight) for a geometry-only slice — smaller-risk path per the task's own judgment-call guidance. Tests: CharacterStatControllerTests' sprite/UseSelectionBars assertions corrected to the authored geometry; new InstalledDat pin for the icon-DID chain. Full hermetic solution suite green (App/Core/Runtime/Headless/ Launcher/Content/etc., 0 failures) and the full InstalledDat lane green (203 App.Tests pins, TowerAscentReplayTests' known Status=KnownFailure case excluded per the acceptance filter). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
01e44a7018
commit
f532f28c5b
7 changed files with 266 additions and 125 deletions
|
|
@ -1,5 +1,6 @@
|
|||
using System.Numerics;
|
||||
using AcDream.App.UI.Layout;
|
||||
using AcDream.Content;
|
||||
using DatReaderWriter;
|
||||
|
||||
namespace AcDream.App.Tests.UI.Layout;
|
||||
|
|
@ -511,4 +512,55 @@ public sealed class CharacterPanelLiveDatTests
|
|||
Assert.Equal("Player Killer Lite", pkLite);
|
||||
Assert.Equal("Non-Player Killer", npk);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign CT slice CT5 (AP-235): <c>gmAttributeUI::PostInit @0x0049DB70</c>
|
||||
/// resolves each row's icon DID via <c>DBObj::GetDIDByEnum(statEnum, category
|
||||
/// 0x10000002)</c> for the 6 primary attributes (statId order 1,2,4,3,5,6 —
|
||||
/// matching <c>CharacterStatController.AttrRows</c>' authored display order)
|
||||
/// and <c>DBObj::GetDIDByEnum(vitalEnum, category 0x10000003)</c> for the 3
|
||||
/// vitals' <c>Attribute2ndInfoRegion</c> icon (verified live: the max-vital
|
||||
/// enum ids 1/3/5 that <c>CharacterStatController.VitalRows</c> already
|
||||
/// stores resolve to the SAME DID as the current-vital ids 2/4/6 retail's
|
||||
/// own decomp literally passes, so either works). Both categories route
|
||||
/// through the shared <c>RetailDataIdResolver.Resolve</c> master-map ->
|
||||
/// category-map -> value chain — this pin proves every hardcoded fallback
|
||||
/// DID in <c>AttrRows</c>/<c>VitalRows</c> already matches the live-resolved
|
||||
/// value byte-exact, the same "regression guard, not a bug pin" pattern as
|
||||
/// <see cref="SkillSectionHeaderTemplates_MatchExistingSpriteConstants"/>.
|
||||
/// </summary>
|
||||
[InstalledDatFact]
|
||||
public void AttributeAndVitalIconDids_MatchTheRetailEnumMapperChain()
|
||||
{
|
||||
using var dats = new DatCollection(DatDirectory, DatReaderWriter.Options.DatAccessType.Read);
|
||||
|
||||
const uint attributeIconCategory = 0x10000002u;
|
||||
(uint statId, uint expectedDid)[] attributes =
|
||||
{
|
||||
(1u, 0x060002C8u), // Strength
|
||||
(2u, 0x060002C4u), // Endurance
|
||||
(4u, 0x060002C9u), // Coordination
|
||||
(3u, 0x060002C6u), // Quickness
|
||||
(5u, 0x060002C5u), // Focus
|
||||
(6u, 0x060002C7u), // Self
|
||||
};
|
||||
foreach (var (statId, expectedDid) in attributes)
|
||||
{
|
||||
uint resolved = RetailDataIdResolver.Resolve(dats, statId, attributeIconCategory);
|
||||
Assert.Equal(expectedDid, resolved);
|
||||
}
|
||||
|
||||
const uint vitalIconCategory = 0x10000003u;
|
||||
(uint maxStatId, uint expectedDid)[] vitals =
|
||||
{
|
||||
(1u, 0x06004C3Bu), // Health
|
||||
(3u, 0x06004C3Cu), // Stamina
|
||||
(5u, 0x06004C3Du), // Mana
|
||||
};
|
||||
foreach (var (maxStatId, expectedDid) in vitals)
|
||||
{
|
||||
uint resolved = RetailDataIdResolver.Resolve(dats, maxStatId, vitalIconCategory);
|
||||
Assert.Equal(expectedDid, resolved);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue