using System.Numerics; using AcDream.Core.CharGen; namespace AcDream.Runtime.Tests.CharGen; /// /// A small, hand-authored fixture for /// tests — NOT installed-DAT data /// (that is CC1's ChargenTableReaderInstalledDatTests concern). Shapes /// mirror retail's field semantics closely enough to exercise every gate: /// two heritages (a normal one and an Olthoi variant that must force template /// 0), a "Custom"/floor template plus a str-heavy preset, a mix of costed /// (including two FREE skills exercising every ResetSkillLevels /// baseline branch) and deliberately uncostable skill ids, and one gender /// with small appearance-option lists to exercise index bounds. /// internal static class RuntimeCharacterCreationStateFixture { public const uint AluvianId = 1u; public const uint OlthoiId = (uint)ChargenHeritageGroup.Olthoi; public const uint ImpoverishedId = 90u; public const uint MaleGenderKey = 1u; /// Campaign CC slice CC5: RandomizeCharacter's gender /// roll (RollDice(1,2)) needs BOTH gender keys resolvable on the /// four human heritages, or half of all random seeds would land on a /// gender can't /// resolve and silently leave appearance untouched (a real, harmless /// ConstrainAllByGender-style fallback — but not what these tests /// are pinning). public const uint FemaleGenderKey = 2u; /// str=10 end=10 coord=10 quick=10 focus=10 self=10 — the /// budget-66 heritage leaves 6 credits unspent after this template, /// matching retail's "Custom sits at the floor" finding. public const uint CustomTemplateIndex = 0u; /// str=16, everything else at floor — sum 66 (fully spent). public const uint PresetTemplateIndex = 1u; /// Costed only by the heritage's own list — Normal 4 / Primary 12. public const uint SkillTrainSpecialize = 1u; /// The Preset template's Primary (specialized) skill. public const uint SkillPresetPrimary = 2u; /// FREE and pre-specialized by ResetSkillLevels's baseline /// (NormalCost=0, PrimaryCost<=0). public const uint SkillFreeSpecialized = 3u; /// FREE to train, costs to specialize /// (NormalCost=0, PrimaryCost>0). public const uint SkillFreeTrained = 4u; /// Uncostable in BOTH tiers — must never be settable (the CC1 /// R1 binding fact: keep the both-miss refund path unreachable). public const uint SkillUncostable = 5u; /// The Custom template's authored Normal skill. public const uint SkillCustomNormal = 24u; public static ChargenOptions Build() { var starterAreas = new List { new(0, "Holtburg", [new ChargenPosition(1u, Vector3.Zero, Quaternion.Identity)]), new(1, "Yaraq", [new ChargenPosition(2u, Vector3.Zero, Quaternion.Identity)]), }; var skillCosts = new Dictionary { [SkillTrainSpecialize] = new(SkillTrainSpecialize, NormalCost: 4, PrimaryCost: 12), [SkillPresetPrimary] = new(SkillPresetPrimary, NormalCost: 3, PrimaryCost: 9), [SkillFreeSpecialized] = new(SkillFreeSpecialized, NormalCost: 0, PrimaryCost: 0), [SkillFreeTrained] = new(SkillFreeTrained, NormalCost: 0, PrimaryCost: 5), [SkillCustomNormal] = new(SkillCustomNormal, NormalCost: 2, PrimaryCost: 6), }; var gender = new ChargenGenderOptions( GenderKey: (int)MaleGenderKey, Name: "Male", Scale: 1u, SetupId: 0x2000054u, SoundTableId: 0u, IconId: 0u, BasePaletteId: 0u, SkinPalSetId: 0u, PhysicsTableId: 0u, MotionTableId: 0u, CombatTableId: 0u, BaseObjDesc: ChargenObjDesc.Empty, HairColors: [100u, 101u], HairStyles: [ new ChargenHairStyle(1u, Bald: false, AlternateSetup: 0u, ObjDesc: ChargenObjDesc.Empty), new ChargenHairStyle(2u, Bald: true, AlternateSetup: 0u, ObjDesc: ChargenObjDesc.Empty), ], EyeColors: [200u, 201u], EyeStrips: [ new ChargenEyeStrip(1u, 2u, ChargenObjDesc.Empty, ChargenObjDesc.Empty), ], NoseStrips: [new ChargenFaceStrip(1u, ChargenObjDesc.Empty)], MouthStrips: [new ChargenFaceStrip(1u, ChargenObjDesc.Empty)], Headgears: [new ChargenGearOption("Cap", 1u, 300u)], Shirts: [new ChargenGearOption("Shirt", 2u, 301u)], Pants: [new ChargenGearOption("Pants", 3u, 302u)], Footwear: [new ChargenGearOption("Boots", 4u, 303u)], ClothingColors: [400u, 401u, 402u]); // Same shape as `gender`, just the other GenderKey — every list is // deliberately populated so a full RandomizeCharacter roll never // finds an empty option list to skip. var femaleGender = gender with { GenderKey = (int)FemaleGenderKey, Name = "Female" }; var bothGenders = new Dictionary { [(int)MaleGenderKey] = gender, [(int)FemaleGenderKey] = femaleGender, }; var aluvianTemplates = new List { new( "Custom", IconId: 0u, TitleStringId: 0u, Attributes: new ChargenAttributeValues(10, 10, 10, 10, 10, 10), NormalSkills: [SkillCustomNormal], PrimarySkills: []), new( "Preset", IconId: 0u, TitleStringId: 0u, Attributes: new ChargenAttributeValues(16, 10, 10, 10, 10, 10), NormalSkills: [SkillTrainSpecialize], PrimarySkills: [SkillPresetPrimary]), }; var aluvian = new ChargenHeritageOptions( AluvianId, "Aluvian", IconId: 0u, SetupId: 0x2000054u, EnvironmentSetupId: 0u, AttributeCredits: 66u, SkillCredits: 50u, PrimaryStartAreaIndices: [0, 1], SecondaryStartAreaIndices: [], SkillCostsBySkillId: skillCosts, Templates: aluvianTemplates, GendersByKey: bothGenders); var olthoiTemplates = new List { new( "Custom", IconId: 0u, TitleStringId: 0u, Attributes: new ChargenAttributeValues(10, 10, 10, 10, 10, 10), NormalSkills: [], PrimarySkills: []), new( "NeverChosen", IconId: 0u, TitleStringId: 0u, Attributes: new ChargenAttributeValues(20, 20, 20, 20, 20, 20), NormalSkills: [], PrimarySkills: []), }; var olthoi = new ChargenHeritageOptions( OlthoiId, "Olthoi", IconId: 0u, SetupId: 0x2000054u, EnvironmentSetupId: 0u, AttributeCredits: 60u, SkillCredits: 0u, PrimaryStartAreaIndices: [0], SecondaryStartAreaIndices: [], SkillCostsBySkillId: new Dictionary(), Templates: olthoiTemplates, GendersByKey: new Dictionary { [(int)MaleGenderKey] = gender }); // Campaign CC slice CC5: CharGenState::RandomizeCharacter @ // 0x005c6d80 rolls a heritage id uniformly in [1, hasToD?4:3] — the // four HUMAN heritage groups (ChargenHeritageGroup.Aluvian.. // Viamontian). RandomizeCharacterLocked's tests need every one of // those four ids resolvable, not just Aluvian, so the roll can never // silently land on a missing heritage. Ids 2-4 mirror Aluvian's own // shape (same gender/template data) — the roll target, not the // template/skill-budget math, is what those tests exercise. ChargenHeritageOptions MakeHumanHeritage(uint id, string name) => new( id, name, IconId: 0u, SetupId: 0x2000054u, EnvironmentSetupId: 0u, AttributeCredits: 66u, SkillCredits: 50u, PrimaryStartAreaIndices: [0, 1], SecondaryStartAreaIndices: [], SkillCostsBySkillId: skillCosts, Templates: aluvianTemplates, GendersByKey: bothGenders); // A deliberately impoverished heritage — just enough skill credits // to train SkillTrainSpecialize but never specialize it — so a // TrySpecializeSkill affordability refusal is directly testable // without needing to hand-drain the richer Aluvian budget. var impoverished = new ChargenHeritageOptions( ImpoverishedId, "Impoverished", IconId: 0u, SetupId: 0x2000054u, EnvironmentSetupId: 0u, AttributeCredits: 60u, SkillCredits: 5u, PrimaryStartAreaIndices: [0], SecondaryStartAreaIndices: [], SkillCostsBySkillId: new Dictionary { [SkillTrainSpecialize] = new(SkillTrainSpecialize, NormalCost: 4, PrimaryCost: 12), }, Templates: [ new ChargenTemplate( "Custom", IconId: 0u, TitleStringId: 0u, Attributes: new ChargenAttributeValues(10, 10, 10, 10, 10, 10), NormalSkills: [], PrimarySkills: []), ], GendersByKey: new Dictionary { [(int)MaleGenderKey] = gender }); return new ChargenOptions( starterAreas, new Dictionary { [AluvianId] = aluvian, [(uint)ChargenHeritageGroup.Gharundim] = MakeHumanHeritage( (uint)ChargenHeritageGroup.Gharundim, "Gharu'ndim"), [(uint)ChargenHeritageGroup.Sho] = MakeHumanHeritage( (uint)ChargenHeritageGroup.Sho, "Sho"), [(uint)ChargenHeritageGroup.Viamontian] = MakeHumanHeritage( (uint)ChargenHeritageGroup.Viamontian, "Viamontian"), [OlthoiId] = olthoi, [ImpoverishedId] = impoverished, }, new Dictionary()); } }