using System; using System.Linq; using ACE.Common; using ACE.Entity; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Server.Entity; using ACE.Server.Entity.Actions; using ACE.Server.Network.GameEvent.Events; using ACE.Server.Network.GameMessages.Messages; using ACE.Server.Network.Structure; using ACE.Server.Physics.Animation; namespace ACE.Server.WorldObjects { partial class Creature { public enum DebugDamageType { None = 0x0, Attacker = 0x1, Defender = 0x2, All = Attacker | Defender }; public DebugDamageType DebugDamage; public ObjectGuid DebugDamageTarget; /// /// The list of combat maneuvers performable by this creature /// public DatLoader.FileTypes.CombatManeuverTable CombatTable { get; set; } public CombatMode CombatMode { get; protected set; } public AttackType AttackType { get; set; } public DamageHistory DamageHistory { get; private set; } /// /// Handles queueing up multiple animation sequences between packets /// ie., when a player switches from bow to sword combat, /// the client will send an unwield item packet for the bow first, /// queueing up a switch to peace mode, and then unarmed combat mode. /// next the client will send a wield item packet for the sword, /// queueing up the switch from unarmed combat -> peace mode -> bow combat /// public double LastWeaponSwap; public float SetCombatMode(CombatMode combatMode) { return SetCombatMode(combatMode, out var _); } /// /// Switches a player or creature to a new combat stance /// public float SetCombatMode(CombatMode combatMode, out float queueTime, bool forceHandCombat = false, bool animOnly = false) { // check if combat stance actually needs switching var combatStance = forceHandCombat ? MotionStance.HandCombat : GetCombatStance(); //Console.WriteLine($"{Name}.SetCombatMode({combatMode}), CombatStance: {combatStance}"); if (combatMode != CombatMode.NonCombat && CurrentMotionState.Stance == combatStance) { queueTime = 0.0f; return 0.0f; } if (CombatMode == CombatMode.Missile) HideAmmo(); if (!animOnly) CombatMode = combatMode; var animLength = 0.0f; switch (combatMode) { case CombatMode.NonCombat: animLength = HandleSwitchToPeaceMode(); break; case CombatMode.Melee: animLength = HandleSwitchToMeleeCombatMode(forceHandCombat); break; case CombatMode.Magic: animLength = HandleSwitchToMagicCombatMode(); break; case CombatMode.Missile: animLength = HandleSwitchToMissileCombatMode(); break; default: log.InfoFormat("Unknown combat mode {0} for {1}", CombatMode, Name); break; } queueTime = HandleStanceQueue(animLength); //Console.WriteLine($"SetCombatMode(): queueTime({queueTime}) + animLength({animLength})"); return queueTime + animLength; } /// /// Switches a player or creature to non-combat mode /// public float HandleSwitchToPeaceMode() { var animLength = MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, MotionCommand.Ready, MotionCommand.NonCombat); var motion = new Motion(MotionStance.NonCombat); ExecuteMotionPersist(motion); var player = this as Player; if (player != null) { player.stance = MotionStance.NonCombat; player.Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.NonCombat)); } //Console.WriteLine("HandleSwitchToPeaceMode() - animLength: " + animLength); return animLength; } /// /// Handles switching between combat stances: /// old style -> peace mode -> hand combat (weapon swap) -> peace mode -> new style /// public float SwitchCombatStyles() { if (CurrentMotionState.Stance == MotionStance.NonCombat || CurrentMotionState.Stance == MotionStance.Invalid || IsMonster) return 0.0f; var combatStance = GetCombatStance(); float peace1 = 0.0f, unarmed = 0.0f, peace2 = 0.0f; // this is now handled as a proper 2-step process in HandleActionChangeCombatMode / NextUseTime // FIXME: just call generic method to switch to HandCombat first peace1 = MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, MotionCommand.Ready, MotionCommand.NonCombat); /*if (CurrentMotionState.Stance != MotionStance.HandCombat && combatStance != MotionStance.HandCombat) { unarmed = MotionTable.GetAnimationLength(MotionTableId, MotionStance.NonCombat, MotionCommand.Ready, MotionCommand.HandCombat); peace2 = MotionTable.GetAnimationLength(MotionTableId, MotionStance.HandCombat, MotionCommand.Ready, MotionCommand.NonCombat); }*/ SetStance(MotionStance.NonCombat, false); //Console.WriteLine($"SwitchCombatStyle() - animLength: {animLength}"); //Console.WriteLine($"SwitchCombatStyle() - peace1({peace1}) + unarmed({unarmed}) + peace2({peace2})"); var animLength = peace1 + unarmed + peace2; return animLength; } /// /// Switches a player or creature to melee attack stance /// public float HandleSwitchToMeleeCombatMode(bool forceHandCombat = false) { // get appropriate combat stance for currently wielded items var combatStance = forceHandCombat ? MotionStance.HandCombat : GetCombatStance(); var animLength = SwitchCombatStyles(); animLength += MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, MotionCommand.Ready, (MotionCommand)combatStance); var motion = new Motion(combatStance); ExecuteMotionPersist(motion); var player = this as Player; if (player != null) { player.HandleActionTradeSwitchToCombatMode(player.Session); player.Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.Melee)); } //Console.WriteLine("HandleSwitchToMeleeCombatMode() - animLength: " + animLength); return animLength; } /// /// Switches a player or creature to magic casting stance /// public float HandleSwitchToMagicCombatMode() { var wand = GetEquippedWand(); if (wand == null) return 0.0f; var animLength = SwitchCombatStyles(); animLength += MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, MotionCommand.Ready, MotionCommand.Magic); var motion = new Motion(MotionStance.Magic); ExecuteMotionPersist(motion); var player = this as Player; if (player != null) { player.HandleActionTradeSwitchToCombatMode(player.Session); player.Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.Magic)); } //Console.WriteLine("HandleSwitchToMagicCombatMode() - animLength: " + animLength); return animLength; } /// /// Switches a player or creature to a missile combat stance /// public float HandleSwitchToMissileCombatMode() { // get appropriate combat stance for currently wielded items var weapon = GetEquippedMissileWeapon(); if (weapon == null) return 0.0f; var combatStance = GetCombatStance(); var swapTime = SwitchCombatStyles(); var motion = new Motion(combatStance); var stanceTime = ExecuteMotionPersist(motion); var ammo = GetEquippedAmmo(); var reloadTime = 0.0f; if (ammo != null && weapon.IsAmmoLauncher) { // bug for bow-wielding skeletons starting from decomposed state: // sleep -> wakeup anim time must be passed in here var actionChain = new ActionChain(); var currentTime = Time.GetUnixTime(); var queueTime = 0.0f; if (currentTime < LastWeaponSwap) queueTime += (float)(LastWeaponSwap - currentTime); actionChain.AddDelaySeconds(queueTime + swapTime + stanceTime); reloadTime = ReloadMissileAmmo(actionChain); actionChain.EnqueueChain(); } var player = this as Player; if (player != null) { player.HandleActionTradeSwitchToCombatMode(player.Session); player.Session.Network.EnqueueSend(new GameMessagePrivateUpdatePropertyInt(this, PropertyInt.CombatMode, (int)CombatMode.Missile)); } //Console.WriteLine("HandleSwitchToMissileCombatMode() - animLength: " + animLength); return swapTime + stanceTime + reloadTime; } /// /// Sends the message to hide the current equipped ammo /// public void HideAmmo() { var ammo = GetEquippedAmmo(); if (ammo != null) EnqueueBroadcast(new GameMessagePickupEvent(ammo)); } /// /// Returns the combat stance for the currently wielded items /// public MotionStance GetCombatStance() { var caster = GetEquippedWand(); if (caster != null) return MotionStance.Magic; var weapon = GetEquippedWeapon(true); var dualWield = GetDualWieldWeapon(); var shield = GetEquippedShield(); var combatStance = MotionStance.HandCombat; if (weapon != null) combatStance = GetWeaponStance(weapon); if (dualWield != null) combatStance = MotionStance.DualWieldCombat; if (shield != null) combatStance = AddShieldStance(combatStance); return combatStance; } /// /// Translates the default combat style for a weapon /// into a combat motion stance /// public MotionStance GetWeaponStance(WorldObject weapon) { var combatStance = MotionStance.HandCombat; switch (weapon.DefaultCombatStyle) { case CombatStyle.Atlatl: combatStance = MotionStance.AtlatlCombat; break; case CombatStyle.Bow: combatStance = MotionStance.BowCombat; break; case CombatStyle.Crossbow: combatStance = MotionStance.CrossbowCombat; break; case CombatStyle.DualWield: combatStance = MotionStance.DualWieldCombat; break; case CombatStyle.Magic: combatStance = MotionStance.Magic; break; case CombatStyle.OneHanded: combatStance = MotionStance.SwordCombat; break; case CombatStyle.OneHandedAndShield: combatStance = MotionStance.SwordShieldCombat; break; case CombatStyle.Sling: combatStance = MotionStance.SlingCombat; break; case CombatStyle.ThrownShield: combatStance = MotionStance.ThrownShieldCombat; break; case CombatStyle.ThrownWeapon: combatStance = MotionStance.ThrownWeaponCombat; break; case CombatStyle.TwoHanded: // MotionStance.TwoHandedStaffCombat doesn't appear to do anything // Additionally, PropertyInt.WeaponType isn't always included, and the 2handed weapons that do appear to use WeaponType.TwoHanded combatStance = MotionStance.TwoHandedSwordCombat; break; case CombatStyle.Unarmed: combatStance = MotionStance.HandCombat; break; default: Console.WriteLine($"{Name}.GetCombatStance() - {weapon.DefaultCombatStyle}"); break; } return combatStance; } /// /// Adds the shield stance to an existing combat stance /// public MotionStance AddShieldStance(MotionStance combatStance) { switch (combatStance) { case MotionStance.SwordCombat: combatStance = MotionStance.SwordShieldCombat; break; case MotionStance.ThrownWeaponCombat: combatStance = MotionStance.ThrownShieldCombat; break; } return combatStance; } /// /// Adds queued weapon swaps to the current animation time /// public float HandleStanceQueue(float animLength) { var currentTime = Time.GetUnixTime(); if (currentTime >= LastWeaponSwap) { LastWeaponSwap = currentTime + animLength; return 0.0f; } else { LastWeaponSwap += animLength; return (float)(LastWeaponSwap - currentTime - animLength); } } /// /// Returns the attack type for non-player creatures /// public virtual CombatType GetCombatType() { return CurrentAttack ?? CombatType.Melee; } /// /// Returns a value between 0.5-1.5 for non-bow attacks, /// depending on the power bar meter /// public virtual float GetPowerMod(WorldObject weapon) { // doesn't apply for non-player creatures? return 1.0f; } /// /// Returns a value between 0.6-1.6 for bow attacks, /// depending on the accuracy meter /// public virtual float GetAccuracyMod(WorldObject weapon) { // doesn't apply for non-player creatures? return 1.0f; } /// /// Returns the attribute damage bonus for a physical attack /// /// Uses strength for melee, coordination for missile public float GetAttributeMod(WorldObject weapon) { var isBow = weapon != null && weapon.IsBow; //var attribute = isBow || GetCurrentWeaponSkill() == Skill.FinesseWeapons ? Coordination : Strength; var attribute = isBow || weapon?.WeaponSkill == Skill.FinesseWeapons ? Coordination : Strength; return SkillFormula.GetAttributeMod((int)attribute.Current, isBow); } /// /// Returns the current attack skill for this monster, /// given their stance and wielded weapon /// public virtual Skill GetCurrentAttackSkill() { return GetCurrentWeaponSkill(); } /// /// Returns the current weapon skill for non-player creatures /// public virtual Skill GetCurrentWeaponSkill() { var weapon = GetEquippedWeapon(); var skill = weapon != null ? weapon.WeaponSkill : Skill.UnarmedCombat; var creatureSkill = GetCreatureSkill(skill); if (creatureSkill.InitLevel == 0) { // convert to post-MoA skill if (weapon != null && weapon.IsRanged) skill = Skill.MissileWeapons; else if (skill == Skill.Sword) skill = Skill.HeavyWeapons; else if (skill == Skill.Dagger) skill = Skill.FinesseWeapons; else skill = Skill.LightWeapons; } //Console.WriteLine("Monster weapon skill: " + skill); return skill; } /// /// Returns the effective attack skill for a non-player creature, /// ie. with Heart Seeker bonus /// public virtual uint GetEffectiveAttackSkill() { var attackSkill = GetCreatureSkill(GetCurrentAttackSkill()).Current; // TODO: don't use for bow? // https://asheron.fandom.com/wiki/Developer_Chat_-_2002/09/23 var offenseMod = GetWeaponOffenseModifier(this); // monsters don't use accuracy mod? return (uint)Math.Round(attackSkill * offenseMod); } /// /// Returns the effective defense skill for a player or creature, /// ie. with Defender bonus and imbues /// public uint GetEffectiveDefenseSkill(CombatType combatType) { var defenseSkill = combatType == CombatType.Missile ? Skill.MissileDefense : Skill.MeleeDefense; var defenseMod = defenseSkill == Skill.MissileDefense ? GetWeaponMissileDefenseModifier(this) : GetWeaponMeleeDefenseModifier(this); var burdenMod = GetBurdenMod(); var imbuedEffectType = defenseSkill == Skill.MissileDefense ? ImbuedEffectType.MissileDefense : ImbuedEffectType.MeleeDefense; var defenseImbues = GetDefenseImbues(imbuedEffectType); var stanceMod = this is Player player ? player.GetDefenseStanceMod() : 1.0f; //if (this is Player) //Console.WriteLine($"StanceMod: {stanceMod}"); var effectiveDefense = (uint)Math.Round(GetCreatureSkill(defenseSkill).Current * defenseMod * burdenMod * stanceMod + defenseImbues); if (IsExhausted) effectiveDefense = 0; return effectiveDefense; } private static double MinAttackSpeed = 0.5; private static double MaxAttackSpeed = 2.0; /// /// Returns the animation speed for an attack, /// based on the current quickness and weapon speed /// public float GetAnimSpeed() { var quickness = Quickness.Current; var weaponSpeed = GetWeaponSpeed(this); var divisor = 1.0 - (quickness / 300.0) + (weaponSpeed / 150.0); if (divisor <= 0) return (float)MaxAttackSpeed; var animSpeed = (float)Math.Clamp((1.0 / divisor), MinAttackSpeed, MaxAttackSpeed); return animSpeed; } /// /// Called when a creature evades an attack /// public virtual void OnEvade(WorldObject attacker, CombatType attackType) { // empty base for non-player creatures? } /// /// Called when a creature hits a target /// public virtual void OnDamageTarget(WorldObject target, CombatType attackType, bool critical) { // empty base for non-player creatures? } /// /// Returns the current attack height as an enumerable string /// public string GetAttackHeight() { return AttackHeight?.GetString(); } /// /// Returns the splatter height for the current attack height /// public string GetSplatterHeight() { if (AttackHeight == null) return "Mid"; switch (AttackHeight.Value) { case ACE.Entity.Enum.AttackHeight.Low: return "Low"; case ACE.Entity.Enum.AttackHeight.Medium: return "Mid"; case ACE.Entity.Enum.AttackHeight.High: default: return "Up"; } } /// /// Returns the splatter direction quadrant string /// public string GetSplatterDir(WorldObject target) { var quadrant = GetRelativeDir(target); var splatterDir = quadrant.HasFlag(Quadrant.Left) ? "Left" : "Right"; splatterDir += quadrant.HasFlag(Quadrant.Front) ? "Front" : "Back"; return splatterDir; } public double GetLifeResistance(DamageType damageType) { double resistance = 1.0; switch (damageType) { case DamageType.Slash: resistance = ResistSlashMod; break; case DamageType.Pierce: resistance = ResistPierceMod; break; case DamageType.Bludgeon: resistance = ResistBludgeonMod; break; case DamageType.Fire: resistance = ResistFireMod; break; case DamageType.Cold: resistance = ResistColdMod; break; case DamageType.Acid: resistance = ResistAcidMod; break; case DamageType.Electric: resistance = ResistElectricMod; break; case DamageType.Nether: resistance = ResistNetherMod; break; } return resistance; } /// /// Reduces a creatures's attack skill while exhausted /// public uint GetExhaustedSkill(uint attackSkill) { var halfSkill = (uint)Math.Round(attackSkill / 2.0f); uint maxPenalty = 50; var reducedSkill = attackSkill >= maxPenalty ? attackSkill - maxPenalty : 0; return Math.Max(reducedSkill, halfSkill); } /// /// Returns a divisor for the target height /// for aiming projectiles /// public virtual float GetAimHeight(WorldObject target) { return 2.0f; } /// /// Return the scalar damage absorbed by a shield /// public float GetShieldMod(WorldObject attacker, DamageType damageType, WorldObject weapon) { // ensure combat stance if (CombatMode == CombatMode.NonCombat) return 1.0f; // does the player have a shield equipped? var shield = GetEquippedShield(); if (shield == null) return 1.0f; // phantom weapons ignore all armor and shields if (weapon != null && weapon.HasImbuedEffect(ImbuedEffectType.IgnoreAllArmor)) return 1.0f; // is monster in front of player, // within shield effectiveness area? var effectiveAngle = 180.0f; var angle = GetAngle(attacker); if (Math.Abs(angle) > effectiveAngle / 2.0f) return 1.0f; // get base shield AL var baseSL = shield.GetProperty(PropertyInt.ArmorLevel) ?? 0.0f; // shield AL item enchantment additives: // impenetrability, brittlemail var ignoreMagicArmor = (weapon?.IgnoreMagicArmor ?? false) || (attacker?.IgnoreMagicArmor ?? false); var modSL = shield.EnchantmentManager.GetArmorMod(); if (ignoreMagicArmor) modSL = attacker is Player ? (int)Math.Round(IgnoreMagicArmorScaled(modSL)) : 0; var effectiveSL = baseSL + modSL; // get shield RL against damage type var baseRL = GetResistance(shield, damageType); // shield RL item enchantment additives: // banes, lures var modRL = shield.EnchantmentManager.GetArmorModVsType(damageType); if (ignoreMagicArmor) modRL = attacker is Player ? IgnoreMagicArmorScaled(modRL) : 0.0f; var effectiveRL = (float)(baseRL + modRL); // resistance clamp effectiveRL = Math.Clamp(effectiveRL, -2.0f, 2.0f); // handle negative SL //if (effectiveSL < 0 && effectiveRL != 0) //effectiveRL = 1.0f / effectiveRL; var effectiveLevel = effectiveSL * effectiveRL; // SL cap: // Trained / untrained: 1/2 shield skill // Spec: shield skill // SL cap is applied *after* item enchantments var shieldSkill = GetCreatureSkill(Skill.Shield); var shieldCap = shieldSkill.Current; if (shieldSkill.AdvancementClass != SkillAdvancementClass.Specialized) shieldCap = (uint)Math.Round(shieldCap / 2.0f); effectiveLevel = Math.Min(effectiveLevel, shieldCap); var ignoreShieldMod = attacker.GetIgnoreShieldMod(weapon); //Console.WriteLine($"IgnoreShieldMod: {ignoreShieldMod}"); effectiveLevel *= ignoreShieldMod; // SL is multiplied by existing AL var shieldMod = SkillFormula.CalcArmorMod(effectiveLevel); //Console.WriteLine("ShieldMod: " + shieldMod); return shieldMod; } /// /// Returns the total applicable Recklessness modifier, /// taking into account both attacker and defender players /// public static float GetRecklessnessMod(Creature attacker, Creature defender) { var playerAttacker = attacker as Player; var playerDefender = defender as Player; var recklessnessMod = 1.0f; // multiplicative or additive? // defender is a negative Damage Reduction Rating // 20 DR combined with 20 DRR = 1.2 * 0.8333... = 1.0 // 20 DR combined with -20 DRR = 1.2 * 1.2 = 1.44 if (playerAttacker != null) recklessnessMod *= playerAttacker.GetRecklessnessMod(); if (playerDefender != null) recklessnessMod *= playerDefender.GetRecklessnessMod(); return recklessnessMod; } public float GetSneakAttackMod(WorldObject target) { // ensure trained var sneakAttack = GetCreatureSkill(Skill.SneakAttack); if (sneakAttack.AdvancementClass < SkillAdvancementClass.Trained) return 1.0f; // ensure creature target var creatureTarget = target as Creature; if (creatureTarget == null) return 1.0f; // Effects: // General Sneak Attack effects: // - 100% chance to sneak attack from behind an opponent. // - Deception trained: 10% chance to sneak attack from the front of an opponent // - Deception specialized: 15% chance to sneak attack from the front of an opponent var angle = creatureTarget.GetAngle(this); var behind = Math.Abs(angle) > 90.0f; var chance = 0.0f; if (behind) { chance = 1.0f; } else { var deception = GetCreatureSkill(Skill.Deception); if (deception.AdvancementClass == SkillAdvancementClass.Trained) chance = 0.1f; else if (deception.AdvancementClass == SkillAdvancementClass.Specialized) chance = 0.15f; // if Deception is below 306 skill, these chances are reduced proportionately. // this is in addition to proprtional reduction if your Sneak Attack skill is below your attack skill. var deceptionCap = 306; if (deception.Current < deceptionCap) chance *= Math.Min((float)deception.Current / deceptionCap, 1.0f); } //Console.WriteLine($"Sneak attack {(behind ? "behind" : "front")}, chance {Math.Round(chance * 100)}%"); var rng = ThreadSafeRandom.Next(0.0f, 1.0f); if (rng >= chance) return 1.0f; // Damage Rating: // Sneak Attack Trained: // + 10 Damage Rating when Sneak Attack activates // Sneak Attack Specialized: // + 20 Damage Rating when Sneak Attack activates var damageRating = sneakAttack.AdvancementClass == SkillAdvancementClass.Specialized ? 20.0f : 10.0f; // Sneak Attack works for melee, missile, and magic attacks. // if the Sneak Attack skill is lower than your attack skill (as determined by your equipped weapon) // then the damage rating is reduced proportionately. Because the damage rating caps at 10 for trained // and 20 for specialized, there is no reason to raise the skill above your attack skill var attackSkill = GetCreatureSkill(GetCurrentAttackSkill()); if (sneakAttack.Current < attackSkill.Current) { if (attackSkill.Current > 0) damageRating *= (float)sneakAttack.Current / attackSkill.Current; else damageRating = 0; } // if the defender has Assess Person, they reduce the extra Sneak Attack damage Deception can add // from the front by up to 100%. // this percent is reduced proportionately if your buffed Assess Person skill is below the deception cap. // this reduction does not apply to attacks from behind. if (!behind) { // compare to assess person or deception?? // wiki info is confusing here, it says 'your buffed Assess Person' // which sounds like its scaling sourceAssess / targetAssess, // but i think it should be targetAssess / deceptionCap? var targetAssess = creatureTarget.GetCreatureSkill(Skill.AssessPerson).Current; var deceptionCap = 306; damageRating *= 1.0f - Math.Min((float)targetAssess / deceptionCap, 1.0f); } var sneakAttackMod = (100 + damageRating) / 100.0f; //Console.WriteLine("SneakAttackMod: " + sneakAttackMod); return sneakAttackMod; } public void FightDirty(WorldObject target, WorldObject weapon) { // Skill description: // Your melee and missile attacks have a chance to weaken your opponent. // - Low attacks can reduce the defense skills of the opponent. // - Medium attacks can cause small amounts of bleeding damage. // - High attacks can reduce opponents' attack and healing skills // Effects: // Low: reduces the defense skills of the opponent by -10 // Medium: bleed ticks for 60 damage per 20 seconds // High: reduces the attack skills of the opponent by -10, and // the healing effects of the opponent by -15 rating // // these damage #s are doubled for dirty fighting specialized. // Notes: // - Dirty fighting works for melee and missile attacks. // - Has a 25% chance to activate on any melee of missile attack. // - This activation is reduced proportionally if Dirty Fighting is lower // than your active weapon skill as determined by your equipped weapon. // - All activate effects last 20 seconds. // - Although a specific effect won't stack with itself, // you can stack all 3 effects on the opponent at the same time. This means // when a skill activates at one attack height, you can move to another attack height // to try to land an additional effect. // - Successfully landing a Dirty Fighting effect is mentioned in chat. Additionally, // the medium height effect results in 'floating glyphs' around the target: // "Dirty Fighting! delivers a Bleeding Assault to !" // "Dirty Fighting! delivers a Traumatic Assault to !" // dirty fighting skill must be at least trained var dirtySkill = GetCreatureSkill(Skill.DirtyFighting); if (dirtySkill.AdvancementClass < SkillAdvancementClass.Trained) return; // ensure creature target var creatureTarget = target as Creature; if (creatureTarget == null) return; var chance = 0.25f; var attackSkill = GetCreatureSkill(GetCurrentWeaponSkill()); if (dirtySkill.Current < attackSkill.Current) { chance *= (float)dirtySkill.Current / attackSkill.Current; } var rng = ThreadSafeRandom.Next(0.0f, 1.0f); if (rng >= chance) return; switch (AttackHeight) { case ACE.Entity.Enum.AttackHeight.Low: FightDirty_ApplyLowAttack(creatureTarget, weapon); break; case ACE.Entity.Enum.AttackHeight.Medium: FightDirty_ApplyMediumAttack(creatureTarget, weapon); break; case ACE.Entity.Enum.AttackHeight.High: FightDirty_ApplyHighAttack(creatureTarget, weapon); break; } } /// /// Reduces the defense skills of the opponent by /// -10 if trained, or -20 if specialized /// public void FightDirty_ApplyLowAttack(Creature target, WorldObject weapon) { var spellID = GetCreatureSkill(Skill.DirtyFighting).AdvancementClass == SkillAdvancementClass.Specialized ? SpellId.DF_Specialized_DefenseDebuff : SpellId.DF_Trained_DefenseDebuff; var spell = new Spell(spellID); if (spell.NotFound) return; // TODO: friendly message to install DF patch var addResult = target.EnchantmentManager.Add(spell, this, weapon); if (target is Player playerTarget) playerTarget.Session.Network.EnqueueSend(new GameEventMagicUpdateEnchantment(playerTarget.Session, new Enchantment(playerTarget, addResult.Enchantment))); target.EnqueueBroadcast(new GameMessageScript(target.Guid, PlayScript.DirtyFightingDefenseDebuff)); FightDirty_SendMessage(target, spell); } /// /// Applies bleed ticks for 60 damage per 20 seconds if trained, /// 120 damage per 20 seconds if specialized /// /// public void FightDirty_ApplyMediumAttack(Creature target, WorldObject weapon) { var spellID = GetCreatureSkill(Skill.DirtyFighting).AdvancementClass == SkillAdvancementClass.Specialized ? SpellId.DF_Specialized_Bleed : SpellId.DF_Trained_Bleed; var spell = new Spell(spellID); if (spell.NotFound) return; // TODO: friendly message to install DF patch var addResult = target.EnchantmentManager.Add(spell, this, weapon); if (target is Player playerTarget) playerTarget.Session.Network.EnqueueSend(new GameEventMagicUpdateEnchantment(playerTarget.Session, new Enchantment(playerTarget, addResult.Enchantment))); // only send if not already applied? target.EnqueueBroadcast(new GameMessageScript(target.Guid, PlayScript.DirtyFightingDamageOverTime)); FightDirty_SendMessage(target, spell); } /// /// Reduces the attack skills and healing rating for opponent /// by -10 if trained, or -20 if specialized /// public void FightDirty_ApplyHighAttack(Creature target, WorldObject weapon) { // attack debuff var spellID = GetCreatureSkill(Skill.DirtyFighting).AdvancementClass == SkillAdvancementClass.Specialized ? SpellId.DF_Specialized_AttackDebuff : SpellId.DF_Trained_AttackDebuff; var spell = new Spell(spellID); if (spell.NotFound) return; // TODO: friendly message to install DF patch var addResult = target.EnchantmentManager.Add(spell, this, weapon); var playerTarget = target as Player; if (playerTarget != null) playerTarget.Session.Network.EnqueueSend(new GameEventMagicUpdateEnchantment(playerTarget.Session, new Enchantment(playerTarget, addResult.Enchantment))); target.EnqueueBroadcast(new GameMessageScript(target.Guid, PlayScript.DirtyFightingAttackDebuff)); FightDirty_SendMessage(target, spell); // healing resistance rating spellID = GetCreatureSkill(Skill.DirtyFighting).AdvancementClass == SkillAdvancementClass.Specialized ? SpellId.DF_Specialized_HealingDebuff : SpellId.DF_Trained_HealingDebuff; spell = new Spell(spellID); if (spell.NotFound) return; // TODO: friendly message to install DF patch addResult = target.EnchantmentManager.Add(spell, this, weapon); if (playerTarget != null) playerTarget.Session.Network.EnqueueSend(new GameEventMagicUpdateEnchantment(playerTarget.Session, new Enchantment(playerTarget, addResult.Enchantment))); target.EnqueueBroadcast(new GameMessageScript(target.Guid, PlayScript.DirtyFightingHealDebuff)); FightDirty_SendMessage(target, spell); } public void FightDirty_SendMessage(Creature target, Spell spell) { // Dirty Fighting! delivers a Unbalancing Blow to ! //var article = spellBase.Name.StartsWithVowel() ? "an" : "a"; var msg = $"Dirty Fighting! {Name} delivers a {spell.Name} to {target.Name}!"; var playerSource = this as Player; var playerTarget = target as Player; if (playerSource != null) playerSource.SendMessage(msg, ChatMessageType.Combat, this); if (playerTarget != null) playerTarget.SendMessage(msg, ChatMessageType.Combat, this); } /// /// Returns TRUE if the creature receives a +5 DR bonus for this weapon type /// public virtual bool GetHeritageBonus(WorldObject weapon) { // only for players return false; } /// /// Returns a ResistanceType for a DamageType /// public static ResistanceType GetResistanceType(DamageType damageType) { switch (damageType) { case DamageType.Slash: return ResistanceType.Slash; case DamageType.Pierce: return ResistanceType.Pierce; case DamageType.Bludgeon: return ResistanceType.Bludgeon; case DamageType.Fire: return ResistanceType.Fire; case DamageType.Cold: return ResistanceType.Cold; case DamageType.Acid: return ResistanceType.Acid; case DamageType.Electric: return ResistanceType.Electric; case DamageType.Nether: return ResistanceType.Nether; case DamageType.Health: return ResistanceType.HealthDrain; case DamageType.Stamina: return ResistanceType.StaminaDrain; case DamageType.Mana: return ResistanceType.ManaDrain; default: return ResistanceType.Undef; } } public virtual bool CanDamage(Creature target) { if (target is Player) { // monster attacking player return true; // other checks handled elsewhere } else { // monster attacking monster var sourcePet = this is CombatPet; var targetPet = target is CombatPet; if (sourcePet || targetPet) { if (sourcePet && targetPet) // combat pets can't damage other pets return false; else if (sourcePet && target.PlayerKillerStatus == PlayerKillerStatus.PK || targetPet && PlayerKillerStatus == PlayerKillerStatus.PK) // combat pets can't damage pk-only creatures (ie. faction banners) return false; else return true; } // faction mobs if (Faction1Bits != null || target.Faction1Bits != null) { if (AllowFactionCombat(target)) return true; } // handle FoeType if (PotentialFoe(target)) return true; return false; } } public static Skill GetDefenseSkill(CombatType combatType) { switch (combatType) { case CombatType.Melee: return Skill.MeleeDefense; case CombatType.Missile: return Skill.MissileDefense; case CombatType.Magic: return Skill.MagicDefense; default: return Skill.None; } } /// /// If one of these fields is set, potential aggro from Player or CombatPet movement terminates immediately /// protected static readonly Tolerance PlayerCombatPet_MoveExclude = Tolerance.NoAttack | Tolerance.Appraise | Tolerance.Provoke | Tolerance.Retaliate | Tolerance.Monster; /// /// If one of these fields is set, potential aggro from other monster movement terminates immediately /// protected static readonly Tolerance Monster_MoveExclude = Tolerance.NoAttack | Tolerance.Appraise | Tolerance.Provoke | Tolerance.Retaliate; /// /// If one of these fields is set, potential aggro from Player or CombatPet attacks terminates immediately /// protected static readonly Tolerance PlayerCombatPet_RetaliateExclude = Tolerance.NoAttack | Tolerance.Monster; /// /// If one of these fields is set, potential aggro from monster alerts terminates immediately /// protected static readonly Tolerance AlertExclude = Tolerance.NoAttack | Tolerance.Provoke; /// /// Wakes up a monster if it can be alerted /// public bool AlertMonster(Creature monster) { // currently used for proximity checking exclusively: // Player_Monster.CheckMonsters() - player movement // Monster_Awareness.CheckTargets_Inner() - monster spawning in // Monster_Awareness.FactionMob_CheckMonsters() - faction mob scanning // non-attackable creatures do not get aggroed, // unless they have a TargetingTactic, such as the invisible archers in Oswald's Dirk Quest if (!monster.Attackable && monster.TargetingTactic == TargetingTactic.None) return false; // ensure monster is currently in idle state to wake up, // and it has no tolerance to players running nearby // TODO: investigate usage for tolerance var tolerance = this is Player ? PlayerCombatPet_MoveExclude : Monster_MoveExclude; if (monster.MonsterState != State.Idle || (monster.Tolerance & tolerance) != 0) return false; // for faction mobs, ensure alerter doesn't belong to same faction if (SameFaction(monster) && !PotentialFoe(monster)) return false; // add to retaliate targets? //Console.WriteLine($"[{Timers.RunningTime}] - {monster.Name} ({monster.Guid}) - waking up"); monster.AttackTarget = this; monster.WakeUp(); return true; } /// /// Returns the damage type for the currently equipped weapon / ammo /// /// If true, returns all of the damage types for the weapon public virtual DamageType GetDamageType(bool multiple = false, CombatType? combatType = null) { // old method, keeping intact for monsters var weapon = GetEquippedWeapon(); var ammo = GetEquippedAmmo(); if (weapon == null) return DamageType.Bludgeon; if (combatType == null) combatType = GetCombatType(); var damageSource = combatType == CombatType.Melee || ammo == null || !weapon.IsAmmoLauncher ? weapon : ammo; var damageTypes = damageSource.W_DamageType; // returning multiple damage types if (multiple) return damageTypes; // get single damage type var motion = CurrentMotionState.MotionState.ForwardCommand.ToString(); foreach (DamageType damageType in Enum.GetValues(typeof(DamageType))) { if ((damageTypes & damageType) != 0 && !damageType.IsMultiDamage()) { // handle multiple damage types if (damageType == DamageType.Slash && motion.Contains("Thrust")) continue; return damageType; } } return damageTypes; } /// /// Flag indicates which overpower formula is used /// True = Formula A / ratings method /// False = Formula B / critical defense method /// public static bool OverpowerMethod = false; public static bool GetOverpower(Creature attacker, Creature defender) { if (OverpowerMethod) return GetOverpower_Method_A(attacker, defender); else return GetOverpower_Method_B(attacker, defender); } public static bool GetOverpower_Method_A(Creature attacker, Creature defender) { // implemented similar to ratings if (attacker.Overpower == null) return false; var overpowerChance = attacker.Overpower.Value; if (defender.OverpowerResist != null) overpowerChance -= defender.OverpowerResist.Value; //Console.WriteLine($"Overpower chance: {GetOverpowerChance_Method_A(attacker, defender)}"); if (overpowerChance <= 0) return false; var rng = ThreadSafeRandom.Next(0.0f, 1.0f); return rng < overpowerChance * 0.01f; } public static bool GetOverpower_Method_B(Creature attacker, Creature defender) { // implemented similar to critical defense if (attacker.Overpower == null) return false; var overpowerChance = attacker.Overpower.Value; //Console.WriteLine($"Overpower chance: {GetOverpowerChance_Method_B(attacker, defender)}"); var rng = ThreadSafeRandom.Next(0.0f, 1.0f); if (rng >= overpowerChance * 0.01f) return false; if (defender.OverpowerResist == null) return true; var resistChance = defender.OverpowerResist.Value; rng = ThreadSafeRandom.Next(0.0f, 1.0f); return rng >= resistChance * 0.01f; } public static float GetOverpowerChance(Creature attacker, Creature defender) { if (OverpowerMethod) return GetOverpowerChance_Method_A(attacker, defender); else return GetOverpowerChance_Method_B(attacker, defender); } public static float GetOverpowerChance_Method_A(Creature attacker, Creature defender) { if (attacker.Overpower == null) return 0.0f; var overpowerChance = attacker.Overpower.Value; if (defender.OverpowerResist != null) overpowerChance -= defender.OverpowerResist.Value; if (overpowerChance <= 0) return 0.0f; return overpowerChance * 0.01f; } public static float GetOverpowerChance_Method_B(Creature attacker, Creature defender) { if (attacker.Overpower == null) return 0.0f; var overpowerChance = (attacker.Overpower ?? 0) * 0.01f; var overpowerResistChance = (defender.OverpowerResist ?? 0) * 0.01f; return overpowerChance * (1.0f - overpowerResistChance); } /// /// Returns the number of equipped items with a particular imbue type /// public int GetDefenseImbues(ImbuedEffectType imbuedEffectType) { return EquippedObjects.Values.Count(i => i.ImbuedEffect.HasFlag(imbuedEffectType)); } /// /// Returns the cloak the creature has equipped, /// or 'null' if no cloak is equipped /// public WorldObject EquippedCloak => EquippedObjects.Values.FirstOrDefault(i => i.ValidLocations == EquipMask.Cloak); /// /// Returns TRUE if creature has cloak equipped /// public bool HasCloakEquipped => EquippedCloak != null; /// /// Called when a monster attacks another monster /// This should only happen between mobs of differing factions, or from FoeType /// public void MonsterOnAttackMonster(Creature monster) { /*Console.WriteLine($"{Name}.MonsterOnAttackMonster({monster.Name})"); Console.WriteLine($"Attackable: {monster.Attackable}"); Console.WriteLine($"Tolerance: {monster.Tolerance}");*/ // when a faction mob attacks a regular mob, the regular mob will retaliate against the faction mob if (Faction1Bits != null && (monster.Faction1Bits == null || (Faction1Bits & monster.Faction1Bits) == 0)) monster.AddRetaliateTarget(this); // when a monster with a FoeType attacks a foe, the foe will retaliate if (FoeType != null && (monster.FoeType == null || monster.FoeType != CreatureType)) monster.AddRetaliateTarget(this); if (monster.MonsterState == State.Idle && !monster.Tolerance.HasFlag(Tolerance.NoAttack)) { monster.AttackTarget = this; monster.WakeUp(); } } /// /// Returns TRUE if creatures are both in the same faction /// public bool SameFaction(Creature creature) { return Faction1Bits != null && creature.Faction1Bits != null && (Faction1Bits & creature.Faction1Bits) != 0; } /// /// Returns TRUE is this creature has a FoeType that matches the input creature's CreatureType, /// or if the input creature has a FoeType that matches this creature's CreatureType /// public bool PotentialFoe(Creature creature) { return FoeType != null && FoeType == creature.CreatureType || creature.FoeType != null && creature.FoeType == CreatureType; } public bool AllowFactionCombat(Creature creature) { if (Faction1Bits == null && creature.Faction1Bits == null) return false; var factionSelf = Faction1Bits ?? FactionBits.None; var factionOther = creature.Faction1Bits ?? FactionBits.None; return (factionSelf & factionOther) == 0; } public void AddRetaliateTarget(WorldObject wo) { PhysicsObj.ObjMaint.AddRetaliateTarget(wo.PhysicsObj); } public bool HasRetaliateTarget(WorldObject wo) { if (wo != null) return PhysicsObj.ObjMaint.RetaliateTargetsContainsKey(wo.Guid.Full); else return false; } public void ClearRetaliateTargets() { PhysicsObj.ObjMaint.ClearRetaliateTargets(); } } }