using System; using System.Collections.Generic; using System.Linq; using log4net; using ACE.Common; using ACE.DatLoader.Entity.AnimationHooks; using ACE.Entity.Enum; using ACE.Entity.Models; using ACE.Server.Managers; using ACE.Server.Network.GameMessages.Messages; using ACE.Server.WorldObjects; namespace ACE.Server.Entity { public class DamageEvent { private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); // factors: // - lifestone protection // - evade // - offense mod (heart seeker) // - accuracy mod (missile) // - defense mod (defender) // - stamina mod // - base damage / mod // - damage rating / mod // - recklessness // - sneak attack // - heritage bonus // - damage resistance rating /mod // - power meter mod // - critical (chance % mod / critical damage mod) // - attribute mod // - armor / mod (base al, impen / bane, life armor / imperil) // - elemental damage bonus // - slayer mod // - resistance mod (natural, prot, vuln) // - resistance cleaving // - shield mod // - rending mod public Creature Attacker; public Creature Defender; public CombatType CombatType; // melee / missile / magic public WorldObject DamageSource; public DamageType DamageType; public WorldObject Weapon; // the attacker's weapon. this can be different from DamageSource, // ie. for a missile attack, the missile would the DamageSource, // and the buffs would come from the Weapon public AttackType AttackType; // slash / thrust / punch / kick / offhand / multistrike public AttackHeight AttackHeight; public bool LifestoneProtection; public float EvasionChance; public uint EffectiveAttackSkill; public uint EffectiveDefenseSkill; public float AccuracyMod; public bool Evaded; public BaseDamageMod BaseDamageMod; public float BaseDamage { get; set; } public float AttributeMod; public float PowerMod; public float SlayerMod; public float DamageRatingBaseMod; public float RecklessnessMod; public float SneakAttackMod; public float HeritageMod; public float PkDamageMod; public float DamageRatingMod; public bool IsCritical; public float CriticalChance; public float CriticalDamageMod; public float CriticalDamageRatingMod; public float CriticalDamageResistanceRatingMod; public float DamageBeforeMitigation; public float ArmorMod; public float ResistanceMod; public float ShieldMod; public float WeaponResistanceMod; public float DamageResistanceRatingBaseMod; public float DamageResistanceRatingMod; public float PkDamageResistanceMod; public float DamageMitigated; // creature attacker public MotionCommand? AttackMotion; public AttackHook AttackHook; public KeyValuePair AttackPart; // the body part this monster is attacking with // creature defender public Quadrant Quadrant; public bool IgnoreMagicArmor => (Weapon?.IgnoreMagicArmor ?? false) || (Attacker?.IgnoreMagicArmor ?? false); // ignores impen / banes public bool IgnoreMagicResist => (Weapon?.IgnoreMagicResist ?? false) || (Attacker?.IgnoreMagicResist ?? false); // ignores life armor / prots public bool Overpower; // player defender public BodyPart BodyPart; public List Armor; // creature defender public KeyValuePair PropertiesBodyPart; public Creature_BodyPart CreaturePart; public float Damage; public bool GeneralFailure; public bool HasDamage => !Evaded && !LifestoneProtection; public bool CriticalDefended; public static HashSet AllowDamageTypeUndef = new HashSet() { 22545, // Obsidian Spines 35191, // Thunder Chicken 38406, // Blessed Moar 38587, // Ardent Moar 38588, // Blessed Moar 38586, // Verdant Moar 40298, // Ardent Moar 40300, // Blessed Moar 40301, // Verdant Moar }; public static DamageEvent CalculateDamage(Creature attacker, Creature defender, WorldObject damageSource, MotionCommand? attackMotion = null, AttackHook attackHook = null) { var damageEvent = new DamageEvent(); damageEvent.AttackMotion = attackMotion; damageEvent.AttackHook = attackHook; if (damageSource == null) damageSource = attacker; var damage = damageEvent.DoCalculateDamage(attacker, defender, damageSource); damageEvent.HandleLogging(attacker, defender); return damageEvent; } private float DoCalculateDamage(Creature attacker, Creature defender, WorldObject damageSource) { var playerAttacker = attacker as Player; var playerDefender = defender as Player; var pkBattle = playerAttacker != null && playerDefender != null; Attacker = attacker; Defender = defender; CombatType = damageSource.ProjectileSource == null ? CombatType.Melee : CombatType.Missile; DamageSource = damageSource; Weapon = damageSource.ProjectileSource == null ? attacker.GetEquippedMeleeWeapon() : (damageSource.ProjectileLauncher ?? damageSource.ProjectileAmmo); AttackType = attacker.AttackType; AttackHeight = attacker.AttackHeight ?? AttackHeight.Medium; // check lifestone protection if (playerDefender != null && playerDefender.UnderLifestoneProtection) { LifestoneProtection = true; playerDefender.HandleLifestoneProtection(); return 0.0f; } if (defender.Invincible) return 0.0f; // overpower if (attacker.Overpower != null) Overpower = Creature.GetOverpower(attacker, defender); // evasion chance if (!Overpower) { EvasionChance = GetEvadeChance(attacker, defender); if (EvasionChance > ThreadSafeRandom.Next(0.0f, 1.0f)) { Evaded = true; return 0.0f; } } // get base damage if (playerAttacker != null) GetBaseDamage(playerAttacker); else GetBaseDamage(attacker, AttackMotion ?? MotionCommand.Invalid, AttackHook); if (DamageType == DamageType.Undef) { if ((attacker?.Guid.IsPlayer() ?? false) || (damageSource?.Guid.IsPlayer() ?? false)) { log.Error($"DamageEvent.DoCalculateDamage({attacker?.Name} ({attacker?.Guid}), {defender?.Name} ({defender?.Guid}), {damageSource?.Name} ({damageSource?.Guid})) - DamageType == DamageType.Undef"); GeneralFailure = true; } } if (GeneralFailure) return 0.0f; // get damage modifiers PowerMod = attacker.GetPowerMod(Weapon); AttributeMod = attacker.GetAttributeMod(Weapon); SlayerMod = WorldObject.GetWeaponCreatureSlayerModifier(Weapon, attacker, defender); // ratings DamageRatingBaseMod = Creature.GetPositiveRatingMod(attacker.GetDamageRating()); RecklessnessMod = Creature.GetRecklessnessMod(attacker, defender); SneakAttackMod = attacker.GetSneakAttackMod(defender); HeritageMod = attacker.GetHeritageBonus(Weapon) ? 1.05f : 1.0f; DamageRatingMod = Creature.AdditiveCombine(DamageRatingBaseMod, RecklessnessMod, SneakAttackMod, HeritageMod); if (pkBattle) { PkDamageMod = Creature.GetPositiveRatingMod(attacker.GetPKDamageRating()); DamageRatingMod = Creature.AdditiveCombine(DamageRatingMod, PkDamageMod); } // damage before mitigation DamageBeforeMitigation = BaseDamage * AttributeMod * PowerMod * SlayerMod * DamageRatingMod; // critical hit? var attackSkill = attacker.GetCreatureSkill(attacker.GetCurrentWeaponSkill()); CriticalChance = WorldObject.GetWeaponCriticalChance(Weapon, attacker, attackSkill, defender); // https://asheron.fandom.com/wiki/Announcements_-_2002/08_-_Atonement // It should be noted that any time a character is logging off, PK or not, all physical attacks against them become automatically critical. // (Note that spells do not share this behavior.) We hope this will stress the need to log off in a safe place. if (playerDefender != null && (playerDefender.IsLoggingOut || playerDefender.PKLogout)) CriticalChance = 1.0f; if (CriticalChance > ThreadSafeRandom.Next(0.0f, 1.0f)) { if (playerDefender != null && playerDefender.AugmentationCriticalDefense > 0) { var criticalDefenseMod = playerAttacker != null ? 0.05f : 0.25f; var criticalDefenseChance = playerDefender.AugmentationCriticalDefense * criticalDefenseMod; if (criticalDefenseChance > ThreadSafeRandom.Next(0.0f, 1.0f)) CriticalDefended = true; } if (!CriticalDefended) { IsCritical = true; // verify: CriticalMultiplier only applied to the additional crit damage, // whereas CD/CDR applied to the total damage (base damage + additional crit damage) CriticalDamageMod = 1.0f + WorldObject.GetWeaponCritDamageMod(Weapon, attacker, attackSkill, defender); CriticalDamageRatingMod = Creature.GetPositiveRatingMod(attacker.GetCritDamageRating()); // recklessness excluded from crits RecklessnessMod = 1.0f; DamageRatingMod = Creature.AdditiveCombine(DamageRatingBaseMod, CriticalDamageRatingMod, SneakAttackMod, HeritageMod); if (pkBattle) DamageRatingMod = Creature.AdditiveCombine(DamageRatingMod, PkDamageMod); DamageBeforeMitigation = BaseDamageMod.MaxDamage * AttributeMod * PowerMod * SlayerMod * DamageRatingMod * CriticalDamageMod; } } // armor rending and cleaving var armorRendingMod = 1.0f; if (Weapon != null && Weapon.HasImbuedEffect(ImbuedEffectType.ArmorRending)) armorRendingMod = WorldObject.GetArmorRendingMod(attackSkill); var armorCleavingMod = attacker.GetArmorCleavingMod(Weapon); var ignoreArmorMod = Math.Min(armorRendingMod, armorCleavingMod); // get body part / armor pieces / armor modifier if (playerDefender != null) { // select random body part @ current attack height GetBodyPart(AttackHeight); // get player armor pieces Armor = attacker.GetArmorLayers(playerDefender, BodyPart); // get armor modifiers ArmorMod = attacker.GetArmorMod(playerDefender, DamageType, Armor, Weapon, ignoreArmorMod); } else { // determine height quadrant Quadrant = GetQuadrant(Defender, Attacker, AttackHeight, DamageSource); // select random body part @ current attack height GetBodyPart(Defender, Quadrant); if (Evaded) return 0.0f; Armor = CreaturePart.GetArmorLayers(PropertiesBodyPart.Key); // get target armor ArmorMod = CreaturePart.GetArmorMod(DamageType, Armor, Attacker, Weapon, ignoreArmorMod); } if (Weapon != null && Weapon.HasImbuedEffect(ImbuedEffectType.IgnoreAllArmor)) ArmorMod = 1.0f; // get resistance modifiers WeaponResistanceMod = WorldObject.GetWeaponResistanceModifier(Weapon, attacker, attackSkill, DamageType); if (playerDefender != null) { ResistanceMod = playerDefender.GetResistanceMod(DamageType, Attacker, Weapon, WeaponResistanceMod); } else { var resistanceType = Creature.GetResistanceType(DamageType); ResistanceMod = (float)Math.Max(0.0f, defender.GetResistanceMod(resistanceType, Attacker, Weapon, WeaponResistanceMod)); } // damage resistance rating DamageResistanceRatingMod = DamageResistanceRatingBaseMod = defender.GetDamageResistRatingMod(CombatType); if (IsCritical) { CriticalDamageResistanceRatingMod = Creature.GetNegativeRatingMod(defender.GetCritDamageResistRating()); DamageResistanceRatingMod = Creature.AdditiveCombine(DamageResistanceRatingBaseMod, CriticalDamageResistanceRatingMod); } if (pkBattle) { PkDamageResistanceMod = Creature.GetNegativeRatingMod(defender.GetPKDamageResistRating()); DamageResistanceRatingMod = Creature.AdditiveCombine(DamageResistanceRatingMod, PkDamageResistanceMod); } // get shield modifier ShieldMod = defender.GetShieldMod(attacker, DamageType, Weapon); // calculate final output damage Damage = DamageBeforeMitigation * ArmorMod * ShieldMod * ResistanceMod * DamageResistanceRatingMod; DamageMitigated = DamageBeforeMitigation - Damage; return Damage; } public Quadrant GetQuadrant(Creature defender, Creature attacker, AttackHeight attackHeight, WorldObject damageSource) { var quadrant = attackHeight.ToQuadrant(); var wo = damageSource.CurrentLandblock != null ? damageSource : attacker; quadrant |= wo.GetRelativeDir(defender); return quadrant; } /// /// Returns the chance for creature to avoid monster attack /// public float GetEvadeChance(Creature attacker, Creature defender) { AccuracyMod = attacker.GetAccuracyMod(Weapon); EffectiveAttackSkill = attacker.GetEffectiveAttackSkill(); //var attackType = attacker.GetCombatType(); EffectiveDefenseSkill = defender.GetEffectiveDefenseSkill(CombatType); var evadeChance = 1.0f - SkillCheck.GetSkillChance(EffectiveAttackSkill, EffectiveDefenseSkill); return (float)evadeChance; } /// /// Returns the base damage for a player attacker /// public void GetBaseDamage(Player attacker) { if (DamageSource.ItemType == ItemType.MissileWeapon) { DamageType = DamageSource.W_DamageType; // handle prismatic arrows if (DamageType == DamageType.Base) { if (Weapon != null && Weapon.W_DamageType != DamageType.Undef) DamageType = Weapon.W_DamageType; else DamageType = DamageType.Pierce; } } else DamageType = attacker.GetDamageType(false, CombatType.Melee); // TODO: combat maneuvers for player? BaseDamageMod = attacker.GetBaseDamageMod(DamageSource); // some quest bows can have built-in damage bonus if (Weapon?.WeenieType == WeenieType.MissileLauncher) BaseDamageMod.DamageBonus += Weapon.Damage ?? 0; if (DamageSource.ItemType == ItemType.MissileWeapon) BaseDamageMod.ElementalBonus = WorldObject.GetMissileElementalDamageBonus(Weapon, attacker, DamageType); BaseDamage = (float)ThreadSafeRandom.Next(BaseDamageMod.MinDamage, BaseDamageMod.MaxDamage); } /// /// Returns the base damage for a non-player attacker /// public void GetBaseDamage(Creature attacker, MotionCommand motionCommand, AttackHook attackHook) { AttackPart = attacker.GetAttackPart(motionCommand, attackHook); if (AttackPart.Value == null) { GeneralFailure = true; return; } BaseDamageMod = attacker.GetBaseDamage(AttackPart.Value); BaseDamage = (float)ThreadSafeRandom.Next(BaseDamageMod.MinDamage, BaseDamageMod.MaxDamage); DamageType = attacker.GetDamageType(AttackPart.Value, CombatType); } /// /// Returns a body part for a player defender /// public void GetBodyPart(AttackHeight attackHeight) { // select random body part @ current attack height BodyPart = BodyParts.GetBodyPart(attackHeight); } public static readonly Quadrant LeftRight = Quadrant.Left | Quadrant.Right; public static readonly Quadrant FrontBack = Quadrant.Front | Quadrant.Back; /// /// Returns a body part for a creature defender /// public void GetBodyPart(Creature defender, Quadrant quadrant) { // get cached body parts table var bodyParts = Creature.GetBodyParts(defender.WeenieClassId); if (bodyParts == null) { Evaded = true; return; } // rng roll for body part var bodyPart = bodyParts.RollBodyPart(quadrant); if (bodyPart == CombatBodyPart.Undefined) { log.DebugFormat("DamageEvent.GetBodyPart({0} ({1}) ) - couldn't find body part for wcid {2}, Quadrant {3}", defender?.Name, defender?.Guid, defender.WeenieClassId, quadrant); Evaded = true; return; } //Console.WriteLine($"AttackHeight: {AttackHeight}, Quadrant: {quadrant & FrontBack}{quadrant & LeftRight}, AttackPart: {bodyPart}"); defender.Biota.PropertiesBodyPart.TryGetValue(bodyPart, out var value); PropertiesBodyPart = new KeyValuePair(bodyPart, value); // select random body part @ current attack height /*BiotaPropertiesBodyPart = BodyParts.GetBodyPart(defender, attackHeight); if (BiotaPropertiesBodyPart == null) { Evaded = true; return; }*/ CreaturePart = new Creature_BodyPart(defender, PropertiesBodyPart); } public void ShowInfo(Creature creature) { var targetInfo = PlayerManager.GetOnlinePlayer(creature.DebugDamageTarget); if (targetInfo == null) { creature.DebugDamage = Creature.DebugDamageType.None; return; } // setup var info = $"Attacker: {Attacker.Name} ({Attacker.Guid})\n"; info += $"Defender: {Defender.Name} ({Defender.Guid})\n"; info += $"CombatType: {CombatType}\n"; info += $"DamageSource: {DamageSource.Name} ({DamageSource.Guid})\n"; info += $"DamageType: {DamageType}\n"; var weaponName = Weapon != null ? $"{Weapon.Name} ({Weapon.Guid})" : "None\n"; info += $"Weapon: {weaponName}\n"; info += $"AttackType: {AttackType}\n"; info += $"AttackHeight: {AttackHeight}\n"; // lifestone protection if (LifestoneProtection) info += $"LifestoneProtection: {LifestoneProtection}\n"; // evade if (AccuracyMod != 0.0f && AccuracyMod != 1.0f) info += $"AccuracyMod: {AccuracyMod}\n"; info += $"EffectiveAttackSkill: {EffectiveAttackSkill}\n"; info += $"EffectiveDefenseSkill: {EffectiveDefenseSkill}\n"; if (Attacker.Overpower != null) info += $"Overpower: {Overpower} ({Creature.GetOverpowerChance(Attacker, Defender)})\n"; info += $"EvasionChance: {EvasionChance}\n"; info += $"Evaded: {Evaded}\n"; if (!(Attacker is Player)) { if (AttackMotion != null) info += $"AttackMotion: {AttackMotion}\n"; if (AttackPart.Value != null) info += $"AttackPart: {AttackPart.Key}\n"; } // base damage if (BaseDamageMod != null) info += $"BaseDamageRange: {BaseDamageMod.Range}\n"; info += $"BaseDamage: {BaseDamage}\n"; // damage modifiers info += $"AttributeMod: {AttributeMod}\n"; if (PowerMod != 0.0f && PowerMod != 1.0f) info += $"PowerMod: {PowerMod}\n"; if (SlayerMod != 0.0f && SlayerMod != 1.0f) info += $"SlayerMod: {SlayerMod}\n"; if (BaseDamageMod != null) { if (BaseDamageMod.DamageBonus != 0) info += $"DamageBonus: {BaseDamageMod.DamageBonus}\n"; if (BaseDamageMod.DamageMod != 0.0f && BaseDamageMod.DamageMod != 1.0f) info += $"DamageMod: {BaseDamageMod.DamageMod}\n"; if (BaseDamageMod.ElementalBonus != 0) info += $"ElementalDamageBonus: {BaseDamageMod.ElementalBonus}\n"; } // critical hit info += $"CriticalChance: {CriticalChance}\n"; info += $"CriticalHit: {IsCritical}\n"; if (CriticalDefended) info += $"CriticalDefended: {CriticalDefended}\n"; if (CriticalDamageMod != 0.0f && CriticalDamageMod != 1.0f) info += $"CriticalDamageMod: {CriticalDamageMod}\n"; if (CriticalDamageRatingMod != 0.0f && CriticalDamageRatingMod != 1.0f) info += $"CriticalDamageRatingMod: {CriticalDamageRatingMod}\n"; // damage ratings if (DamageRatingBaseMod != 0.0f && DamageRatingBaseMod != 1.0f) info += $"DamageRatingBaseMod: {DamageRatingBaseMod}\n"; if (HeritageMod != 0.0f && HeritageMod != 1.0f) info += $"HeritageMod: {HeritageMod}\n"; if (RecklessnessMod != 0.0f && RecklessnessMod != 1.0f) info += $"RecklessnessMod: {RecklessnessMod}\n"; if (SneakAttackMod != 0.0f && SneakAttackMod != 1.0f) info += $"SneakAttackMod: {SneakAttackMod}\n"; if (PkDamageMod != 0.0f && PkDamageMod != 1.0f) info += $"PkDamageMod: {PkDamageMod}\n"; if (DamageRatingMod != 0.0f && DamageRatingMod != 1.0f) info += $"DamageRatingMod: {DamageRatingMod}\n"; if (BodyPart != 0) { // player body part info += $"BodyPart: {BodyPart}\n"; } if (Armor != null && Armor.Count > 0) { info += $"Armors: {string.Join(", ", Armor.Select(i => i.Name))}\n"; } if (CreaturePart != null) { // creature body part info += $"BodyPart: {PropertiesBodyPart.Key}\n"; info += $"BaseArmor: {CreaturePart.Biota.Value.BaseArmor}\n"; } // damage mitigation if (ArmorMod != 0.0f && ArmorMod != 1.0f) info += $"ArmorMod: {ArmorMod}\n"; if (ResistanceMod != 0.0f && ResistanceMod != 1.0f) info += $"ResistanceMod: {ResistanceMod}\n"; if (ShieldMod != 0.0f && ShieldMod != 1.0f) info += $"ShieldMod: {ShieldMod}\n"; if (WeaponResistanceMod != 0.0f && WeaponResistanceMod != 1.0f) info += $"WeaponResistanceMod: {WeaponResistanceMod}\n"; if (DamageResistanceRatingBaseMod != 0.0f && DamageResistanceRatingBaseMod != 1.0f) info += $"DamageResistanceRatingBaseMod: {DamageResistanceRatingBaseMod}\n"; if (CriticalDamageResistanceRatingMod != 0.0f && CriticalDamageResistanceRatingMod != 1.0f) info += $"CriticalDamageResistanceRatingMod: {CriticalDamageResistanceRatingMod}\n"; if (PkDamageResistanceMod != 0.0f && PkDamageResistanceMod != 1.0f) info += $"PkDamageResistanceMod: {PkDamageResistanceMod}\n"; if (DamageResistanceRatingMod != 0.0f && DamageResistanceRatingMod != 1.0f) info += $"DamageResistanceRatingMod: {DamageResistanceRatingMod}\n"; if (IgnoreMagicArmor) info += $"IgnoreMagicArmor: {IgnoreMagicArmor}\n"; if (IgnoreMagicResist) info += $"IgnoreMagicResist: {IgnoreMagicResist}\n"; // final damage info += $"DamageBeforeMitigation: {DamageBeforeMitigation}\n"; info += $"DamageMitigated: {DamageMitigated}\n"; info += $"Damage: {Damage}\n"; info += "----"; targetInfo.Session.Network.EnqueueSend(new GameMessageSystemChat(info, ChatMessageType.Broadcast)); } public void HandleLogging(Creature attacker, Creature defender) { if (attacker != null && (attacker.DebugDamage & Creature.DebugDamageType.Attacker) != 0) { ShowInfo(attacker); } if (defender != null && (defender.DebugDamage & Creature.DebugDamageType.Defender) != 0) { ShowInfo(defender); } } public AttackConditions AttackConditions { get { var attackConditions = new AttackConditions(); if (CriticalDefended) attackConditions |= AttackConditions.CriticalProtectionAugmentation; if (RecklessnessMod > 1.0f) attackConditions |= AttackConditions.Recklessness; if (SneakAttackMod > 1.0f) attackConditions |= AttackConditions.SneakAttack; if (Overpower) attackConditions |= AttackConditions.Overpower; return attackConditions; } } } }