using AcDream.Core.Items; using AcDream.Core.Properties; using AcDream.Core.Spells; namespace AcDream.Core.Player; /// /// Retail CACQualities::InqSkill @ 0x00592660 composition. /// Keeps the augmentation/enchantment ordering in one presentation-independent /// place so character UI and movement consume the same effective skill. /// public static class PlayerSkillMath { public readonly record struct AugmentationBonuses( int AllSkills, bool JackOfAllTrades, int SkilledSpecialized, bool SkilledMelee, bool SkilledMissile, bool SkilledMagic) { public static AugmentationBonuses FromProperties(PropertyBundle properties) { ArgumentNullException.ThrowIfNull(properties); return new( Positive(properties.GetInt((uint)PropertyInt.LumAugAllSkills)), properties.GetInt((uint)PropertyInt.AugmentationJackOfAllTrades) > 0, Positive(properties.GetInt((uint)PropertyInt.LumAugSkilledSpec)), properties.GetInt((uint)PropertyInt.AugmentationSkilledMelee) > 0, properties.GetInt((uint)PropertyInt.AugmentationSkilledMissile) > 0, properties.GetInt((uint)PropertyInt.AugmentationSkilledMagic) > 0); } public int BeforeEnchantments(uint skillId) { int category = skillId switch { // Retail switch at pc 0x005926F6. A positive category // augmentation adds ten once; its stored rank is not multiplied. 0x29u or 0x2Cu or 0x2Du or 0x2Eu or 0x31u when SkilledMelee => 10, 0x2Fu when SkilledMissile => 10, 0x1Fu or 0x20u or 0x21u or 0x22u or 0x2Bu when SkilledMagic => 10, _ => 0, }; return SaturatingAdd(AllSkills, category); } public int AfterEnchantments(uint advancementClass) { int result = JackOfAllTrades ? 5 : 0; if (advancementClass == 3u) result = SaturatingAdd(result, SaturatingMultiply(SkilledSpecialized, 2)); return result; } } public readonly record struct Value( int UnenchantedLevel, int EffectiveLevel, int VitaeModifier); /// /// Compose one skill exactly in retail order: /// intrinsic + LumAugAllSkills/category bonus; EnchantSkill; then /// Jack of All Trades and the specialized luminance bonus. /// public static Value Calculate( int intrinsicLevel, uint skillId, uint advancementClass, AugmentationBonuses augmentations, EnchantmentMath.VitalMod enchantment, float vitaeMultiplier) { int intrinsic = Math.Max(0, intrinsicLevel); int unenchanted = SaturatingAdd( intrinsic, augmentations.BeforeEnchantments(skillId)); int enchanted = EnchantmentMath.EnchantSkill( enchantment, (uint)unenchanted); int effective = SaturatingAdd( enchanted, augmentations.AfterEnchantments(advancementClass)); int vitaeModifier = EnchantmentMath.SkillVitaeModifier( vitaeMultiplier, (uint)unenchanted); return new Value(unenchanted, effective, vitaeModifier); } private static int Positive(int value) => value > 0 ? value : 0; private static int SaturatingAdd(int left, int right) { long result = (long)left + right; return result > int.MaxValue ? int.MaxValue : result < int.MinValue ? int.MinValue : (int)result; } private static int SaturatingMultiply(int left, int right) { long result = (long)left * right; return result > int.MaxValue ? int.MaxValue : result < int.MinValue ? int.MinValue : (int)result; } }