fix(chargen): Campaign CC gate round 1 closeout — Group 2: Skills page four-bucket model
Ports the last remaining half of retail's Skills page: the four-bucket sorted skill list (Specialized/Trained/UseableUntrained/UnuseableUntrained, UpdateSkillEntry's own iMinlevel <= 1 test), plus the info box's description + formula completion. - ChargenSkillDetail/ChargenSkillFormula (Core) thread SkillBase.MinLevel/ Description/Formula from the global SkillTable, exposed via a new ChargenOptions.TryGetSkillDetail (nullable-with-default parameter, so every pre-existing ChargenOptions call site compiles unchanged). ChargenTableReader.Project populates it from the same SkillTable loop that already builds GlobalSkillCostsBySkillId. - CharacterCreationSkillsPage.RebuildRows now groups every costable skill into SkillBucket, sorts each bucket alphabetically by name (InsertEntrySorted's wcscmp, ported as string.CompareOrdinal), and builds one Templates[0] header row per bucket ahead of that bucket's Templates[1] skill rows — DoSkillRecords' own unconditional 4-header-then-populate order. A level change re-buckets the row (detected per-refresh against each row's own cached bucket, then a full rebuild with the current selection explicitly preserved). - RefreshInfoBox now composes description (word-wrapped via DatRichText.Compose) + the level-gated bonus line (an exact, unwrapped literal — NOT routed through word-wrap, which would have collapsed its authored double-space formatting) + ComposeFormula's "Formula : ..." line (MakeSkillFormula ported with high confidence for the prefix/ per-attribute-term/divisor/bonus-suffix shape; the two-attribute connector text is a disclosed approximation, register AP-231, since the decompiled function's own connector literals could not be recovered byte-exact by this session's static-only tooling). Register: AP-213 RETIRED (160 active rows). Live-DAT gate: the installed SkillTable's MinLevel distribution matches the investigation's own recorded finding exactly (38 entries, 23 useable-untrained / 15 trained-required). 3 new fixture tests + 1 new live-DAT test; 3 pre-existing integration tests fixed (they captured row widget references before a bucket-changing click, which now rebuilds and discards those references — a real, correct consequence of the new model, not a bug). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
e1d7d09599
commit
0fed5fdd91
6 changed files with 712 additions and 121 deletions
|
|
@ -277,6 +277,54 @@ public sealed class ChargenTableReaderInstalledDatTests
|
|||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Campaign CC gate round 1 closeout (Group 2): pins
|
||||
/// <see cref="ChargenOptions.GlobalSkillDetailsBySkillId"/>'s real
|
||||
/// shape against the installed DAT — same 38-entry population as
|
||||
/// <see cref="ChargenOptions.GlobalSkillCostsBySkillId"/> (both read the
|
||||
/// SAME SkillTable loop), MinLevel distributed 23 at <c><= 1</c>
|
||||
/// (useable while Untrained) / 15 at exactly <c>2</c> (Trained
|
||||
/// required), per the Batch F investigation's own recorded finding, and
|
||||
/// never above 2 — <c>UpdateSkillEntry</c>'s own bucket test only ever
|
||||
/// distinguishes <c><= 1</c> from <c>> 1</c>, so a MinLevel of 3
|
||||
/// would be observationally identical to 2 but is worth flagging if a
|
||||
/// future DAT drop ever introduces one.
|
||||
/// </summary>
|
||||
[Fact]
|
||||
public void InstalledSkillTable_GlobalSkillDetails_MinLevelDistributionMatchesCostCoverage()
|
||||
{
|
||||
string? datDir = ContentConformanceDats.ResolveDatDir();
|
||||
if (datDir is null)
|
||||
{
|
||||
Console.WriteLine("SKIP: installed retail DAT directory is unavailable.");
|
||||
return;
|
||||
}
|
||||
|
||||
using var dats = new DatCollection(datDir, DatAccessType.Read);
|
||||
using var adapter = new DatCollectionAdapter(dats);
|
||||
ChargenOptions options = ChargenTableReader.Load(adapter);
|
||||
|
||||
Assert.Equal(options.GlobalSkillCostsBySkillId.Count, options.GlobalSkillDetailsBySkillId!.Count);
|
||||
Assert.Equal(38, options.GlobalSkillDetailsBySkillId.Count);
|
||||
|
||||
int useableUntrained = 0;
|
||||
int trainedRequired = 0;
|
||||
bool anyDescriptionNonEmpty = false;
|
||||
foreach (ChargenSkillDetail detail in options.GlobalSkillDetailsBySkillId.Values)
|
||||
{
|
||||
Assert.True(detail.MinLevel <= 2u, $"skill {detail.SkillId}: MinLevel {detail.MinLevel} exceeds Trained(2).");
|
||||
if (detail.MinLevel <= 1u)
|
||||
useableUntrained++;
|
||||
else
|
||||
trainedRequired++;
|
||||
anyDescriptionNonEmpty |= !string.IsNullOrEmpty(detail.Description);
|
||||
}
|
||||
|
||||
Assert.Equal(23, useableUntrained);
|
||||
Assert.Equal(15, trainedRequired);
|
||||
Assert.True(anyDescriptionNonEmpty, "Expected at least one skill to carry a non-empty description.");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// F5's strengthened installed-DAT gate. <see cref="ChargenGenderOptions.HasAnyAppearanceOptions"/>
|
||||
/// only proves an OR across eight lists for at least one gender per
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue