diff --git a/Assets/Scripts/Game/Formulas/FormulaHelper.cs b/Assets/Scripts/Game/Formulas/FormulaHelper.cs index 8e0fbf86a6..4af462d71e 100644 --- a/Assets/Scripts/Game/Formulas/FormulaHelper.cs +++ b/Assets/Scripts/Game/Formulas/FormulaHelper.cs @@ -1739,6 +1739,127 @@ public static void FatigueDamage(EnemyEntity attacker, DaggerfallEntity target, #endregion + + #region Spell Absorption + + /// + /// Tests incoming effect for spell absorption. If absorption succeeds the entity will + /// block effect and recover spell points equal to the casting cost of blocked effect. + /// If target does not have enough spell points free to absorb effect cost then effect will NOT be absorbed. + /// For example if player has 0 of 50 spell points available, they can absorb an incoming effect costing up to 50 spell points. + /// An effect costing 51 spell points cannot be absorbed. It's "all or nothing". + /// Notes: + /// - There are two variants of spell absorption in Daggerfall. + /// - Career-based: This is the "none / in light / in darkness / always" assigned to entity career kit. + /// - Effect-based: Generated by having an active Spell Absorption effect from a spell or item. + /// - In classic effect-based absorption from spells/items will override career-based absorption. Not sure if bug. + /// - Career-based absorption will always succeed chance check. + /// - Spell-based will roll for check on each absorb attempt. + /// + /// Incoming effect. + /// Source bundle target type for spell cost calculation. + /// Caster entity for spell cost calculation. + /// Target entity for spell cost calculation. + /// The absorb incumbent effect, if any, on target + /// The amount of spell points absorbed. 0 if absorb failed + public static int TryAbsorption(IEntityEffect effect, TargetTypes targetType, DaggerfallEntity casterEntity, DaggerfallEntity targetEntity, SpellAbsorption absorbEffectOnTarget) + { + Func del; + if (TryGetOverride("TryAbsorption", out del)) + { + return del(effect, targetType, casterEntity, targetEntity, absorbEffectOnTarget); + } + + var absorbSpellPointsOut = 0; + + // Effect cannot be null + if (effect == null) + return 0; + + // Currently only absorbing Destruction magic - not sure on status of absorbing other magic schools + // This is to prevent something as benign as a self-heal from player being blocked and absorbed + // With current design, absorption is checked for ALL incoming effects to entity so require some sanity checks + if (effect.Properties.MagicSkill != DFCareer.MagicSkills.Destruction) + return 0; + + // Get casting cost for this effect + // Costs are calculated as if target cast the spell, not the actual caster + // Note that if player self-absorbs a spell this will be equal anyway + int effectCastingCost = GetEffectCastingCost(effect, targetType, targetEntity); + + // The entity must have enough spell points free to absorb incoming effect + int availableSpellPoints = targetEntity.MaxMagicka - targetEntity.CurrentMagicka; + if (effectCastingCost > availableSpellPoints) + return 0; + else + absorbSpellPointsOut = effectCastingCost; + + // Handle effect-based absorption + if (absorbEffectOnTarget != null && TryEffectBasedAbsorption(effect, absorbEffectOnTarget, targetEntity)) + return absorbSpellPointsOut; + + // Handle career-based absorption + if (targetEntity.Career.SpellAbsorption != DFCareer.SpellAbsorptionFlags.None && TryCareerBasedAbsorption(effect, targetEntity)) + return absorbSpellPointsOut; + + // Handle persistant absorption (e.g. special advantage general/day/night or from weapon effects) + if (targetEntity.IsAbsorbingSpells) + return absorbSpellPointsOut; + + return 0; + } + + static int GetEffectCastingCost(IEntityEffect effect, TargetTypes targetType, DaggerfallEntity casterEntity) + { + (int _, int spellPointCost) = FormulaHelper.CalculateEffectCosts(effect, effect.Settings, casterEntity); + spellPointCost = FormulaHelper.ApplyTargetCostMultiplier(spellPointCost, targetType); + + // Spells always cost at least 5 spell points + // Otherwise it's possible for absorbs to make spell point pool go down as spell costs 5 but caster absorbs 0 + if (spellPointCost < 5) + spellPointCost = 5; + + //Debug.LogFormat("Calculated {0} spell point cost for effect {1}", spellPointCost, effect.Key); + + return spellPointCost; + } + + static bool TryEffectBasedAbsorption(IEntityEffect effect, SpellAbsorption absorbEffect, DaggerfallEntity entity) + { + int chance = absorbEffect.Settings.ChanceBase + absorbEffect.Settings.ChancePlus * (int)Mathf.Floor(entity.Level / absorbEffect.Settings.ChancePerLevel); + + return Dice100.SuccessRoll(chance); + } + + static bool TryCareerBasedAbsorption(IEntityEffect effect, DaggerfallEntity entity) + { + // Always resists + DFCareer.SpellAbsorptionFlags spellAbsorption = entity.Career.SpellAbsorption; + if (spellAbsorption == DFCareer.SpellAbsorptionFlags.Always) + return true; + + // Resist in darkness (inside building or dungeon or outside at night) + // Use player for inside/outside context - everything is where the player is + if (spellAbsorption == DFCareer.SpellAbsorptionFlags.InDarkness) + { + if (GameManager.Instance.PlayerEnterExit.IsPlayerInside) + return true; + else if (DaggerfallUnity.Instance.WorldTime.Now.IsNight) + return true; + } + + // Resist in light (outside during the day) + if (spellAbsorption == DFCareer.SpellAbsorptionFlags.InLight) + { + if (!GameManager.Instance.PlayerEnterExit.IsPlayerInside && DaggerfallUnity.Instance.WorldTime.Now.IsDay) + return true; + } + + return false; + } + + #endregion + #region Enemies // Generates health for enemy classes based on level and class diff --git a/Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs b/Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs index 0d6d2c94b3..858e10cfa3 100644 --- a/Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs +++ b/Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs @@ -501,12 +501,14 @@ public void AssignBundle(EntityEffectBundle sourceBundle, AssignBundleFlags flag // Set parent bundle effect.ParentBundle = instancedBundle; - // Spell Absorption, Reflection, Resistance - must have a caster entity set - if (sourceBundle.CasterEntityBehaviour) + // Spell Absorption, Reflection, Resistance - must have a caster entity set and only work for spells + if (sourceBundle.CasterEntityBehaviour && sourceBundle.Settings.BundleType == BundleTypes.Spell) { // Spell Absorption - int absorbSpellPoints; - if (sourceBundle.Settings.BundleType == BundleTypes.Spell && TryAbsorption(effect, sourceBundle.Settings.TargetType, sourceBundle.CasterEntityBehaviour.Entity, out absorbSpellPoints)) + + SpellAbsorption absorbEffect = FindIncumbentEffect() as SpellAbsorption; + int absorbSpellPoints = FormulaHelper.TryAbsorption(effect, sourceBundle.Settings.TargetType, sourceBundle.CasterEntityBehaviour.Entity, entityBehaviour.Entity, absorbEffect); + if (absorbSpellPoints > 0) { // Spell passed all checks and was absorbed - tally cost output to target totalAbsorbed += absorbSpellPoints; @@ -518,11 +520,11 @@ public void AssignBundle(EntityEffectBundle sourceBundle, AssignBundleFlags flag } // Spell Reflection - if (!bypassSavingThrows && sourceBundle.Settings.BundleType == BundleTypes.Spell && TryReflection(sourceBundle)) + if (!bypassSavingThrows && TryReflection(sourceBundle)) continue; // Spell Resistance - if (!bypassSavingThrows && sourceBundle.Settings.BundleType == BundleTypes.Spell && TryResistance(sourceBundle)) + if (!bypassSavingThrows && TryResistance(sourceBundle)) continue; } @@ -1136,68 +1138,7 @@ void RerollEffectCallback(IEntityEffect effectTemplate, DaggerfallUnityItem item #endregion - #region Spell Absorption - - /// - /// Tests incoming effect for spell absorption. If absorption succeeds the entity will - /// block effect and recover spell points equal to the casting cost of blocked effect. - /// If target does not have enough spell points free to absorb effect cost then effect will NOT be absorbed. - /// For example if player has 0 of 50 spell points available, they can absorb an incoming effect costing up to 50 spell points. - /// An effect costing 51 spell points cannot be absorbed. It's "all or nothing". - /// Notes: - /// - There are two variants of spell absorption in Daggerfall. - /// - Career-based: This is the "none / in light / in darkness / always" assigned to entity career kit. - /// - Effect-based: Generated by having an active Spell Absorption effect from a spell or item. - /// - In classic effect-based absorption from spells/items will override career-based absorption. Not sure if bug. - /// - Career-based absorption will always succeed chance check. - /// - Spell-based will roll for check on each absorb attempt. - /// - /// Incoming effect. - /// Source bundle target type for spell cost calculation. - /// Source caster entity behaviour for spell cost calculation. - /// Number of spell points absorbed. Only valid when returning true. - /// True if absorbed. - bool TryAbsorption(IEntityEffect effect, TargetTypes targetType, DaggerfallEntity casterEntity, out int absorbSpellPointsOut) - { - absorbSpellPointsOut = 0; - - // Effect cannot be null - if (effect == null) - return false; - - // Currently only absorbing Destruction magic - not sure on status of absorbing other magic schools - // This is to prevent something as benign as a self-heal from player being blocked and absorbed - // With current design, absorption is checked for ALL incoming effects to entity so require some sanity checks - if (effect.Properties.MagicSkill != DFCareer.MagicSkills.Destruction) - return false; - - // Get casting cost for this effect - // Costs are calculated as if target cast the spell, not the actual caster - // Note that if player self-absorbs a spell this will be equal anyway - int effectCastingCost = GetEffectCastingCost(effect, targetType, entityBehaviour.Entity); - - // The entity must have enough spell points free to absorb incoming effect - int availableSpellPoints = entityBehaviour.Entity.MaxMagicka - entityBehaviour.Entity.CurrentMagicka; - if (effectCastingCost > availableSpellPoints) - return false; - else - absorbSpellPointsOut = effectCastingCost; - - // Handle effect-based absorption - SpellAbsorption absorbEffect = FindIncumbentEffect() as SpellAbsorption; - if (absorbEffect != null && TryEffectBasedAbsorption(effect, absorbEffect, entityBehaviour.Entity)) - return true; - - // Handle career-based absorption - if (entityBehaviour.Entity.Career.SpellAbsorption != DFCareer.SpellAbsorptionFlags.None && TryCareerBasedAbsorption(effect, entityBehaviour.Entity)) - return true; - - // Handle persistant absorption (e.g. special advantage general/day/night or from weapon effects) - if (entityBehaviour.Entity.IsAbsorbingSpells) - return true; - - return false; - } + #region Reflection & Resistance /// /// Tests incoming spell bundle for reflection. @@ -1267,56 +1208,7 @@ bool TryResistance(EntityEffectBundle sourceBundle) } return false; - } - - int GetEffectCastingCost(IEntityEffect effect, TargetTypes targetType, DaggerfallEntity casterEntity) - { - (int _, int spellPointCost) = FormulaHelper.CalculateEffectCosts(effect, effect.Settings, casterEntity); - spellPointCost = FormulaHelper.ApplyTargetCostMultiplier(spellPointCost, targetType); - - // Spells always cost at least 5 spell points - // Otherwise it's possible for absorbs to make spell point pool go down as spell costs 5 but caster absorbs 0 - if (spellPointCost < 5) - spellPointCost = 5; - - //Debug.LogFormat("Calculated {0} spell point cost for effect {1}", spellPointCost, effect.Key); - - return spellPointCost; - } - - bool TryEffectBasedAbsorption(IEntityEffect effect, SpellAbsorption absorbEffect, DaggerfallEntity entity) - { - int chance = absorbEffect.Settings.ChanceBase + absorbEffect.Settings.ChancePlus * (int)Mathf.Floor(entity.Level / absorbEffect.Settings.ChancePerLevel); - - return Dice100.SuccessRoll(chance); - } - - bool TryCareerBasedAbsorption(IEntityEffect effect, DaggerfallEntity entity) - { - // Always resists - DFCareer.SpellAbsorptionFlags spellAbsorption = entity.Career.SpellAbsorption; - if (spellAbsorption == DFCareer.SpellAbsorptionFlags.Always) - return true; - - // Resist in darkness (inside building or dungeon or outside at night) - // Use player for inside/outside context - everything is where the player is - if (spellAbsorption == DFCareer.SpellAbsorptionFlags.InDarkness) - { - if (GameManager.Instance.PlayerEnterExit.IsPlayerInside) - return true; - else if (DaggerfallUnity.Instance.WorldTime.Now.IsNight) - return true; - } - - // Resist in light (outside during the day) - if (spellAbsorption == DFCareer.SpellAbsorptionFlags.InLight) - { - if (!GameManager.Instance.PlayerEnterExit.IsPlayerInside && DaggerfallUnity.Instance.WorldTime.Now.IsDay) - return true; - } - - return false; - } + } #endregion