using Theriapolis.Core; using Theriapolis.Core.Data; using Theriapolis.Core.Rules.Character; using Theriapolis.Core.Rules.Stats; using Xunit; namespace Theriapolis.Tests.Dungeons; /// /// Phase 7 M0 — verifies the headless level-up loop the /// character-roll --level N Tools flag uses. The Tools command /// re-creates this loop in ; /// this test asserts the API contract works deterministically without /// invoking the Tools assembly directly. /// public sealed class CharacterRollLevelFlagTests { private static (Character pc, IReadOnlyDictionary subs) BuildBase() { var loader = new ContentLoader(TestHelpers.DataDirectory); var content = new ContentResolver(loader); var b = new CharacterBuilder { Clade = content.Clades["canidae"], Species = content.Species["wolf"], ClassDef = content.Classes["fangsworn"], Background = content.Backgrounds["pack_raised"], BaseAbilities = new AbilityScores(15, 14, 13, 10, 12, 8), Name = "Test", }; int n = b.ClassDef.SkillsChoose; foreach (var raw in b.ClassDef.SkillOptions) { if (b.ChosenClassSkills.Count >= n) break; try { b.ChosenClassSkills.Add(SkillIdExtensions.FromJson(raw)); } catch { } } Assert.True(b.Validate(out _)); return (b.Build(content.Items), content.Subclasses); } private static Character LevelTo(int target, ulong worldSeed = 12345UL, ulong msOverride = 0UL) { var (pc, subs) = BuildBase(); for (int lv = 2; lv <= target; lv++) { ulong seed = worldSeed ^ msOverride ^ C.RNG_LEVELUP ^ (ulong)lv; var result = LevelUpFlow.Compute(pc, lv, seed, takeAverage: true, subclasses: subs); var choices = new LevelUpChoices { TakeAverageHp = true, SubclassId = result.GrantsSubclassChoice && pc.ClassDef.SubclassIds.Length > 0 ? pc.ClassDef.SubclassIds[0] : null, }; if (result.GrantsAsiChoice) choices.AsiAdjustments[AbilityId.CON] = 2; pc.ApplyLevelUp(result, choices); } return pc; } [Fact] public void LevelN_ProducesExpectedLevelAndProficiency() { var pc1 = LevelTo(1); Assert.Equal(1, pc1.Level); Assert.Equal(2, pc1.ProficiencyBonus); var pc5 = LevelTo(5); Assert.Equal(5, pc5.Level); Assert.Equal(3, pc5.ProficiencyBonus); var pc11 = LevelTo(11); Assert.Equal(11, pc11.Level); Assert.Equal(4, pc11.ProficiencyBonus); } [Fact] public void LevelN_PicksSubclassAtLevelThree() { var pc3 = LevelTo(3); Assert.Equal(3, pc3.Level); Assert.False(string.IsNullOrEmpty(pc3.SubclassId), "level-3 character must have a subclass selected"); } [Fact] public void LevelN_AppliesAsiAtLevelFour() { // Auto-pilot ASI puts +2 to CON at level 4 (one of the C.ASI_LEVELS). // Compare CON pre/post — clade + species mods are baked in by the // builder, so absolute values vary by build choices but the // delta is exactly 2. var pc3 = LevelTo(3); var pc4 = LevelTo(4); Assert.Equal(pc3.Abilities.CON + 2, pc4.Abilities.CON); } [Fact] public void LevelN_IsDeterministic() { var a = LevelTo(7, worldSeed: 99UL, msOverride: 12345UL); var b = LevelTo(7, worldSeed: 99UL, msOverride: 12345UL); Assert.Equal(a.Level, b.Level); Assert.Equal(a.MaxHp, b.MaxHp); Assert.Equal(a.SubclassId, b.SubclassId); } }