Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions Assets/Scripts/Game/Formulas/FormulaHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1739,6 +1739,127 @@ public static void FatigueDamage(EnemyEntity attacker, DaggerfallEntity target,

#endregion


#region Spell Absorption

/// <summary>
/// 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.
/// </summary>
/// <param name="effect">Incoming effect.</param>
/// <param name="targetType">Source bundle target type for spell cost calculation.</param>
/// <param name="casterEntity">Caster entity for spell cost calculation.</param>
/// <param name="targetEntity">Target entity for spell cost calculation.</param>
/// <param name="absorbEffectOnTarget">The absorb incumbent effect, if any, on target</param>
/// <returns>The amount of spell points absorbed. 0 if absorb failed</returns>
public static int TryAbsorption(IEntityEffect effect, TargetTypes targetType, DaggerfallEntity casterEntity, DaggerfallEntity targetEntity, SpellAbsorption absorbEffectOnTarget)
{
Func<IEntityEffect , TargetTypes, DaggerfallEntity, DaggerfallEntity, SpellAbsorption, int> 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
Expand Down
128 changes: 10 additions & 118 deletions Assets/Scripts/Game/MagicAndEffects/EntityEffectManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<SpellAbsorption>() 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;
Expand All @@ -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;
}

Expand Down Expand Up @@ -1136,68 +1138,7 @@ void RerollEffectCallback(IEntityEffect effectTemplate, DaggerfallUnityItem item

#endregion

#region Spell Absorption

/// <summary>
/// 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.
/// </summary>
/// <param name="effect">Incoming effect.</param>
/// <param name="targetType">Source bundle target type for spell cost calculation.</param>
/// <param name="casterEntity">Source caster entity behaviour for spell cost calculation.</param>
/// <param name="absorbSpellPointsOut">Number of spell points absorbed. Only valid when returning true.</param>
/// <returns>True if absorbed.</returns>
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<SpellAbsorption>() 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

/// <summary>
/// Tests incoming spell bundle for reflection.
Expand Down Expand Up @@ -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

Expand Down