using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using ACE.Common;
using ACE.Common.Extensions;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Entity.Models;
using ACE.Server.Entity;
using ACE.Server.Managers;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.Network.GameEvent.Events;
using ACE.Server.Network.Structure;
using ACE.Server.WorldObjects.Entity;
namespace ACE.Server.WorldObjects.Managers
{
public enum StackType
{
None,
Initial,
Surpass,
Refresh,
Surpassed,
};
public class EnchantmentManager
{
public WorldObject WorldObject { get; }
public Player Player { get; }
///
/// Returns TRUE if this object has any active enchantments in the registry
///
public virtual bool HasEnchantments => WorldObject.Biota.PropertiesEnchantmentRegistry.HasEnchantments(WorldObject.BiotaDatabaseLock);
///
/// Returns TRUE If this object has a vitae penalty
///
public virtual bool HasVitae => WorldObject.Biota.PropertiesEnchantmentRegistry.HasEnchantment((uint)SpellId.Vitae, WorldObject.BiotaDatabaseLock);
///
/// Constructs a new EnchantmentManager for a WorldObject
///
public EnchantmentManager(WorldObject obj)
{
WorldObject = obj;
Player = obj as Player;
}
///
/// Returns TRUE if registry contains spellId for this creature
///
public bool HasSpell(uint spellId)
{
return WorldObject.Biota.PropertiesEnchantmentRegistry.HasEnchantment(spellId, WorldObject.BiotaDatabaseLock);
}
///
/// Returns the enchantments for a specific spell
///
public PropertiesEnchantmentRegistry GetEnchantment(uint spellID, uint? casterGuid = null)
{
return WorldObject.Biota.PropertiesEnchantmentRegistry.GetEnchantmentBySpell((int)spellID, casterGuid, WorldObject.BiotaDatabaseLock);
}
///
/// Returns the enchantments for a specific spell from an equipment set
///
public PropertiesEnchantmentRegistry GetEnchantment(uint spellID, EquipmentSet equipmentSet)
{
return WorldObject.Biota.PropertiesEnchantmentRegistry.GetEnchantmentBySpellSet((int)spellID, equipmentSet, WorldObject.BiotaDatabaseLock);
}
///
/// Returns a list of all the active enchantments for a magic school
///
public List GetEnchantments(MagicSchool magicSchool)
{
var spells = new List();
var topLayerEnchantments = WorldObject.Biota.PropertiesEnchantmentRegistry.GetEnchantmentsTopLayer(WorldObject.BiotaDatabaseLock, SpellSet.SetSpells);
foreach (var enchantment in topLayerEnchantments)
{
if (enchantment.SpellId > SpellCategory_Cooldown)
continue;
var spell = new Spell(enchantment.SpellId);
if (spell.NotFound)
{
Console.WriteLine($"EnchantmentManager.GetEnchantments({magicSchool}): couldn't find spell {enchantment.SpellId} for {WorldObject.Name}");
continue;
}
if (spell.School == magicSchool)
spells.Add(enchantment);
}
return spells;
}
///
/// Returns all of the enchantments for a category
///
public List GetEnchantments(SpellCategory spellCategory)
{
return WorldObject.Biota.PropertiesEnchantmentRegistry.GetEnchantmentsByCategory(spellCategory, WorldObject.BiotaDatabaseLock);
}
///
/// Returns the top layers in each spell category for a StatMod type
///
public List GetEnchantments_TopLayer(EnchantmentTypeFlags statModType)
{
return WorldObject.Biota.PropertiesEnchantmentRegistry.GetEnchantmentsTopLayerByStatModType(statModType, WorldObject.BiotaDatabaseLock, SpellSet.SetSpells);
}
///
/// Returns the top layers in each spell category for a StatMod type + key
///
public List GetEnchantments_TopLayer(EnchantmentTypeFlags statModType, uint statModKey, bool handleMultiple = false)
{
return WorldObject.Biota.PropertiesEnchantmentRegistry.GetEnchantmentsTopLayerByStatModType(statModType, statModKey, WorldObject.BiotaDatabaseLock, SpellSet.SetSpells, handleMultiple);
}
///
/// Add/update an enchantment in this object's registry
///
public virtual AddEnchantmentResult Add(Spell spell, WorldObject caster, WorldObject weapon, bool equip = false, bool isWeaponSpell = false)
{
var result = new AddEnchantmentResult();
// check for existing spell in this category
var entries = GetEnchantments(spell.Category);
// if none, add new record
if (entries.Count == 0)
{
var newEntry = BuildEntry(spell, caster, weapon, equip, isWeaponSpell);
newEntry.LayerId = 1;
WorldObject.Biota.PropertiesEnchantmentRegistry.AddEnchantment(newEntry, WorldObject.BiotaDatabaseLock);
WorldObject.ChangesDetected = true;
result.Enchantment = newEntry;
result.StackType = StackType.Initial;
return result;
}
result.BuildStack(entries, spell, caster, equip, isWeaponSpell);
// handle cases:
// surpassing: new spell is written to next layer
// refreshing: - key by caster guid
// surpassed: - underpowered spell is written to next layer?
// note that these cases are not exclusive,
// consider case: strength 3 -> strength 6 -> strength 3
// for the 2nd cast of strength 3, it would have 1 refresh and 1 surpassed
// would 2nd cast of strength 3 refresh the 1st, but still be surpassed by 6?
var refreshSpell = result.Refresh.Count > 0 ? result.RefreshCaster : null;
if (refreshSpell == null)
{
var newEntry = BuildEntry(spell, caster, weapon, equip);
newEntry.LayerId = result.NextLayerId;
WorldObject.Biota.PropertiesEnchantmentRegistry.AddEnchantment(newEntry, WorldObject.BiotaDatabaseLock);
result.Enchantment = newEntry;
}
else
{
// for multiple void casters casting the same DoT,
// we might want to sort by StatModValue in GetEnchantments_TopLayer()
// for the same void caster re-casting the same DoT,
// should be update the StatModVal here?
var duration = spell.Duration;
if (caster is Player player && player.AugmentationIncreasedSpellDuration > 0 && !isWeaponSpell && spell.DotDuration == 0)
duration *= 1.0f + player.AugmentationIncreasedSpellDuration * 0.2f;
var timeRemaining = refreshSpell.Duration + refreshSpell.StartTime;
if (duration > timeRemaining)
{
refreshSpell.StartTime = 0;
refreshSpell.Duration = duration;
}
result.Enchantment = refreshSpell;
}
WorldObject.ChangesDetected = true;
// output message is from StackType,
// which is the largest of the combined StackTypes
return result;
}
///
/// Builds an enchantment registry entry from a spell ID
///
private PropertiesEnchantmentRegistry BuildEntry(Spell spell, WorldObject caster = null, WorldObject weapon = null, bool equip = false, bool isWeaponSpell = false)
{
var entry = new PropertiesEnchantmentRegistry();
entry.EnchantmentCategory = (uint)spell.MetaSpellType;
entry.SpellId = (int)spell.Id;
entry.SpellCategory = spell.Category;
entry.PowerLevel = spell.Power;
if (caster is Creature)
{
entry.Duration = spell.Duration;
if (caster is Player player && player.AugmentationIncreasedSpellDuration > 0 && !isWeaponSpell && spell.DotDuration == 0)
entry.Duration *= 1.0f + player.AugmentationIncreasedSpellDuration * 0.2f;
}
else
{
if (!equip)
{
entry.Duration = spell.Duration;
}
else
{
// enchantments from equipping items are active until the item is dequipped
entry.Duration = -1.0;
entry.StartTime = 0;
}
}
if (caster == null)
entry.CasterObjectId = WorldObject.Guid.Full;
else
entry.CasterObjectId = caster.Guid.Full;
entry.DegradeModifier = spell.DegradeModifier;
entry.DegradeLimit = spell.DegradeLimit;
entry.StatModType = spell.StatModType;
entry.StatModKey = spell.StatModKey;
entry.StatModValue = spell.StatModVal;
if (spell.IsBeneficial) // should "server" data be fixed or is this the better way to do this?
entry.StatModType |= EnchantmentTypeFlags.Beneficial;
if (spell.IsDamageOverTime)
{
var heartbeatInterval = WorldObject.HeartbeatInterval ?? 5.0f;
// scale StatModValue for HeartbeatIntervals other than the default 5s
if (heartbeatInterval != 5.0f)
{
entry.StatModValue = spell.GetDamagePerTick((float)heartbeatInterval);
}
// calculate runtime StatModValue for enchantment
if (caster != null)
{
entry.StatModValue = caster.CalculateDotEnchantment_StatModValue(spell, WorldObject, weapon, entry.StatModValue);
}
//Console.WriteLine($"enchantment_statModVal: {entry.StatModValue}");
}
// handle equipment sets
if (caster != null && caster.HasItemSet && caster.ItemSetContains(spell.Id))
{
entry.HasSpellSetId = true;
entry.SpellSetId = (EquipmentSet)caster.EquipmentSetId;
}
return entry;
}
///
/// Adds a cooldown spell to the enchantment registry
///
public virtual bool StartCooldown(WorldObject item)
{
var cooldownID = item.CooldownId;
if (cooldownID == null)
return false;
var newEntry = new PropertiesEnchantmentRegistry();
// TODO: BiotaPropertiesEnchantmentRegistry.SpellId should be uint
newEntry.SpellId = (int)GetCooldownSpellID(cooldownID.Value);
newEntry.SpellCategory = (SpellCategory)SpellCategory_Cooldown;
newEntry.HasSpellSetId = true;
newEntry.Duration = item.CooldownDuration ?? 0.0f;
newEntry.CasterObjectId = item.Guid.Full;
newEntry.DegradeLimit = -666;
newEntry.StatModType = EnchantmentTypeFlags.Cooldown;
newEntry.EnchantmentCategory = (uint)EnchantmentMask.Cooldown;
newEntry.LayerId = 1; // cooldown at layer 1, any spells at layer 2?
WorldObject.Biota.PropertiesEnchantmentRegistry.AddEnchantment(newEntry, WorldObject.BiotaDatabaseLock);
WorldObject.ChangesDetected = true;
Player.Session.Network.EnqueueSend(new GameEventMagicUpdateEnchantment(Player.Session, new Enchantment(Player, newEntry)));
return true;
}
///
/// Removes a spell from the enchantment registry, and
/// sends the relevant network messages for spell removal
///
public virtual void Remove(PropertiesEnchantmentRegistry entry, bool sound = true)
{
if (entry == null)
return;
var spellID = entry.SpellId;
if (WorldObject.Biota.PropertiesEnchantmentRegistry.TryRemoveEnchantment(entry.SpellId, entry.CasterObjectId, WorldObject.BiotaDatabaseLock))
WorldObject.ChangesDetected = true;
if (Player != null)
{
var layer = (entry.SpellId == (uint)SpellId.Vitae) ? (ushort)0 : entry.LayerId; // this line is to force vitae to be layer 0 to match retail pcaps. We save it as layer 1 to make EF Core happy.
Player.Session.Network.EnqueueSend(new GameEventMagicRemoveEnchantment(Player.Session, (ushort)entry.SpellId, layer));
if (sound && entry.SpellCategory != (SpellCategory)SpellCategory_Cooldown)
Player.Session.Network.EnqueueSend(new GameMessageSound(Player.Guid, Sound.SpellExpire, 1.0f));
}
else
{
var ownerID = WorldObject.OwnerId ?? WorldObject.WielderId;
if (ownerID != null)
{
var owner = PlayerManager.GetOnlinePlayer((uint)ownerID);
if (owner != null)
{
var spell = new Spell(spellID);
owner.Session.Network.EnqueueSend(new GameMessageSystemChat($"The spell {spell.Name} on {WorldObject.Name} has expired.", ChatMessageType.Magic));
if (sound)
owner.Session.Network.EnqueueSend(new GameMessageSound(owner.Guid, Sound.SpellExpire, 1.0f));
}
}
}
}
///
/// Removes all enchantments except for vitae and item spells
/// Called on player death
///
public virtual void RemoveAllEnchantments()
{
// exclude cooldowns and enchantments from items
var spellsToExclude = WorldObject.Biota.PropertiesEnchantmentRegistry.Clone(WorldObject.BiotaDatabaseLock).Where(i => i.Duration == -1 || i.SpellId > short.MaxValue).Select(i => i.SpellId);
WorldObject.Biota.PropertiesEnchantmentRegistry.RemoveAllEnchantments(spellsToExclude, WorldObject.BiotaDatabaseLock);
WorldObject.ChangesDetected = true;
}
///
/// Removes all enchantments except for beneficial enchantments, vitae and item spells
/// Called on player death
///
public virtual void RemoveAllBadEnchantments()
{
// exclude beneficial enchantments, cooldowns and enchantments from items
var spellsToExclude = WorldObject.Biota.PropertiesEnchantmentRegistry.Clone(WorldObject.BiotaDatabaseLock).Where(i => i.StatModType.HasFlag(EnchantmentTypeFlags.Beneficial) || i.Duration == -1 || i.SpellId > short.MaxValue).Select(i => i.SpellId);
WorldObject.Biota.PropertiesEnchantmentRegistry.RemoveAllEnchantments(spellsToExclude, WorldObject.BiotaDatabaseLock);
WorldObject.ChangesDetected = true;
}
///
/// Returns the vitae enchantment
///
public PropertiesEnchantmentRegistry GetVitae()
{
return GetEnchantment((uint)SpellId.Vitae);
}
///
/// Returns the minimum vitae for a player level
///
public float GetMinVitae(uint level)
{
var propVitae = 1.0 - PropertyManager.GetDouble("vitae_penalty_max").Item;
var maxPenalty = (level - 1) * 3;
if (maxPenalty < 1)
maxPenalty = 1;
var globalMax = 100 - (uint)Math.Round(propVitae * 100);
if (maxPenalty > globalMax)
maxPenalty = globalMax;
var minVitae = (100 - maxPenalty) / 100.0f;
if (minVitae < propVitae)
minVitae = (float)propVitae;
return minVitae;
}
///
/// Called on player death
///
public virtual float UpdateVitae()
{
if (Player == null) return 0;
PropertiesEnchantmentRegistry vitae;
if (!HasVitae)
{
// TODO refactor this so it uses the existing Add() method.
// add entry for new vitae
var spell = new Spell(SpellId.Vitae);
vitae = BuildEntry(spell);
vitae.EnchantmentCategory = (uint)EnchantmentMask.Vitae;
vitae.LayerId = 1; // This should be 0 but EF Core seems to be very unhappy with 0 as the layer id now that we're using layer as part of the composite key.
vitae.StatModValue = 1.0f - (float)PropertyManager.GetDouble("vitae_penalty").Item;
WorldObject.Biota.PropertiesEnchantmentRegistry.AddEnchantment(vitae, WorldObject.BiotaDatabaseLock);
WorldObject.ChangesDetected = true;
}
else
{
// update existing vitae
vitae = GetVitae();
vitae.StatModValue -= (float)PropertyManager.GetDouble("vitae_penalty").Item;
WorldObject.ChangesDetected = true;
}
var minVitae = GetMinVitae((uint)Player.Level);
if (vitae.StatModValue < minVitae)
vitae.StatModValue = minVitae;
if (vitae.StatModValue > 1.0f)
vitae.StatModValue = 1.0f;
return vitae.StatModValue;
}
///
/// Called when player crosses the VitaeCPPool threshold
///
public virtual float ReduceVitae()
{
var vitae = GetVitae();
vitae.StatModValue += 0.01f;
if (vitae.StatModValue.EpsilonEquals(1.0f) || vitae.StatModValue > 1.0f)
return 1.0f;
return vitae.StatModValue;
}
///
/// Removes the vitae penalty for a player
///
public void RemoveVitae()
{
if (Player == null)
return;
var vitae = GetVitae();
Remove(vitae);
}
///
/// Silently removes a spell from the enchantment registry, and sends the relevant network message for dispel
///
public virtual void Dispel(PropertiesEnchantmentRegistry entry)
{
if (entry == null)
return;
var spellID = entry.SpellId;
if (WorldObject.Biota.PropertiesEnchantmentRegistry.TryRemoveEnchantment(entry.SpellId, entry.CasterObjectId, WorldObject.BiotaDatabaseLock))
WorldObject.ChangesDetected = true;
if (Player != null)
Player.Session.Network.EnqueueSend(new GameEventMagicDispelEnchantment(Player.Session, (ushort)entry.SpellId, entry.LayerId));
}
///
/// Silently removes multiple spells from the enchantment registry, and sends the relevent network messages for dispel
///
public virtual void Dispel(List entries)
{
if (entries == null || entries.Count == 0)
return;
foreach (var entry in entries)
{
if (WorldObject.Biota.PropertiesEnchantmentRegistry.TryRemoveEnchantment(entry.SpellId, entry.CasterObjectId, WorldObject.BiotaDatabaseLock))
WorldObject.ChangesDetected = true;
}
if (Player != null)
Player.Session.Network.EnqueueSend(new GameEventMagicDispelMultipleEnchantments(Player.Session, entries));
}
///
/// Removes all enchantments from the player on server, and sends network messages to silently dispel the enchantments
///
public void DispelAllEnchantments()
{
var enchantments = WorldObject.Biota.PropertiesEnchantmentRegistry.Clone(WorldObject.BiotaDatabaseLock);
Dispel(enchantments);
}
///
/// Selects a list of spells to dispel
///
/// The dispel spell
public List SelectDispel(Spell spell)
{
// NOTE: in the default 16PY db,
// there are a lot of dispels where the actual #s do not match up with the spell descriptions...
// ie. the description will say it dispels 3-6 spells, and it will only dispel 2-4 etc.
// dispel factors:
// min_power - the minimum power level of spell to dispel (unused?)
// max_power - the maximum power level of spell to dispel
// power_variance - rng for power level, unused?
// dispel_school - the magic school to dispel, 0 if all
// align - type of spells to dispel: positive, negative, or all
// number - the maximum # of spells to dispel
// number_variance - number * number_variance = the minimum # of spells to dispel
var minPower = spell.MinPower;
var maxPower = spell.MaxPower;
var powerVariance = spell.PowerVariance;
var dispelSchool = spell.DispelSchool;
var align = spell.Align;
var number = spell.Number;
var numberVariance = spell.NumberVariance;
//var enchantments = GetEnchantments_TopLayer(WorldObject.Biota.GetEnchantments(WorldObject.BiotaDatabaseLock));
var enchantments = WorldObject.Biota.PropertiesEnchantmentRegistry.Clone(WorldObject.BiotaDatabaseLock);
var filtered = enchantments.Where(e => e.PowerLevel >= minPower && e.PowerLevel <= maxPower);
// no dispel for enchantments from item sources (and vitae)
filtered = filtered.Where(e => e.Duration != -1);
// for dispelSchool and align,
// we probably could do some calculations to figure out these values directly from the enchantments
// but it would be far easier and more reliable to just do them through the spells
// since dispels are not a time-critical function, this should still be fine
var spells = new List();
foreach (var filter in filtered)
{
var spellEnchantment = new SpellEnchantment(filter);
if (!spellEnchantment.Spell.NotFound)
spells.Add(spellEnchantment);
}
var filterSpells = spells;
if (dispelSchool != MagicSchool.None)
filterSpells = filterSpells.Where(s => s.Spell != null && s.Spell.School == dispelSchool).ToList();
if (align != DispelType.All)
{
if (align == DispelType.Positive)
filterSpells = filterSpells.Where(s => s.Spell != null && s.Spell.IsBeneficial).ToList();
else if (align == DispelType.Negative)
filterSpells = filterSpells.Where(s => s.Spell != null && s.Spell.IsHarmful).ToList();
}
// dispel all
if (number == -1)
return filterSpells;
// get number of spells to dispel
var dispelNum = number;
if (numberVariance != 1.0f)
{
var maxDispelNum = dispelNum;
var minDispelNum = (int)Math.Round(dispelNum * (1.0f - numberVariance));
// factor in rng variance
dispelNum = ThreadSafeRandom.Next(minDispelNum, maxDispelNum);
}
// randomize the filtered spell list
filterSpells.Shuffle();
// select the required # of spells
return filterSpells.Take(dispelNum).ToList();
}
///
/// Gets the VitalRate key for a CreatureVital
///
public PropertyFloat GetVitalRateKey(CreatureVital vital)
{
switch (vital.Vital)
{
case PropertyAttribute2nd.MaxHealth:
return PropertyFloat.HealthRate;
case PropertyAttribute2nd.MaxStamina:
return PropertyFloat.StaminaRate;
case PropertyAttribute2nd.MaxMana:
return PropertyFloat.ManaRate;
}
return 0;
}
///
/// Gets the ArmorModVsType key for a DamageType
///
public PropertyFloat GetImpenBaneKey(DamageType damageType)
{
switch (damageType)
{
case DamageType.Slash:
return PropertyFloat.ArmorModVsSlash;
case DamageType.Pierce:
return PropertyFloat.ArmorModVsPierce;
case DamageType.Bludgeon:
return PropertyFloat.ArmorModVsBludgeon;
case DamageType.Fire:
return PropertyFloat.ArmorModVsFire;
case DamageType.Cold:
return PropertyFloat.ArmorModVsCold;
case DamageType.Acid:
return PropertyFloat.ArmorModVsAcid;
case DamageType.Electric:
return PropertyFloat.ArmorModVsElectric;
case DamageType.Nether:
return PropertyFloat.ArmorModVsNether;
}
return 0;
}
///
/// Gets the resistance PropertyFloat for a DamageType
///
public PropertyFloat GetResistanceKey(DamageType damageType)
{
switch (damageType)
{
case DamageType.Slash:
return PropertyFloat.ResistSlash;
case DamageType.Pierce:
return PropertyFloat.ResistPierce;
case DamageType.Bludgeon:
return PropertyFloat.ResistBludgeon;
case DamageType.Fire:
return PropertyFloat.ResistFire;
case DamageType.Cold:
return PropertyFloat.ResistCold;
case DamageType.Acid:
return PropertyFloat.ResistAcid;
case DamageType.Electric:
return PropertyFloat.ResistElectric;
case DamageType.Nether:
return PropertyFloat.ResistNether;
}
return 0;
}
// refactor me
///
/// Returns the additive modifers to an attribute from enchantments
///
public virtual int GetAttributeMod_Additive(PropertyAttribute attribute)
{
var enchantments = GetEnchantments_TopLayer(EnchantmentTypeFlags.Attribute | EnchantmentTypeFlags.Additive, (uint)attribute, true);
var attributeMod = 0;
foreach (var enchantment in enchantments)
attributeMod += (int)enchantment.StatModValue;
return attributeMod;
}
///
/// Returns the multiplicative modifiers to an attribute from enchantments
///
public virtual float GetAttributeMod_Multiplier(PropertyAttribute attribute)
{
var enchantments = GetEnchantments_TopLayer(EnchantmentTypeFlags.Attribute | EnchantmentTypeFlags.Multiplicative, (uint)attribute, true);
var multiplier = 1.0f;
foreach (var enchantment in enchantments)
multiplier *= enchantment.StatModValue;
return multiplier;
}
///
/// Gets the additive modifiers to a vital / secondary attribute
///
public virtual float GetVitalMod_Additives(CreatureVital vital)
{
var enchantments = GetEnchantments_TopLayer(EnchantmentTypeFlags.SecondAtt | EnchantmentTypeFlags.Additive, (uint)vital.Vital, true);
// additive
var modifier = 0.0f;
foreach (var enchantment in enchantments)
modifier += enchantment.StatModValue;
return modifier;
}
///
/// Gets the multiplicative modifiers to a vital / secondary attribute
///
public virtual float GetVitalMod_Multiplier(CreatureVital vital)
{
// multiplicatives (asheron's lesser benediction)
var enchantments = GetEnchantments_TopLayer(EnchantmentTypeFlags.SecondAtt | EnchantmentTypeFlags.Multiplicative, (uint)vital.Vital, true);
var multiplier = 1.0f;
foreach (var enchantment in enchantments)
multiplier *= enchantment.StatModValue;
return multiplier;
}
///
/// Returns the additive bonus from XP enchantments, such as Augmented Understanding
///
public virtual float GetXPBonus()
{
var enchantments = GetEnchantments(SpellCategory.TrinketXPRaising);
// TODO: temporary code to handle both additive and multiplicative mods
// should be additive in database, update when everything is in sync
var modifier = 0.0f;
foreach (var enchantment in enchantments.OrderByDescending(i => i.PowerLevel).Take(1))
{
if (enchantment.StatModType.HasFlag(EnchantmentTypeFlags.Multiplicative))
modifier += enchantment.StatModValue - 1.0f;
else
modifier += enchantment.StatModValue;
}
return modifier;
}
///
/// Returns the additive modifiers to a skill from enchantments
///
public virtual int GetSkillMod_Additives(Skill skill)
{
var enchantments = GetEnchantments_TopLayer(EnchantmentTypeFlags.Skill | EnchantmentTypeFlags.Additive, (uint)skill, true);
var skillMod = 0;
foreach (var enchantment in enchantments)
skillMod += (int)enchantment.StatModValue;
if (SkillHelper.DefenseSkills.Contains(skill))
skillMod += GetDefenseDebuffMod();
if (SkillHelper.AttackSkills.Contains(skill))
skillMod += GetAttackDebuffMod();
return skillMod;
}
///
/// Returns the multiplicative modifiers to a skill from enchantments
///
public virtual float GetSkillMod_Multiplier(Skill skill)
{
// shroud spells
var enchantments = GetEnchantments_TopLayer(EnchantmentTypeFlags.Skill | EnchantmentTypeFlags.Multiplicative, (uint)skill, true);
var multiplier = 1.0f;
foreach (var enchantment in enchantments)
multiplier *= enchantment.StatModValue;
return multiplier;
}
///
/// Returns the sum of the StatModValues for an EnchantmentTypeFlag
///
public int GetModifier(EnchantmentTypeFlags type, bool? positive = null)
{
var enchantments = GetEnchantments_TopLayer(type);
var modifier = 0;
foreach (var enchantment in enchantments)
{
var statModVal = (int)enchantment.StatModValue;
if (positive == null || positive.Value && statModVal > 0 || !positive.Value && statModVal < 0)
{
modifier += statModVal;
}
}
return modifier;
}
///
/// Returns the sum of the modifiers for a StatModKey
///
public int GetAdditiveMod(PropertyInt statModKey)
{
var enchantments = GetEnchantments_TopLayer(EnchantmentTypeFlags.Additive, (uint)statModKey);
var modifier = 0;
foreach (var enchantment in enchantments.Where(e => (e.StatModType & EnchantmentTypeFlags.Skill) == 0))
modifier += (int)enchantment.StatModValue;
return modifier;
}
///
/// Returns the sum of the enchantment statmod values
///
public float GetAdditiveMod(List enchantments)
{
var modifier = 0.0f;
foreach (var enchantment in enchantments)
modifier += enchantment.StatModValue;
return modifier;
}
public float GetAdditiveMod(PropertyFloat statModKey)
{
var typeFlags = EnchantmentTypeFlags.Float | EnchantmentTypeFlags.SingleStat | EnchantmentTypeFlags.Additive;
var enchantments = GetEnchantments_TopLayer(typeFlags, (uint)statModKey);
var modifier = 0.0f;
foreach (var enchantment in enchantments)
modifier += enchantment.StatModValue;
return modifier;
}
///
/// Returns the product of the modifiers for a StatModKey
///
public float GetMultiplicativeMod(PropertyFloat statModKey)
{
var typeFlags = EnchantmentTypeFlags.Float | EnchantmentTypeFlags.SingleStat | EnchantmentTypeFlags.Multiplicative;
var enchantments = GetEnchantments_TopLayer(typeFlags, (uint)statModKey);
// multiplicative
var modifier = 1.0f;
foreach (var enchantment in enchantments)
modifier *= enchantment.StatModValue;
return modifier;
}
///
/// Returns the base armor modifier from enchantments
///
public virtual int GetBodyArmorMod()
{
return GetModifier(EnchantmentTypeFlags.BodyArmorValue);
}
///
/// Returns either the positive body armor from life spells (ie. Armor Self)
/// or the negative body armor (ie. Imperil)
///
public virtual int GetBodyArmorMod(bool positive)
{
return GetModifier(EnchantmentTypeFlags.BodyArmorValue, positive);
}
///
/// Gets the resistance modifier for a damage type
///
public virtual float GetResistanceMod(DamageType damageType)
{
var typeFlags = EnchantmentTypeFlags.Float | EnchantmentTypeFlags.SingleStat | EnchantmentTypeFlags.Multiplicative;
var resistance = GetResistanceKey(damageType);
var enchantments = GetEnchantments_TopLayer(typeFlags, (uint)resistance);
// multiplicative
var modifier = 1.0f;
foreach (var enchantment in enchantments)
modifier *= enchantment.StatModValue;
return modifier;
}
///
/// Gets the resistance modifier for a damage type
///
public virtual float GetProtectionResistanceMod(DamageType damageType)
{
var typeFlags = EnchantmentTypeFlags.Float | EnchantmentTypeFlags.SingleStat | EnchantmentTypeFlags.Multiplicative;
var resistance = GetResistanceKey(damageType);
var enchantments = GetEnchantments_TopLayer(typeFlags, (uint)resistance);
// multiplicative
var modifier = 1.0f;
foreach (var enchantment in enchantments)
{
if (enchantment.StatModValue < 1.0f)
modifier *= enchantment.StatModValue;
}
return modifier;
}
///
/// Gets the resistance modifier for a damage type
///
public virtual float GetVulnerabilityResistanceMod(DamageType damageType)
{
var typeFlags = EnchantmentTypeFlags.Float | EnchantmentTypeFlags.SingleStat | EnchantmentTypeFlags.Multiplicative;
var resistance = GetResistanceKey(damageType);
var enchantments = GetEnchantments_TopLayer(typeFlags, (uint)resistance);
// multiplicative
var modifier = 1.0f;
foreach (var enchantment in enchantments)
{
if (enchantment.StatModValue > 1.0f)
modifier *= enchantment.StatModValue;
}
return modifier;
}
///
/// Gets the regeneration modifier for a vital type
/// (regeneration / rejuvenation / mana renewal)
///
public virtual float GetRegenerationMod(CreatureVital vital)
{
var typeFlags = EnchantmentTypeFlags.Float | EnchantmentTypeFlags.SingleStat | EnchantmentTypeFlags.Multiplicative;
var vitalKey = GetVitalRateKey(vital);
var enchantments = GetEnchantments_TopLayer(typeFlags, (uint)vitalKey);
// multiplicative
var modifier = 1.0f;
foreach (var enchantment in enchantments)
modifier *= enchantment.StatModValue;
return modifier;
}
///
/// Returns the weapon damage bonus, ie. Blood Drinker
///
public virtual int GetDamageBonus()
{
var damageMod = GetAdditiveMod(PropertyInt.Damage);
var auraDamageMod = GetAdditiveMod(PropertyInt.WeaponAuraDamage);
// there is an unfortunate situation in the spell db,
// where blood drinker 1-7 are defined as PropertyInt.Damage
// (possibly from also being cast as direct item spells elsewhere?)
// and blood drinker 8 is properly defined as aura...
/*if (WorldObject is Creature && auraDamageMod != 0)
return auraDamageMod;
else
return damageMod;*/
return auraDamageMod + damageMod;
}
///
/// Returns the DamageMod for bow / crossbow
///
public virtual float GetDamageMod()
{
return GetAdditiveMod(PropertyFloat.DamageMod);
}
///
/// Returns the attack skill modifier, ie. Heart Seeker
///
public virtual float GetAttackMod()
{
var offenseMod = GetAdditiveMod(PropertyFloat.WeaponOffense);
var auraOffenseMod = GetAdditiveMod(PropertyFloat.WeaponAuraOffense);
/*if (WorldObject is Creature && auraOffenseMod != 0)
return auraOffenseMod;
else
return offenseMod;*/
return auraOffenseMod + offenseMod;
}
///
/// Returns the weapon speed modifier, ie. Swift Killer
///
public virtual int GetWeaponSpeedMod()
{
var speedMod = GetAdditiveMod(PropertyInt.WeaponTime);
var auraSpeedMod = GetAdditiveMod(PropertyInt.WeaponAuraSpeed);
/*if (WorldObject is Creature && auraSpeedMod != 0)
return auraSpeedMod;
else
return speedMod;*/
return auraSpeedMod + speedMod;
}
///
/// Returns the defense skill modifier, ie. Defender
///
public virtual float GetDefenseMod()
{
var defenseMod = GetAdditiveMod(PropertyFloat.WeaponDefense);
var auraDefenseMod = GetAdditiveMod(PropertyFloat.WeaponAuraDefense);
/*if (WorldObject is Creature && auraDefenseMod != 0)
return auraDefenseMod;
else
return defenseMod;*/
return auraDefenseMod + defenseMod;
}
///
/// Returns the mana conversion bonus modifier, ie. Hermetic Link / Void
///
public virtual float GetManaConvMod()
{
var manaConvMod = GetMultiplicativeMod(PropertyFloat.ManaConversionMod);
var manaConvAuraMod = GetMultiplicativeMod(PropertyFloat.WeaponAuraManaConv);
/*if (WorldObject is Creature && manaConvAuraMod != 1.0f)
return manaConvAuraMod;
else
return manaConvMod;*/
return manaConvAuraMod * manaConvMod;
}
///
/// Returns the elemental damage bonus modifier, ie. Spirit Drinker / Loather
///
public virtual float GetElementalDamageMod()
{
var elementalDamageMod = GetAdditiveMod(PropertyFloat.ElementalDamageMod);
var elementalDamageAuraMod = GetAdditiveMod(PropertyFloat.WeaponAuraElemental);
/*if (WorldObject is Creature && elementalDamageAuraMod != 0)
return elementalDamageAuraMod;
else
return elementalDamageMod;*/
return elementalDamageAuraMod + elementalDamageMod;
}
///
/// Returns the weapon damage variance modifier
///
public virtual float GetVarianceMod()
{
return GetMultiplicativeMod(PropertyFloat.DamageVariance);
}
///
/// Returns the additive armor level modifier, ie. Impenetrability
///
public virtual int GetArmorMod()
{
return GetAdditiveMod(PropertyInt.ArmorLevel);
}
///
/// Gets the additive armor level vs type modifier, ie. banes
///
public virtual float GetArmorModVsType(DamageType damageType)
{
var typeFlags = EnchantmentTypeFlags.Float | EnchantmentTypeFlags.SingleStat | EnchantmentTypeFlags.Additive;
var key = GetImpenBaneKey(damageType);
var enchantments = GetEnchantments_TopLayer(typeFlags, (uint)key);
// additive
var modifier = 0.0f;
foreach (var enchantment in enchantments)
modifier += enchantment.StatModValue;
return modifier;
}
///
/// Returns the defense skill debuffs for Dirty Fighting
///
public int GetDefenseDebuffMod()
{
var typeFlags = EnchantmentTypeFlags.Skill | EnchantmentTypeFlags.Additive | EnchantmentTypeFlags.DefenseSkills;
var enchantments = GetEnchantments_TopLayer(typeFlags, 0);
// additive
return (int)Math.Round(GetAdditiveMod(enchantments));
}
///
/// Returns the attack skill debuffs for Dirty Fighting
///
public int GetAttackDebuffMod()
{
var typeFlags = EnchantmentTypeFlags.Skill | EnchantmentTypeFlags.Additive | EnchantmentTypeFlags.AttackSkills;
var enchantments = GetEnchantments_TopLayer(typeFlags, 0);
// additive
return (int)Math.Round(GetAdditiveMod(enchantments));
}
///
/// Returns the ResistLockpick enchantment additives, ie. Strengthen/Weaken Lock
///
///
public virtual int GetResistLockpick()
{
return GetAdditiveMod(PropertyInt.ResistLockpick);
}
///
/// Returns a rating enchantment modifier
///
/// The rating to return an enchantment modifier
public virtual int GetRating(PropertyInt property)
{
var typeFlags = EnchantmentTypeFlags.Int | EnchantmentTypeFlags.SingleStat | EnchantmentTypeFlags.Additive;
var enchantments = GetEnchantments_TopLayer(typeFlags, (uint)property);
return (int)Math.Round(GetAdditiveMod(enchantments));
}
public virtual int GetNetherDotDamageRating()
{
var type = EnchantmentTypeFlags.Int | EnchantmentTypeFlags.SingleStat | EnchantmentTypeFlags.Additive;
var netherDots = GetEnchantments_TopLayer(type, (uint)PropertyInt.NetherOverTime);
// this function produces a similar value to the original ACE function,
// but is using the actual retail calculation method
var totalBaseDamage = 0.0f;
foreach (var netherDot in netherDots)
{
// normally we could just use netherDot.StatModValue here,
// but in case WorldObject has a non-default HeartbeatInterval,
// we want this value to still be based on the damage per default heartbeat interval
totalBaseDamage += GetDamagePerTick(netherDot, 5.0);
}
var rating = (int)Math.Round(totalBaseDamage / 8.0f); // thanks to Xenocide for this formula!
//Console.WriteLine($"{WorldObject.Name}.NetherDotDamageRating: {rating}");
return rating;
}
///
/// Returns the damage over time (DoT) enchantment mod
///
public float GetDamageOverTimeMod()
{
var typeFlags = EnchantmentTypeFlags.Int | EnchantmentTypeFlags.SingleStat | EnchantmentTypeFlags.Additive;
var enchantments = GetEnchantments_TopLayer(typeFlags, (uint)PropertyInt.DamageOverTime);
// additive float
return GetAdditiveMod(enchantments);
}
public static ushort SpellCategory_Cooldown = 0x8000;
///
/// Adds 0x8000 to the sharedCooldownID
///
public uint GetCooldownSpellID(int sharedCooldownID)
{
return (uint)(SpellCategory_Cooldown | sharedCooldownID);
}
///
/// Returns the seconds until this item's cooldown expires
///
public float GetCooldown(int sharedCooldownID)
{
var cooldownSpellID = GetCooldownSpellID(sharedCooldownID);
var cooldown = GetEnchantment(cooldownSpellID);
if (cooldown != null)
return (float)(cooldown.Duration - Math.Abs(cooldown.StartTime));
else
return 0.0f;
}
///
/// Returns TRUE if this item can be activated at this time
///
public bool CheckCooldown(int? sharedCooldownID)
{
if (sharedCooldownID == null)
return true;
return GetCooldown(sharedCooldownID.Value) == 0.0f;
}
///
/// Called every ~5 seconds for active object
///
public void HeartBeat(double heartbeatInterval)
{
var topLayerEnchantments = WorldObject.Biota.PropertiesEnchantmentRegistry.GetEnchantmentsTopLayer(WorldObject.BiotaDatabaseLock, SpellSet.SetSpells);
HeartBeat_DamageOverTime(topLayerEnchantments);
var expired = WorldObject.Biota.PropertiesEnchantmentRegistry.HeartBeatEnchantmentsAndReturnExpired(heartbeatInterval, WorldObject.BiotaDatabaseLock);
foreach (var enchantment in expired)
Remove(enchantment);
}
///
/// Applies damage from DoTs every ~5 seconds
///
/// A list of active enchantments at the top layers
public void HeartBeat_DamageOverTime(List enchantments)
{
var dots = new List();
var netherDots = new List();
var aetheriaDots = new List();
var heals = new List();
foreach (var enchantment in enchantments)
{
// combine DoTs from multiple sources
if (enchantment.StatModKey == (int)PropertyInt.DamageOverTime)
{
if (enchantment.SpellCategory == SpellCategory.AetheriaProcDamageOverTimeRaising)
aetheriaDots.Add(enchantment);
else
dots.Add(enchantment);
}
else if (enchantment.StatModKey == (int)PropertyInt.NetherOverTime)
netherDots.Add(enchantment);
else if (enchantment.StatModKey == (int)PropertyInt.HealOverTime)
heals.Add(enchantment);
}
// apply damage over time (DoTs)
if (dots.Count > 0)
ApplyDamageTick(dots, DamageType.Undef);
if (netherDots.Count > 0)
ApplyDamageTick(netherDots, DamageType.Nether);
if (aetheriaDots.Count > 0)
ApplyDamageTick(aetheriaDots, DamageType.Undef, true);
// apply healing over time (HoTs)
if (heals.Count > 0)
ApplyHealingTick(heals);
}
public void ApplyHealingTick(List enchantments)
{
var creature = WorldObject as Creature;
if (creature == null || creature.IsDead) return;
// get the total tick amount
var tickAmountTotal = 0.0f;
foreach (var enchantment in enchantments)
{
//var totalAmount = enchantment.StatModValue;
//var totalTicks = GetNumTicks(enchantment);
var tickAmount = enchantment.StatModValue;
tickAmountTotal += tickAmount;
}
// apply healing ratings?
tickAmountTotal *= creature.GetHealingRatingMod();
// do healing
var healAmount = creature.UpdateVitalDelta(creature.Health, (int)Math.Round(tickAmountTotal));
// account for negative HealOverTime spells, such as 5172 - Spectral Fountain Sip
if (healAmount >= 0)
creature.DamageHistory.OnHeal((uint)healAmount);
else
creature.DamageHistory.Add(creature, DamageType.Health, (uint)-healAmount);
if (creature is Player player)
player.SendMessage($"You receive {Math.Abs(healAmount)} points of periodic {((healAmount >= 0) ? "healing" : "harm")}.", PropertyManager.GetBool("aetheria_heal_color").Item ? ChatMessageType.Broadcast : ChatMessageType.Combat);
if (creature.IsDead)
{
creature.OnDeath(creature.DamageHistory.LastDamager, DamageType.Health, false);
creature.Die();
}
}
///
/// Applies 1 tick of damage from a DoT spell
///
/// The damage over time (DoT) spells
public void ApplyDamageTick(List enchantments, DamageType damageType, bool aetheria = false)
{
var creature = WorldObject as Creature;
if (creature == null || creature.IsDead) return;
bool isDead = false;
var damagers = new Dictionary();
var targetPlayer = WorldObject as Player;
// get the total tick amount
var tickAmountTotal = 0.0f;
foreach (var enchantment in enchantments)
{
//var totalAmount = enchantment.StatModValue;
//var totalTicks = GetNumTicks(enchantment);
var tickAmount = enchantment.StatModValue;
// run tick amount through damage calculation functions?
// it appears retail might have done an initial damage calc,
// and then applied that to the enchantment StatModVal beforehand
// for each damage tick, this pre-calc would then be multiplied
// against the realtime resistances
var damager = WorldObject.CurrentLandblock?.GetObject(enchantment.CasterObjectId);
if (damager == null)
{
//Console.WriteLine($"{WorldObject.Name}.ApplyDamageTick() - couldn't find damager {enchantment.CasterObjectId:X8}");
continue;
}
var resistanceMod = creature.GetResistanceMod(damageType, damager, null);
var sourcePlayer = damager as Player;
if (sourcePlayer != null && targetPlayer != null)
{
// if a PKType with Enduring Enchantment has died, ensure they don't continue to take DoT from PK sources
if (!targetPlayer.IsPKType)
continue;
// void spell projectile direct damage was modified to apply this pvp modifier *on top of* the player's natural resistance to nether,
// which supposedly brings the direct damage from void spells in pvp closer to retail
// however, dots were already supposedly on par, so we replace resistanceMod with void_pvp_modifier for dots,
// instead of applying it on top like direct damage
if (damageType == DamageType.Nether)
resistanceMod = (float)PropertyManager.GetDouble("void_pvp_modifier").Item;
}
// with the halvening, this actually seems like the fairest balance currently..
var useNetherDotDamageRating = targetPlayer != null;
var damageResistRatingMod = creature.GetDamageResistRatingMod(CombatType.Magic, useNetherDotDamageRating); // df?
if (sourcePlayer != null && targetPlayer != null)
{
var pkDamageResistRatingMod = Creature.GetNegativeRatingMod(targetPlayer.GetPKDamageResistRating());
damageResistRatingMod = Creature.AdditiveCombine(damageResistRatingMod, pkDamageResistRatingMod);
}
var dotResistRatingMod = Creature.GetNegativeRatingMod(creature.GetDotResistanceRating()); // should this be here, or somewhere else?
// should this affect NetherDotDamageRating?
//Console.WriteLine("DR: " + Creature.ModToRating(damageRatingMod));
//Console.WriteLine("DRR: " + Creature.NegativeModToRating(damageResistRatingMod));
//Console.WriteLine("NRR: " + Creature.NegativeModToRating(netherResistRatingMod));
tickAmount *= resistanceMod * damageResistRatingMod * dotResistRatingMod;
// make sure the target's current health is not exceeded
if (tickAmountTotal + tickAmount >= creature.Health.Current)
{
tickAmount = creature.Health.Current - tickAmountTotal;
isDead = true;
}
if (damagers.ContainsKey(damager))
damagers[damager] += tickAmount;
else
damagers.Add(damager, tickAmount);
creature.DamageHistory.Add(damager, damageType, (uint)Math.Round(tickAmount));
tickAmountTotal += tickAmount;
if (isDead) break;
}
creature.TakeDamageOverTime(tickAmountTotal, damageType);
if (!creature.IsAlive) return;
foreach (var kvp in damagers)
{
var damager = kvp.Key;
var amount = kvp.Value;
if (creature.Invincible)
amount = 0;
var damageSourcePlayer = damager as Player;
if (damageSourcePlayer != null)
{
creature.TakeDamageOverTime_NotifySource(damageSourcePlayer, damageType, amount, aetheria);
if (creature.IsAlive)
creature.EmoteManager.OnDamage(damageSourcePlayer);
}
}
}
///
/// Writes the EnchantmentRegistry to the network stream
///
public void SendRegistry(BinaryWriter writer)
{
if (Player == null) return;
var enchantmentRegistry = new EnchantmentRegistry(Player);
writer.Write(enchantmentRegistry);
}
///
/// Writes UpdateEnchantment vitae to the network stream
///
public void SendUpdateVitae()
{
if (Player == null) return;
var vitae = new Enchantment(Player, GetVitae());
Player.Session.Network.EnqueueSend(new GameEventMagicUpdateEnchantment(Player.Session, vitae));
}
///
/// Returns the number of ticks for a DoT enchantment
///
public int GetNumTicks(PropertiesEnchantmentRegistry enchantment, double? heartbeatInterval = null)
{
// assumed to be DoT enchantment
if (heartbeatInterval == null)
heartbeatInterval = WorldObject.HeartbeatInterval ?? 5.0;
// it's possible retail had a separate ticking mechanism for these,
// that ensured ticks every 5s, instead of heartbeat intervals
return (int)Math.Ceiling(enchantment.Duration / heartbeatInterval.Value);
}
///
/// Returns the total damage for a DoT enchantment
///
public float GetTotalDamage(PropertiesEnchantmentRegistry enchantment)
{
// assumed to be DoT enchantment
return enchantment.StatModValue * GetNumTicks(enchantment);
}
public float GetDamagePerTick(PropertiesEnchantmentRegistry enchantment, double? heartbeatInterval = null)
{
// assumed to be DoT enchantment
var creatureHeartbeatInterval = WorldObject.HeartbeatInterval ?? 5.0;
if (heartbeatInterval == null)
heartbeatInterval = creatureHeartbeatInterval;
if (heartbeatInterval == creatureHeartbeatInterval)
return enchantment.StatModValue;
// calculate the total damage w/ creature heartbeat interval
var totalDamage = GetTotalDamage(enchantment);
// divide totalDamage by the requested tick interval
var numTicks = GetNumTicks(enchantment, heartbeatInterval);
return totalDamage / numTicks;
}
}
}