using System; using System.Collections.Generic; using System.Linq; using ACE.Common; using ACE.DatLoader.Entity; using ACE.Entity; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Server.Entity; using ACE.Server.Managers; using ACE.Server.Entity.Actions; using ACE.Server.Network.Enum; using ACE.Server.Network.GameEvent.Events; using ACE.Server.Network.GameMessages.Messages; namespace ACE.Server.WorldObjects { public enum CombatType { Melee, Missile, Magic }; /// /// Handles combat with a Player as the attacker /// generalized methods for melee / missile /// partial class Player { public int AttackSequence; public bool Attacking; public bool AttackCancelled; public DateTime NextRefillTime; public double LastPkAttackTimestamp { get => GetProperty(PropertyFloat.LastPkAttackTimestamp) ?? 0; set { if (value == 0) RemoveProperty(PropertyFloat.LastPkAttackTimestamp); else SetProperty(PropertyFloat.LastPkAttackTimestamp, value); } } public double PkTimestamp { get => GetProperty(PropertyFloat.PkTimestamp) ?? 0; set { if (value == 0) RemoveProperty(PropertyFloat.PkTimestamp); else SetProperty(PropertyFloat.PkTimestamp, value); } } /// /// Returns the current attack skill for the player /// public override Skill GetCurrentAttackSkill() { if (CombatMode == CombatMode.Magic) return GetCurrentMagicSkill(); else return GetCurrentWeaponSkill(); } /// /// Returns the current weapon skill for the player /// public override Skill GetCurrentWeaponSkill() { var weapon = GetEquippedWeapon(); if (weapon?.WeaponSkill == null) return GetHighestMeleeSkill(); var skill = ConvertToMoASkill(weapon.WeaponSkill); // DualWieldAlternate will be TRUE if *next* attack is offhand if (IsDualWieldAttack && !DualWieldAlternate) { var weaponSkill = GetCreatureSkill(skill); var dualWield = GetCreatureSkill(Skill.DualWield); // offhand attacks use the lower skill level between dual wield and weapon skill if (dualWield.Current < weaponSkill.Current) skill = Skill.DualWield; } //Console.WriteLine($"{Name}.GetCurrentWeaponSkill - {skill}"); return skill; } /// /// Returns the highest melee skill for the player /// (light / heavy / finesse) /// public Skill GetHighestMeleeSkill() { var light = GetCreatureSkill(Skill.LightWeapons); var heavy = GetCreatureSkill(Skill.HeavyWeapons); var finesse = GetCreatureSkill(Skill.FinesseWeapons); var maxMelee = light; if (heavy.Current > maxMelee.Current) maxMelee = heavy; if (finesse.Current > maxMelee.Current) maxMelee = finesse; return maxMelee.Skill; } public override CombatType GetCombatType() { // this is an unsafe function, move away from this var weapon = GetEquippedWeapon(); if (weapon == null || weapon.CurrentWieldedLocation != EquipMask.MissileWeapon) return CombatType.Melee; else return CombatType.Missile; } public DamageEvent DamageTarget(Creature target, WorldObject damageSource) { if (target.Health.Current <= 0) return null; var targetPlayer = target as Player; // check PK status var pkError = CheckPKStatusVsTarget(target, null); if (pkError != null) { Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(Session, pkError[0], target.Name)); if (targetPlayer != null) targetPlayer.Session.Network.EnqueueSend(new GameEventWeenieErrorWithString(targetPlayer.Session, pkError[1], Name)); return null; } var damageEvent = DamageEvent.CalculateDamage(this, target, damageSource); if (damageEvent.HasDamage) { OnDamageTarget(target, damageEvent.CombatType, damageEvent.IsCritical); if (targetPlayer != null) targetPlayer.TakeDamage(this, damageEvent); else target.TakeDamage(this, damageEvent.DamageType, damageEvent.Damage, damageEvent.IsCritical); } else { if (damageEvent.LifestoneProtection) Session.Network.EnqueueSend(new GameMessageSystemChat($"The Lifestone's magic protects {target.Name} from the attack!", ChatMessageType.Magic)); else if (!SquelchManager.Squelches.Contains(target, ChatMessageType.CombatSelf)) Session.Network.EnqueueSend(new GameEventEvasionAttackerNotification(Session, target.Name)); if (targetPlayer != null) targetPlayer.OnEvade(this, damageEvent.CombatType); } if (damageEvent.HasDamage && target.IsAlive) { // notify attacker var intDamage = (uint)Math.Round(damageEvent.Damage); if (!SquelchManager.Squelches.Contains(this, ChatMessageType.CombatSelf)) Session.Network.EnqueueSend(new GameEventAttackerNotification(Session, target.Name, damageEvent.DamageType, (float)intDamage / target.Health.MaxValue, intDamage, damageEvent.IsCritical, damageEvent.AttackConditions)); // splatter effects if (targetPlayer == null) { Session.Network.EnqueueSend(new GameMessageSound(target.Guid, Sound.HitFlesh1, 0.5f)); if (damageEvent.Damage >= target.Health.MaxValue * 0.25f) { var painSound = (Sound)Enum.Parse(typeof(Sound), "Wound" + ThreadSafeRandom.Next(1, 3), true); Session.Network.EnqueueSend(new GameMessageSound(target.Guid, painSound, 1.0f)); } var splatter = (PlayScript)Enum.Parse(typeof(PlayScript), "Splatter" + GetSplatterHeight() + GetSplatterDir(target)); Session.Network.EnqueueSend(new GameMessageScript(target.Guid, splatter)); } // handle Dirty Fighting if (GetCreatureSkill(Skill.DirtyFighting).AdvancementClass >= SkillAdvancementClass.Trained) FightDirty(target, damageEvent.Weapon); target.EmoteManager.OnDamage(this); if (damageEvent.IsCritical) target.EmoteManager.OnReceiveCritical(this); } if (targetPlayer == null) OnAttackMonster(target); return damageEvent; } /// /// Sets the creature that last attacked a player /// This is called when the player takes damage, evades, or resists a spell from a creature /// If the CurrentAttacker has changed, sends a network message to the player's client /// This enables the 'last attacker' functionality in the client, which is bound to the 'home' key by default /// public void SetCurrentAttacker(Creature currentAttacker) { if (currentAttacker == this || CurrentAttacker == currentAttacker.Guid.Full) return; CurrentAttacker = currentAttacker.Guid.Full; Session.Network.EnqueueSend(new GameMessagePrivateUpdateInstanceID(this, PropertyInstanceId.CurrentAttacker, currentAttacker.Guid.Full)); } /// /// Called when a player hits a target /// public override void OnDamageTarget(WorldObject target, CombatType attackType, bool critical) { var attackSkill = GetCreatureSkill(GetCurrentWeaponSkill()); var difficulty = GetTargetEffectiveDefenseSkill(target); Proficiency.OnSuccessUse(this, attackSkill, difficulty); } public override uint GetEffectiveAttackSkill() { var weapon = GetEquippedWeapon(); var attackSkill = GetCreatureSkill(GetCurrentWeaponSkill()).Current; var offenseMod = GetWeaponOffenseModifier(this); var accuracyMod = GetAccuracyMod(weapon); attackSkill = (uint)Math.Round(attackSkill * accuracyMod * offenseMod); //if (IsExhausted) //attackSkill = GetExhaustedSkill(attackSkill); //var baseStr = offenseMod != 1.0f ? $" (base: {GetCreatureSkill(GetCurrentWeaponSkill()).Current})" : ""; //Console.WriteLine("Attack skill: " + attackSkill + baseStr); return attackSkill; } public uint GetTargetEffectiveDefenseSkill(WorldObject target) { var creature = target as Creature; if (creature == null) return 0; var attackType = GetCombatType(); var defenseSkill = attackType == CombatType.Missile ? Skill.MissileDefense : Skill.MeleeDefense; var defenseMod = defenseSkill == Skill.MeleeDefense ? GetWeaponMeleeDefenseModifier(creature) : 1.0f; var effectiveDefense = (uint)Math.Round(creature.GetCreatureSkill(defenseSkill).Current * defenseMod); if (creature.IsExhausted) effectiveDefense = 0; //var baseStr = defenseMod != 1.0f ? $" (base: {creature.GetCreatureSkill(defenseSkill).Current})" : ""; //Console.WriteLine("Defense skill: " + effectiveDefense + baseStr); return effectiveDefense; } /// /// Returns a modifier to the player's defense skill, based on current motion state /// /// public float GetDefenseStanceMod() { if (IsJumping) return 0.5f; if (IsLoggingOut) return 0.8f; if (CombatMode != CombatMode.NonCombat) return 1.0f; var forwardCommand = CurrentMovementData.MovementType == MovementType.Invalid && CurrentMovementData.Invalid != null ? CurrentMovementData.Invalid.State.ForwardCommand : MotionCommand.Invalid; switch (forwardCommand) { // TODO: verify multipliers case MotionCommand.Crouch: return 0.4f; case MotionCommand.Sitting: return 0.3f; case MotionCommand.Sleeping: return 0.2f; default: return 1.0f; } } /// /// Called when player successfully avoids an attack /// public override void OnEvade(WorldObject attacker, CombatType attackType) { var creatureAttacker = attacker as Creature; if (creatureAttacker != null) SetCurrentAttacker(creatureAttacker); if (UnderLifestoneProtection) return; // http://asheron.wikia.com/wiki/Attributes // Endurance will also make it less likely that you use a point of stamina to successfully evade a missile or melee attack. // A player is required to have Melee Defense for melee attacks or Missile Defense for missile attacks trained or specialized // in order for this specific ability to work. This benefit is tied to Endurance only, and it caps out at around a 75% chance // to avoid losing a point of stamina per successful evasion. var defenseSkillType = attackType == CombatType.Missile ? Skill.MissileDefense : Skill.MeleeDefense; var defenseSkill = GetCreatureSkill(defenseSkillType); if (CombatMode != CombatMode.NonCombat) { if (defenseSkill.AdvancementClass >= SkillAdvancementClass.Trained) { var enduranceBase = (int)Endurance.Base; // TODO: find exact formula / where it caps out at 75% // more literal / linear formula //var noStaminaUseChance = (enduranceBase - 50) / 320.0f; // gdle curve-based formula, caps at 300 instead of 290 var noStaminaUseChance = (enduranceBase * enduranceBase * 0.000005f) + (enduranceBase * 0.00124f) - 0.07f; noStaminaUseChance = Math.Clamp(noStaminaUseChance, 0.0f, 0.75f); //Console.WriteLine($"NoStaminaUseChance: {noStaminaUseChance}"); if (noStaminaUseChance <= ThreadSafeRandom.Next(0.0f, 1.0f)) UpdateVitalDelta(Stamina, -1); } else UpdateVitalDelta(Stamina, -1); } else { // if the player is in non-combat mode, no stamina is consumed on evade // reference: https://youtu.be/uFoQVgmSggo?t=145 // from the dm guide, page 147: "if you are not in Combat mode, you lose no Stamina when an attack is thrown at you" //UpdateVitalDelta(Stamina, -1); } if (!SquelchManager.Squelches.Contains(attacker, ChatMessageType.CombatEnemy)) Session.Network.EnqueueSend(new GameEventEvasionDefenderNotification(Session, attacker.Name)); if (creatureAttacker == null) return; var difficulty = creatureAttacker.GetCreatureSkill(creatureAttacker.GetCurrentWeaponSkill()).Current; // attackMod? Proficiency.OnSuccessUse(this, defenseSkill, difficulty); } public BaseDamageMod GetBaseDamageMod(WorldObject damageSource) { if (damageSource == this) { if (AttackType == AttackType.Punch) damageSource = HandArmor; else if (AttackType == AttackType.Kick) damageSource = FootArmor; // no weapon, no hand or foot armor if (damageSource?.Damage == null) return HeritageGroup == HeritageGroup.Olthoi ? new BaseDamageMod(new BaseDamage(130, 0.75f)) : new BaseDamageMod(new BaseDamage(2, 0.75f)); else return damageSource.GetDamageMod(this, damageSource); } return damageSource.GetDamageMod(this); } public override float GetPowerMod(WorldObject weapon) { if (weapon == null || !weapon.IsRanged) return PowerLevel + 0.5f; else return 1.0f; } public override float GetAccuracyMod(WorldObject weapon) { if (weapon != null && weapon.IsRanged) return AccuracyLevel + 0.6f; else return 1.0f; } public float GetPowerAccuracyBar() { return GetCombatType() == CombatType.Missile ? AccuracyLevel : PowerLevel; } public Sound GetHitSound(WorldObject source, BodyPart bodyPart) { /*var creature = source as Creature; var armors = creature.GetArmor(bodyPart); foreach (var armor in armors) { var material = armor.GetProperty(PropertyInt.MaterialType) ?? 0; //Console.WriteLine("Name: " + armor.Name + " | Material: " + material); }*/ return Sound.HitFlesh1; } /// /// Simplified player take damage function, only called for DoTs currently /// public override void TakeDamageOverTime(float _amount, DamageType damageType) { if (Invincible || IsDead) return; // check lifestone protection if (UnderLifestoneProtection) { HandleLifestoneProtection(); return; } var amount = (uint)Math.Round(_amount); var percent = (float)amount / Health.MaxValue; // update health var damageTaken = (uint)-UpdateVitalDelta(Health, (int)-amount); // update stamina //UpdateVitalDelta(Stamina, -1); //if (Fellowship != null) //Fellowship.OnVitalUpdate(this); // send damage text message //if (PropertyManager.GetBool("show_dot_messages").Item) //{ var nether = damageType == DamageType.Nether ? "nether " : ""; var chatMessageType = damageType == DamageType.Nether ? ChatMessageType.Magic : ChatMessageType.Combat; var text = $"You receive {amount} points of periodic {nether}damage."; SendMessage(text, chatMessageType); //} // splatter effects //var splatter = new GameMessageScript(Guid, (PlayScript)Enum.Parse(typeof(PlayScript), "Splatter" + creature.GetSplatterHeight() + creature.GetSplatterDir(this))); // not sent in retail, but great visual indicator? var splatter = new GameMessageScript(Guid, damageType == DamageType.Nether ? PlayScript.HealthDownVoid : PlayScript.DirtyFightingDamageOverTime); EnqueueBroadcast(splatter); if (Health.Current <= 0) { // since damage over time is possibly combined from multiple sources, // sending a message to the last damager here could be tricky.. // TODO: get last damager from dot stack instead? OnDeath(DamageHistory.LastDamager, damageType, false); Die(); return; } if (percent >= 0.1f) EnqueueBroadcast(new GameMessageSound(Guid, Sound.Wound1, 1.0f)); } public int TakeDamage(WorldObject source, DamageEvent damageEvent) { return TakeDamage(source, damageEvent.DamageType, damageEvent.Damage, damageEvent.BodyPart, damageEvent.IsCritical, damageEvent.AttackConditions); } /// /// Applies damages to a player from a physical damage source /// public int TakeDamage(WorldObject source, DamageType damageType, float _amount, BodyPart bodyPart, bool crit = false, AttackConditions attackConditions = AttackConditions.None) { if (Invincible || IsDead) return 0; if (source is Creature creatureAttacker) SetCurrentAttacker(creatureAttacker); // check lifestone protection if (UnderLifestoneProtection) { HandleLifestoneProtection(); return 0; } if (_amount < 0) { log.Error($"{Name}.TakeDamage({source?.Name} ({source?.Guid}), {damageType}, {_amount}) - negative damage, this shouldn't happen"); return 0; } var amount = (uint)Math.Round(_amount); var percent = (float)amount / Health.MaxValue; var equippedCloak = EquippedCloak; if (equippedCloak != null && Cloak.HasDamageProc(equippedCloak) && Cloak.RollProc(equippedCloak, percent)) { var reducedAmount = Cloak.GetReducedAmount(source, amount); Cloak.ShowMessage(this, source, amount, reducedAmount); amount = reducedAmount; percent = (float)amount / Health.MaxValue; } // update health var damageTaken = (uint)-UpdateVitalDelta(Health, (int)-amount); DamageHistory.Add(source, damageType, damageTaken); // update stamina if (CombatMode != CombatMode.NonCombat) { // if the player is in non-combat mode, no stamina is consumed on evade // reference: https://youtu.be/uFoQVgmSggo?t=145 // from the dm guide, page 147: "if you are not in Combat mode, you lose no Stamina when an attack is thrown at you" UpdateVitalDelta(Stamina, -1); } //if (Fellowship != null) //Fellowship.OnVitalUpdate(this); if (Health.Current <= 0) { OnDeath(new DamageHistoryInfo(source), damageType, crit); Die(); return (int)damageTaken; } if (!BodyParts.Indices.TryGetValue(bodyPart, out var iDamageLocation)) { log.Warn($"{Name}.TakeDamage({source.Name}, {damageType}, {amount}, {bodyPart}, {crit}): avoided crash for bad damage location"); return 0; } var damageLocation = (DamageLocation)iDamageLocation; // send network messages if (source is Creature creature) { if (!SquelchManager.Squelches.Contains(source, ChatMessageType.CombatEnemy)) Session.Network.EnqueueSend(new GameEventDefenderNotification(Session, creature.Name, damageType, percent, amount, damageLocation, crit, attackConditions)); var hitSound = new GameMessageSound(Guid, GetHitSound(source, bodyPart), 1.0f); var splatter = new GameMessageScript(Guid, (PlayScript)Enum.Parse(typeof(PlayScript), "Splatter" + creature.GetSplatterHeight() + creature.GetSplatterDir(this))); EnqueueBroadcast(hitSound, splatter); } if (percent >= 0.1f) { // Wound1 - Aahhh! - elemental attacks above some threshold // Wound2 - Deep Ugh! - bludgeoning attacks above some threshold // Wound3 - Ooh! - slashing / piercing / undef attacks above some threshold var woundSound = Sound.Wound3; if (damageType == DamageType.Bludgeon) woundSound = Sound.Wound2; else if ((damageType & DamageType.Elemental) != 0) woundSound = Sound.Wound1; EnqueueBroadcast(new GameMessageSound(Guid, woundSound, 1.0f)); } if (equippedCloak != null && Cloak.HasProcSpell(equippedCloak)) Cloak.TryProcSpell(this, source, equippedCloak, percent); // if player attacker, update PK timer if (source is Player attacker) UpdatePKTimers(attacker, this); return (int)damageTaken; } public string GetArmorType(BodyPart bodyPart) { // Flesh, Leather, Chain, Plate // for hit sounds return null; } /// /// Returns the total burden of items held in both hands /// (main hand and offhand) /// public int GetHeldItemBurden() { var mainhand = GetEquippedMainHand(); var offhand = GetEquippedOffHand(); var mainhandBurden = mainhand?.EncumbranceVal ?? 0; var offhandBurden = offhand?.EncumbranceVal ?? 0; return mainhandBurden + offhandBurden; } public float GetStaminaMod() { var endurance = (int)Endurance.Base; // more literal / linear formula var staminaMod = 1.0f - (endurance - 50) / 480.0f; // gdle curve-based formula, caps at 300 instead of 290 //var staminaMod = (endurance * endurance * -0.000003175f) - (endurance * 0.0008889f) + 1.052f; staminaMod = Math.Clamp(staminaMod, 0.5f, 1.0f); // this is also specific to gdle, // additive luck which can send the base stamina way over 1.0 /*var luck = ThreadSafeRandom.Next(0.0f, 1.0f); staminaMod += luck;*/ return staminaMod; } /// /// Calculates the amount of stamina required to perform this attack /// public int GetAttackStamina(PowerAccuracy powerAccuracy) { // Stamina cost for melee and missile attacks is based on the total burden of what you are holding // in your hands (main hand and offhand), and your power/accuracy bar. // Attacking(Low power / accuracy bar) 1 point per 700 burden units // 1 point per 1200 burden units // 1.5 points per 1600 burden units // Attacking(Mid power / accuracy bar) 1 point per 700 burden units // 2 points per 1200 burden units // 3 points per 1600 burden units // Attacking(High power / accuracy bar) 2 point per 700 burden units // 4 points per 1200 burden units // 6 points per 1600 burden units // The higher a player's base Endurance, the less stamina one uses while attacking. This benefit is tied to Endurance only, // and caps out at 50% less stamina used per attack. Scaling is similar to other Endurance bonuses. Applies only to players. // When stamina drops to 0, your melee and missile defenses also drop to 0 and you will be incapable of attacking. // In addition, you will suffer a 50% penalty to your weapon skill. This applies to players and creatures. var burden = GetHeldItemBurden(); var baseCost = StaminaTable.GetStaminaCost(powerAccuracy, burden); var staminaMod = GetStaminaMod(); var staminaCost = Math.Max(baseCost * staminaMod, 1); //Console.WriteLine($"GetAttackStamina({powerAccuracy}) - burden: {burden}, baseCost: {baseCost}, staminaMod: {staminaMod}, staminaCost: {staminaCost}"); return (int)Math.Round(staminaCost); } /// /// Returns the damage rating modifier for an applicable Recklessness attack /// /// The 0.0 - 1.0 power/accurary bar public float GetRecklessnessMod(/*float powerAccuracyBar*/) { // ensure melee or missile combat mode if (CombatMode != CombatMode.Melee && CombatMode != CombatMode.Missile) return 1.0f; var skill = GetCreatureSkill(Skill.Recklessness); // recklessness skill must be either trained or specialized to use if (skill.AdvancementClass < SkillAdvancementClass.Trained) return 1.0f; // recklessness is active when attack bar is between 20% and 80% (according to wiki) // client attack bar range seems to indicate this might have been updated, between 10% and 90%? var powerAccuracyBar = GetPowerAccuracyBar(); //if (powerAccuracyBar < 0.2f || powerAccuracyBar > 0.8f) if (powerAccuracyBar < 0.1f || powerAccuracyBar > 0.9f) return 1.0f; // recklessness only applies to non-critical hits, // which is handled outside of this method. // damage rating is increased by 20 for specialized, and 10 for trained. // incoming non-critical damage from all sources is increased by the same. var damageRating = skill.AdvancementClass == SkillAdvancementClass.Specialized ? 20 : 10; // if recklessness skill is lower than current attack skill (as determined by your equipped weapon) // then the damage rating is reduced proportionately. The damage rating caps at 10 for trained // and 20 for specialized, so there is no reason to raise the skill above your attack skill. var attackSkill = GetCreatureSkill(GetCurrentAttackSkill()); if (skill.Current < attackSkill.Current) { var scale = (float)skill.Current / attackSkill.Current; damageRating = (int)Math.Round(damageRating * scale); } // The damage rating adjustment for incoming damage is also adjusted proportinally if your Recklessness skill // is lower than your active attack skill var recklessnessMod = GetDamageRating(damageRating); // trained DR 1.10 = 10% additional damage // specialized DR 1.20 = 20% additional damage return recklessnessMod; } /// /// Returns TRUE if this player is PK and died to another player /// public bool IsPKDeath(DamageHistoryInfo topDamager) { return IsPKDeath(topDamager?.Guid.Full); } public bool IsPKDeath(uint? killerGuid) { return PlayerKillerStatus.HasFlag(PlayerKillerStatus.PK) && new ObjectGuid(killerGuid ?? 0).IsPlayer() && killerGuid != Guid.Full; } /// /// Returns TRUE if this player is PKLite and died to another player /// public bool IsPKLiteDeath(DamageHistoryInfo topDamager) { return IsPKLiteDeath(topDamager?.Guid.Full); } public bool IsPKLiteDeath(uint? killerGuid) { return PlayerKillerStatus.HasFlag(PlayerKillerStatus.PKLite) && new ObjectGuid(killerGuid ?? 0).IsPlayer() && killerGuid != Guid.Full; } public CombatMode LastCombatMode; public const float UseTimeEpsilon = 0.05f; /// /// This method processes the Game Action (F7B1) Change Combat Mode (0x0053) /// public void HandleActionChangeCombatMode(CombatMode newCombatMode, bool forceHandCombat = false, Action callback = null) { //log.Info($"{Name}.HandleActionChangeCombatMode({newCombatMode})"); // Make sure the player doesn't have an invalid weapon setup (e.g. sword + wand) if (!CheckWeaponCollision(null, null, newCombatMode)) { Session.Network.EnqueueSend(new GameEventWeenieError(Session, WeenieError.ActionCancelled)); // "Action cancelled!" // Go back to non-Combat mode float animTime = 0.0f, queueTime = 0.0f; animTime = SetCombatMode(newCombatMode, out queueTime, false, true); var actionChain = new ActionChain(); actionChain.AddDelaySeconds(animTime); actionChain.AddAction(this, () => { SetCombatMode(CombatMode.NonCombat); }); actionChain.EnqueueChain(); NextUseTime = DateTime.UtcNow.AddSeconds(animTime); return; } LastCombatMode = newCombatMode; if (DateTime.UtcNow >= NextUseTime.AddSeconds(UseTimeEpsilon)) HandleActionChangeCombatMode_Inner(newCombatMode, forceHandCombat, callback); else { var actionChain = new ActionChain(); actionChain.AddDelaySeconds((NextUseTime - DateTime.UtcNow).TotalSeconds + UseTimeEpsilon); actionChain.AddAction(this, () => HandleActionChangeCombatMode_Inner(newCombatMode, forceHandCombat, callback)); actionChain.EnqueueChain(); } if (IsAfk) HandleActionSetAFKMode(false); } public void HandleActionChangeCombatMode_Inner(CombatMode newCombatMode, bool forceHandCombat = false, Action callback = null) { //log.Info($"{Name}.HandleActionChangeCombatMode_Inner({newCombatMode})"); var currentCombatStance = GetCombatStance(); var missileWeapon = GetEquippedMissileWeapon(); var caster = GetEquippedWand(); if (CombatMode == CombatMode.Magic && MagicState.IsCasting) FailCast(); HandleActionCancelAttack(); float animTime = 0.0f, queueTime = 0.0f; switch (newCombatMode) { case CombatMode.NonCombat: { switch (currentCombatStance) { case MotionStance.BowCombat: case MotionStance.CrossbowCombat: case MotionStance.AtlatlCombat: { var equippedAmmo = GetEquippedAmmo(); if (equippedAmmo != null) ClearChild(equippedAmmo); // We must clear the placement/parent when going back to peace break; } } break; } case CombatMode.Melee: // todo expand checks if (!forceHandCombat && (missileWeapon != null || caster != null)) { // client has already independently brought the melee bar up by this point, revert and sync everything back up SetCombatMode(CombatMode.NonCombat); return; } break; case CombatMode.Missile: { if (missileWeapon == null) { // client has already independently switched to missile mode by this point, // so instead of simply returning here, we need to deny the request by reverting to either the current server combat state, or switching to NonCombat to maintain client sync // this is especially important for missile, because the client is unable to break out of this bugged state for this mode specifically // see: ClientCombatSystem::PlayerInReadyPosition SetCombatMode(CombatMode.NonCombat); return; } switch (currentCombatStance) { case MotionStance.BowCombat: case MotionStance.CrossbowCombat: case MotionStance.AtlatlCombat: { var equippedAmmo = GetEquippedAmmo(); if (equippedAmmo == null) { animTime = SetCombatMode(newCombatMode, out queueTime); var actionChain = new ActionChain(); actionChain.AddDelaySeconds(animTime); actionChain.AddAction(this, () => { Session.Network.EnqueueSend(new GameEventCommunicationTransientString(Session, "You are out of ammunition!")); SetCombatMode(CombatMode.NonCombat); }); actionChain.EnqueueChain(); NextUseTime = DateTime.UtcNow.AddSeconds(animTime); return; } else { // We must set the placement/parent when going into combat equippedAmmo.Placement = ACE.Entity.Enum.Placement.RightHandCombat; equippedAmmo.ParentLocation = ACE.Entity.Enum.ParentLocation.RightHand; } break; } } break; } case CombatMode.Magic: // todo expand checks if (caster == null) { // client has already independently brought the magic bar up by this point, revert and sync everything back up SetCombatMode(CombatMode.NonCombat); return; } break; } // animTime already includes queueTime animTime = SetCombatMode(newCombatMode, out queueTime, forceHandCombat); //log.Info($"{Name}.HandleActionChangeCombatMode_Inner({newCombatMode}) - animTime: {animTime}, queueTime: {queueTime}"); NextUseTime = DateTime.UtcNow.AddSeconds(animTime); if (MagicState.IsCasting && RecordCast.Enabled) RecordCast.OnSetCombatMode(newCombatMode); if (callback != null) { var callbackChain = new ActionChain(); callbackChain.AddDelaySeconds(animTime); callbackChain.AddAction(this, callback); callbackChain.EnqueueChain(); } } public override bool CanDamage(Creature target) { return target.Attackable && !target.Teleporting && !(target is CombatPet); } // http://acpedia.org/wiki/Announcements_-_2002/04_-_Betrayal // Some combination of strength and endurance (the two are roughly of equivalent importance) now allows one to have a level of "natural resistances" to the 7 damage types, // and to partially resist drain health and harm attacks. // This caps out at a 50% resistance (the equivalent to level 5 life prots) to these damage types. // This resistance is not additive to life protections: higher level life protections will overwrite these natural resistances, // although life vulns will take these natural resistances into account, if the player does not have a higher level life protection cast upon them. // For example, a player will not get a free protective bonus from natural resistances if they have both Prot 7 and Vuln 7 cast upon them. // The Prot and Vuln will cancel each other out, and since the Prot has overwritten the natural resistances, there will be no resistance bonus. // The natural resistances, drain resistances, and regeneration rate info are now visible on the Character Information Panel, in what was once the Burden panel. // The 5 categories for the endurance benefits are, in order from lowest benefit to highest: Poor, Mediocre, Hardy, Resilient, and Indomitable, // with each range of benefits divided up equally amongst the 5 (e.g. Poor describes having anywhere from 1-10% resistance against drain health attacks, etc.). // A few other important notes: // - The abilities that Endurance or Endurance/Strength conveys are not increased by Strength or Endurance buffs. // It is the raw Strength and/or Endurance scores that determine the various bonuses. // - For April, natural resistances will offer some protection versus hollow type damage, whether it is from a Hollow Minion or a Hollow weapon. This will be changed in May. // - These abilities are player-only, creatures with high endurance will not benefit from any of these changes. // - Come May, you can type @help endurance for a summary of the April changes to Endurance. public override float GetNaturalResistance(DamageType damageType) { if (damageType == DamageType.Undef) return 1.0f; // http://acpedia.org/wiki/Announcements_-_11th_Anniversary_Preview#Void_Magic_and_You.21 // Creatures under Asheron’s protection take half damage from any nether type spell. if (damageType == DamageType.Nether) return 0.5f; // base strength and endurance give the player a natural resistance to damage, // which caps at 50% (equivalent to level 5 life prots) // these do not stack with life protection spells // - natural resistances are ignored by hollow damage var strAndEnd = Strength.Base + Endurance.Base; if (strAndEnd <= 200) return 1.0f; var naturalResistance = 1.0f - (float)(strAndEnd - 200) / 300 * 0.5f; naturalResistance = Math.Max(naturalResistance, 0.5f); return naturalResistance; } public string GetNaturalResistanceString(ResistanceType resistanceType) { var strAndEnd = Strength.Base + Endurance.Base; if (strAndEnd > 440) return "Indomitable"; else if (strAndEnd > 380) return "Resilient"; else if (strAndEnd > 320) return "Hardy"; else if (strAndEnd > 260) return "Mediocre"; else if (strAndEnd > 200) return "Poor"; else return "None"; } public string GetRegenBonusString() { var strAndEnd = Strength.Base + 2 * Endurance.Base; if (strAndEnd > 690) return "Indomitable"; else if (strAndEnd > 580) return "Resilient"; else if (strAndEnd > 470) return "Hardy"; else if (strAndEnd > 346) return "Mediocre"; else if (strAndEnd > 200) return "Poor"; else return "None"; } /// /// If a player has been involved in a PK battle this recently, /// logging off leaves their character in a frozen state for 20 seconds /// public static TimeSpan PKLogoffTimer = TimeSpan.FromMinutes(2); public void UpdatePKTimer() { //log.Info($"Updating PK timer for {Name}"); LastPkAttackTimestamp = Time.GetUnixTime(); } /// /// Called when a successful attack is landed in PVP /// The timestamp for both PKs are updated /// /// If a physical attack is evaded, or a magic spell is resisted, /// this function should NOT be called. /// public static void UpdatePKTimers(Player attacker, Player defender) { if (attacker == defender) return; if (attacker.PlayerKillerStatus == PlayerKillerStatus.Free || defender.PlayerKillerStatus == PlayerKillerStatus.Free) return; attacker.UpdatePKTimer(); defender.UpdatePKTimer(); } public bool PKTimerActive => IsPKType && Time.GetUnixTime() - LastPkAttackTimestamp < PropertyManager.GetLong("pk_timer").Item; public bool PKLogoutActive => IsPKType && Time.GetUnixTime() - LastPkAttackTimestamp < PKLogoffTimer.TotalSeconds; public bool IsPKType => PlayerKillerStatus == PlayerKillerStatus.PK || PlayerKillerStatus == PlayerKillerStatus.PKLite; public bool IsPK => PlayerKillerStatus == PlayerKillerStatus.PK; public bool IsPKL => PlayerKillerStatus == PlayerKillerStatus.PKLite; public bool IsNPK => PlayerKillerStatus == PlayerKillerStatus.NPK; public bool CheckHouseRestrictions(Player player) { if (Location.Cell == player.Location.Cell) return true; // dealing with outdoor cell equivalents at this point, if applicable var cell = (CurrentLandblock?.IsDungeon ?? false) ? Location.Cell : Location.GetOutdoorCell(); var playerCell = (player.CurrentLandblock?.IsDungeon ?? false) ? player.Location.Cell : player.Location.GetOutdoorCell(); if (cell == playerCell) return true; HouseCell.HouseCells.TryGetValue(cell, out var houseGuid); HouseCell.HouseCells.TryGetValue(playerCell, out var playerHouseGuid); // pass if both of these players aren't in a house cell if (houseGuid == 0 && playerHouseGuid == 0) return true; var houses = new HashSet(); CheckHouseRestrictions_GetHouse(houseGuid, houses); player.CheckHouseRestrictions_GetHouse(playerHouseGuid, houses); foreach (var house in houses) { if (!house.HasPermission(this) || !house.HasPermission(player)) return false; } return true; } public void CheckHouseRestrictions_GetHouse(uint houseGuid, HashSet houses) { if (houseGuid == 0) return; var house = CurrentLandblock.GetObject(houseGuid) as House; if (house != null) { var rootHouse = house.LinkedHouses.Count > 0 ? house.LinkedHouses[0] : house; if (rootHouse.HouseOwner == null || rootHouse.OpenStatus || houses.Contains(rootHouse)) return; //Console.WriteLine($"{Name}.CheckHouseRestrictions_GetHouse({houseGuid:X8}): found root house {house.Name} ({house.HouseId})"); houses.Add(rootHouse); } else log.Error($"{Name}.CheckHouseRestrictions_GetHouse({houseGuid:X8}): couldn't find house from {CurrentLandblock.Id.Raw:X8}"); } /// /// Returns the damage type for the currently equipped weapon / ammo /// /// If true, returns all of the damage types for the weapon public override DamageType GetDamageType(bool multiple = false, CombatType? combatType = null) { // player override if (combatType == null) combatType = GetCombatType(); var weapon = GetEquippedWeapon(); var ammo = GetEquippedAmmo(); if (weapon == null && combatType == CombatType.Melee) { // handle gauntlets/ boots if (AttackType == AttackType.Punch) weapon = HandArmor; else if (AttackType == AttackType.Kick) weapon = FootArmor; else { log.Warn($"{Name}.GetDamageType(): no weapon, AttackType={AttackType}"); return DamageType.Undef; } if (weapon != null && weapon.W_DamageType == DamageType.Undef) return DamageType.Bludgeon; } if (weapon == null) return DamageType.Bludgeon; var damageSource = combatType == CombatType.Melee || ammo == null || !weapon.IsAmmoLauncher ? weapon : ammo; var damageType = damageSource.W_DamageType; if (damageType == DamageType.Undef) { log.Warn($"{Name}.GetDamageType(): {damageSource} ({damageSource.Guid}, {damageSource.WeenieClassId}): no DamageType"); return DamageType.Bludgeon; } // return multiple damage types if (multiple || !damageType.IsMultiDamage()) return damageType; // get single damage type if (damageType == (DamageType.Pierce | DamageType.Slash)) { if ((AttackType & AttackType.Punches) != 0) { if (PowerLevel < ThrustThreshold) return DamageType.Pierce; else return DamageType.Slash; } if ((AttackType & AttackType.Thrusts) != 0) return DamageType.Pierce; else return DamageType.Slash; } var powerLevel = combatType == CombatType.Melee ? (float?)PowerLevel : null; return damageType.SelectDamageType(powerLevel); } public WorldObject HandArmor => EquippedObjects.Values.FirstOrDefault(i => (i.ClothingPriority & CoverageMask.Hands) > 0); public WorldObject FootArmor => EquippedObjects.Values.FirstOrDefault(i => (i.ClothingPriority & CoverageMask.Feet) > 0); /// /// Determines if player can damage a target via PlayerKillerStatus /// /// null if no errors, else pk error list public override List CheckPKStatusVsTarget(WorldObject target, Spell spell) { if (target == null ||target == this) return null; var targetCreature = target as Creature; if (targetCreature == null && target.WielderId != null) { // handle casting item spells targetCreature = CurrentLandblock.GetObject(target.WielderId.Value) as Creature; } if (targetCreature == null) return null; if (PlayerKillerStatus == PlayerKillerStatus.Free || targetCreature.PlayerKillerStatus == PlayerKillerStatus.Free) return null; var targetPlayer = target as Player; if (targetPlayer != null) { if (spell == null || spell.IsHarmful) { // Ensure that a non-PK cannot cast harmful spells on another player if (PlayerKillerStatus == PlayerKillerStatus.NPK) return new List() { WeenieErrorWithString.YouFailToAffect_YouAreNotPK, WeenieErrorWithString._FailsToAffectYou_TheyAreNotPK }; if (targetPlayer.PlayerKillerStatus == PlayerKillerStatus.NPK) return new List() { WeenieErrorWithString.YouFailToAffect_TheyAreNotPK, WeenieErrorWithString._FailsToAffectYou_YouAreNotPK }; // Ensure not attacking across housing boundary if (!CheckHouseRestrictions(targetPlayer)) return new List() { WeenieErrorWithString.YouFailToAffect_AcrossHouseBoundary, WeenieErrorWithString._FailsToAffectYouAcrossHouseBoundary }; } // additional checks for different PKTypes if (PlayerKillerStatus != targetPlayer.PlayerKillerStatus) { // require same pk status, unless beneficial spell being cast on NPK // https://asheron.fandom.com/wiki/Player_Killer // https://asheron.fandom.com/wiki/Player_Killer_Lite if (spell == null || spell.IsHarmful || targetPlayer.PlayerKillerStatus != PlayerKillerStatus.NPK) return new List() { WeenieErrorWithString.YouFailToAffect_NotSamePKType, WeenieErrorWithString._FailsToAffectYou_NotSamePKType }; } } else { // if monster has a non-default pk status, ensure pk types match up if (targetCreature.PlayerKillerStatus != PlayerKillerStatus.NPK && PlayerKillerStatus != targetCreature.PlayerKillerStatus) { return new List() { WeenieErrorWithString.YouFailToAffect_NotSamePKType, WeenieErrorWithString._FailsToAffectYou_NotSamePKType }; } } return null; } } }