namespace AcDream.Core.CharGen; /// /// Pure port of retail's skill-credit spend calculation /// (CharGenState::UpdateRemainingSkillCredits @ 0x005C37C0): walk /// every skill slot, add NormalCost for Trained or PrimaryCost /// for Specialized (never both), and subtract the total from the heritage's /// SkillCredits budget. No DAT/DatReaderWriter dependency — callers /// (CC3's Runtime owner) pass in the heritage's already-projected /// lookup. /// public static class ChargenSkillCreditMath { /// /// Total credits spent across every Trained/Specialized skill in /// . A skill with no cost entry for the /// active heritage (i.e. the heritage doesn't offer it) is skipped — /// retail's own UI can never reach that state, so this is defensive /// rather than a documented retail behavior. /// public static int ComputeSpent( ChargenSkillAdvancementSet advancement, IReadOnlyDictionary costsBySkillId) { ArgumentNullException.ThrowIfNull(advancement); ArgumentNullException.ThrowIfNull(costsBySkillId); int spent = 0; for (uint skillId = 1; skillId < ChargenSkillAdvancementSet.SlotCount; skillId++) { ChargenSkillAdvancementClass cls = advancement[skillId]; if (cls != ChargenSkillAdvancementClass.Trained && cls != ChargenSkillAdvancementClass.Specialized) { continue; } if (!costsBySkillId.TryGetValue(skillId, out ChargenSkillCost cost)) continue; spent += cls == ChargenSkillAdvancementClass.Specialized ? cost.PrimaryCost : cost.NormalCost; } return spent; } /// /// totalSkillCredits - ComputeSpent(...) — retail's /// remainingSkillCredits. Retail's Finish gate (DoFinish @ /// 0x004E91F2-adjacent) only checks remainingAtrbCredits > 0 /// for attributes; skill credits are NOT required to hit exactly zero /// (unspent skill credits are simply lost on creation) — callers should /// not port an "exact spend" gate for skills the way /// does for attributes. /// public static int RemainingCredits( uint totalSkillCredits, ChargenSkillAdvancementSet advancement, IReadOnlyDictionary costsBySkillId) => checked((int)totalSkillCredits) - ComputeSpent(advancement, costsBySkillId); }