ace/Source/ACE.Server/WorldObjects/Player_Combat.cs
Coldeve-ACE a31112ea51
Common Logging Changes (#4345)
* Update ShardDatabaseOfflineTools.cs

* Update Corpse.cs

* Update Creature_Death.cs

* Update Player_Death.cs

* Update Player_Inventory.cs

* Update WorldObject_Decay.cs

* Update HouseManager.cs

* Update Player_House.cs

* Update RecipeManager.cs

* Update Player_Allegiance.cs

* Update AuthenticationDatabase.cs

* Update ShardDatabase.cs

* Update WorldDatabase.cs

* Update Player_Magic.cs

* Update Player_Skills.cs

* Update Player_Attributes.cs

* Update Player_Combat.cs

* Update PhysicsObj.cs

* Update Player_Vitals.cs

* Update Player_Missile.cs

* Update Player_Melee.cs

* Update Monster_Melee.cs

* Update Session.cs
2025-08-11 22:38:12 -04:00

1217 lines
51 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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
};
/// <summary>
/// Handles combat with a Player as the attacker
/// generalized methods for melee / missile
/// </summary>
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); }
}
/// <summary>
/// Returns the current attack skill for the player
/// </summary>
public override Skill GetCurrentAttackSkill()
{
if (CombatMode == CombatMode.Magic)
return GetCurrentMagicSkill();
else
return GetCurrentWeaponSkill();
}
/// <summary>
/// Returns the current weapon skill for the player
/// </summary>
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;
}
/// <summary>
/// Returns the highest melee skill for the player
/// (light / heavy / finesse)
/// </summary>
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;
}
/// <summary>
/// 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
/// </summary>
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));
}
/// <summary>
/// Called when a player hits a target
/// </summary>
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;
}
/// <summary>
/// Returns a modifier to the player's defense skill, based on current motion state
/// </summary>
/// <returns></returns>
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;
}
}
/// <summary>
/// Called when player successfully avoids an attack
/// </summary>
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;
}
/// <summary>
/// Simplified player take damage function, only called for DoTs currently
/// </summary>
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);
}
/// <summary>
/// Applies damages to a player from a physical damage source
/// </summary>
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;
}
/// <summary>
/// Returns the total burden of items held in both hands
/// (main hand and offhand)
/// </summary>
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;
}
/// <summary>
/// Calculates the amount of stamina required to perform this attack
/// </summary>
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);
}
/// <summary>
/// Returns the damage rating modifier for an applicable Recklessness attack
/// </summary>
/// <param name="powerAccuracyBar">The 0.0 - 1.0 power/accurary bar</param>
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;
}
/// <summary>
/// Returns TRUE if this player is PK and died to another player
/// </summary>
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;
}
/// <summary>
/// Returns TRUE if this player is PKLite and died to another player
/// </summary>
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;
/// <summary>
/// This method processes the Game Action (F7B1) Change Combat Mode (0x0053)
/// </summary>
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 Asherons 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";
}
/// <summary>
/// If a player has been involved in a PK battle this recently,
/// logging off leaves their character in a frozen state for 20 seconds
/// </summary>
public static TimeSpan PKLogoffTimer = TimeSpan.FromMinutes(2);
public void UpdatePKTimer()
{
//log.Info($"Updating PK timer for {Name}");
LastPkAttackTimestamp = Time.GetUnixTime();
}
/// <summary>
/// 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.
/// </summary>
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<House>();
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<House> 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}");
}
/// <summary>
/// Returns the damage type for the currently equipped weapon / ammo
/// </summary>
/// <param name="multiple">If true, returns all of the damage types for the weapon</param>
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);
/// <summary>
/// Determines if player can damage a target via PlayerKillerStatus
/// </summary>
/// <returns>null if no errors, else pk error list</returns>
public override List<WeenieErrorWithString> 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>() { WeenieErrorWithString.YouFailToAffect_YouAreNotPK, WeenieErrorWithString._FailsToAffectYou_TheyAreNotPK };
if (targetPlayer.PlayerKillerStatus == PlayerKillerStatus.NPK)
return new List<WeenieErrorWithString>() { WeenieErrorWithString.YouFailToAffect_TheyAreNotPK, WeenieErrorWithString._FailsToAffectYou_YouAreNotPK };
// Ensure not attacking across housing boundary
if (!CheckHouseRestrictions(targetPlayer))
return new List<WeenieErrorWithString>() { 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>() { 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>() { WeenieErrorWithString.YouFailToAffect_NotSamePKType, WeenieErrorWithString._FailsToAffectYou_NotSamePKType };
}
}
return null;
}
}
}