using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using ACE.Common;
using ACE.DatLoader;
using ACE.DatLoader.Entity.AnimationHooks;
using ACE.Entity;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Entity.Models;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Managers;
using ACE.Server.Physics.Animation;
namespace ACE.Server.WorldObjects
{
partial class Creature
{
///
/// The maximum delay in seconds between the end of a monster's previous attack,
/// and their next attack
///
public double? PowerupTime
{
get => GetProperty(PropertyFloat.PowerupTime);
set { if (!value.HasValue) RemoveProperty(PropertyFloat.PowerupTime); else SetProperty(PropertyFloat.PowerupTime, value.Value); }
}
///
/// Returns TRUE if creature can perform a melee attack
///
public bool MeleeReady()
{
return IsMeleeRange() && Timers.RunningTime >= NextAttackTime;
}
///
/// Performs a melee attack for the monster
///
/// The length in seconds for the attack animation
public float MeleeAttack()
{
var target = AttackTarget as Creature;
var targetPlayer = AttackTarget as Player;
var targetPet = AttackTarget as CombatPet;
var combatPet = this as CombatPet;
if (target == null || !target.IsAlive)
{
FindNextTarget();
return 0.0f;
}
if (CurrentMotionState.Stance == MotionStance.NonCombat)
DoAttackStance();
var weapon = GetEquippedWeapon();
// select combat maneuver
var motionCommand = GetCombatManeuver();
if (motionCommand == null)
return 0.0f;
DoSwingMotion(AttackTarget, motionCommand.Value, out float animLength, out var attackFrames);
if (!AiImmobile)
PhysicsObj.stick_to_object(AttackTarget.PhysicsObj.ID);
var numStrikes = attackFrames.Count;
var actionChain = new ActionChain();
// handle self-procs
TryProcEquippedItems(this, this, true, weapon);
var prevTime = 0.0f;
bool targetProc = false;
for (var i = 0; i < numStrikes; i++)
{
actionChain.AddDelaySeconds(attackFrames[i].time * animLength - prevTime);
prevTime = attackFrames[i].time * animLength;
actionChain.AddAction(this, () =>
{
if (AttackTarget == null || IsDead || target.IsDead) return;
if (WeenieType == WeenieType.GamePiece)
{
target.TakeDamage(this, DamageType.Slash, target.Health.Current);
(this as GamePiece).OnDealtDamage();
return;
}
var damageEvent = DamageEvent.CalculateDamage(this, target, weapon, motionCommand, attackFrames[0].attackHook);
//var damage = CalculateDamage(ref damageType, maneuver, bodyPart, ref critical, ref shieldMod);
if (damageEvent.HasDamage)
{
if (targetPlayer != null)
{
// this is a player taking damage
targetPlayer.TakeDamage(this, damageEvent);
if (damageEvent.ShieldMod != 1.0f)
{
var shieldSkill = targetPlayer.GetCreatureSkill(Skill.Shield);
Proficiency.OnSuccessUse(targetPlayer, shieldSkill, shieldSkill.Current); // ?
}
// handle Dirty Fighting
if (GetCreatureSkill(Skill.DirtyFighting).AdvancementClass >= SkillAdvancementClass.Trained)
FightDirty(targetPlayer, damageEvent.Weapon);
}
else if (combatPet != null || targetPet != null || Faction1Bits != null || target.Faction1Bits != null || PotentialFoe(target))
{
// combat pet inflicting or receiving damage
//Console.WriteLine($"{target.Name} taking {Math.Round(damage)} {damageType} damage from {Name}");
target.TakeDamage(this, damageEvent.DamageType, damageEvent.Damage);
EmitSplatter(target, damageEvent.Damage);
// handle Dirty Fighting
if (GetCreatureSkill(Skill.DirtyFighting).AdvancementClass >= SkillAdvancementClass.Trained)
FightDirty(target, damageEvent.Weapon);
}
// handle target procs
if (!targetProc)
{
TryProcEquippedItems(this, target, false, weapon);
targetProc = true;
}
}
else
target.OnEvade(this, CombatType.Melee);
if (combatPet != null)
combatPet.PetOnAttackMonster(target);
else if (targetPlayer == null)
MonsterOnAttackMonster(target);
});
}
actionChain.EnqueueChain();
PrevAttackTime = Timers.RunningTime;
NextMoveTime = PrevAttackTime + animLength + 0.5f;
var meleeDelay = ThreadSafeRandom.Next(0.0f, (float)(PowerupTime ?? 1.0f));
NextAttackTime = PrevAttackTime + animLength + meleeDelay;
return animLength;
}
///
/// Selects a random combat maneuver for a monster's next attack
///
public MotionCommand? GetCombatManeuver()
{
// similar to Player.GetSwingAnimation(), more logging
if (CombatTable == null)
{
log.Error($"{Name} ({Guid}).GetCombatManeuver() - WCID: {WeenieClassId} | CombatTable is null");
return null;
}
//ShowCombatTable();
var motionTable = DatManager.PortalDat.ReadFromDat(MotionTableId);
if (motionTable == null)
{
log.Error($"{Name} ({Guid}).GetCombatManeuver() - WCID: {WeenieClassId} | motionTable is null");
return null;
}
if (!CombatTable.Stances.TryGetValue(CurrentMotionState.Stance, out var stanceManeuvers))
{
log.Error($"{Name} ({Guid}).GetCombatManeuver() - WCID: {WeenieClassId} | couldn't find stance {CurrentMotionState.Stance} in CMT {CombatTableDID:X8}");
return null;
}
var stanceKey = (uint)CurrentMotionState.Stance << 16 | ((uint)MotionCommand.Ready & 0xFFFFFF);
motionTable.Links.TryGetValue(stanceKey, out var motions);
if (motions == null)
{
log.Error($"{Name} ({Guid}).GetCombatManeuver() - WCID: {WeenieClassId} | couldn't find stance {CurrentMotionState.Stance} in MotionTable {MotionTableId:X8}");
return null;
}
// choose a random attack height?
// apparently this might have been based on monster Z vs. player Z?
// 28659 - Uber Penguin (CMT 30000040) doesn't have High attack height
// do a more thorough investigation for this...
var startHeight = stanceManeuvers.Table.Count == 3 ? 1 : 2;
AttackHeight = (AttackHeight)ThreadSafeRandom.Next(startHeight, 3);
if (!stanceManeuvers.Table.TryGetValue(AttackHeight.Value, out var attackTypes))
{
log.Error($"{Name} ({Guid}).GetCombatManeuver() - WCID: {WeenieClassId} | couldn't find attack height {AttackHeight} for stance {CurrentMotionState.Stance} in CMT {CombatTableDID:X8}");
return null;
}
if (IsDualWieldAttack)
DualWieldAlternate = !DualWieldAlternate;
var offhand = IsDualWieldAttack && !DualWieldAlternate;
var weapon = GetEquippedMeleeWeapon();
// monsters supposedly always used 0.5 PowerLevel according to anon docs,
// which translates into a 1.0 PowerMod
if (weapon != null)
{
AttackType = weapon.GetAttackType(CurrentMotionState.Stance, 0.5f, offhand);
}
else
{
if (AttackHeight != ACE.Entity.Enum.AttackHeight.Low)
AttackType = AttackType.Punch;
else
AttackType = AttackType.Kick;
}
if (!attackTypes.Table.TryGetValue(AttackType, out var maneuvers) || maneuvers.Count == 0)
{
if (AttackType == AttackType.Punch && AttackHeight == ACE.Entity.Enum.AttackHeight.Low || AttackType == AttackType.Kick)
{
// 27864 - Mosswart Muckstalker w/ a katar, low punch not found in CMT, but contains kick
// might need additional research
AttackType = AttackType == AttackType.Punch ? AttackType.Kick : AttackType.Punch;
if (!attackTypes.Table.TryGetValue(AttackType, out maneuvers) || maneuvers.Count == 0)
{
log.Error($"{Name} ({Guid}).GetCombatManeuver() - WCID: {WeenieClassId} | couldn't find attack type Kick or Punch for attack height {AttackHeight} and stance {CurrentMotionState.Stance} in CMT {CombatTableDID:X8}");
return null;
}
}
else if (AttackType.IsMultiStrike())
{
var reduced = AttackType.ReduceMultiStrike();
if (!attackTypes.Table.TryGetValue(reduced, out maneuvers) || maneuvers.Count == 0)
{
log.Error($"{Name} ({Guid}).GetCombatManeuver() - WCID: {WeenieClassId} | couldn't find attack type {reduced} for attack height {AttackHeight} and stance {CurrentMotionState.Stance} in CMT {CombatTableDID:X8}");
return null;
}
//else
//log.Info($"{Name} ({Guid}).GetCombatManeuver() - WCID: {WeenieClassId} | successfully reduced attack type {AttackType} to {reduced} for attack height {AttackHeight} and stance {CurrentMotionState.Stance} in CMT {CombatTableDID:X8}");
}
else
{
log.Error($"{Name} ({Guid}).GetCombatManeuver() - WCID: {WeenieClassId} | couldn't find attack type {AttackType} for attack height {AttackHeight} and stance {CurrentMotionState.Stance} in CMT {CombatTableDID:X8}");
return null;
}
}
var motionCommand = maneuvers[0];
if (maneuvers.Count > 1)
{
// only used for special attacks?
// note that with rolling for AttackHeight first,
// for a CMT with high, med, med-special, and low
// the chance of rolling the special attack is reduced from 1/4 to 1/6 -- investigate
var rng = ThreadSafeRandom.Next(0, maneuvers.Count - 1);
motionCommand = maneuvers[rng];
}
// ensure this motionCommand exists in monster's motion table
if (!motions.ContainsKey((uint)motionCommand))
{
// for some reason, the combat maneuvers table can return stance motions that don't exist in the motion table
// ie. skeletons (combat maneuvers table 0x30000000, motion table 0x09000025)
// for sword combat, they have double and triple strikes (dagger / two-handed only?)
if (motionCommand.IsMultiStrike())
{
var singleStrike = motionCommand.ReduceMultiStrike();
if (motions.ContainsKey((uint)singleStrike))
{
//log.Info($"{Name} ({Guid}).GetCombatManeuver() - WCID: {WeenieClassId} | successfully reduced {motionCommand} to {singleStrike}");
return singleStrike;
}
}
else if (motionCommand.IsSubsequent())
{
var firstCommand = motionCommand.ReduceSubsequent();
if (motions.ContainsKey((uint)firstCommand))
{
//log.Info($"{Name} ({Guid}).GetCombatManeuver() - WCID: {WeenieClassId} | successfully reduced {motionCommand} to {firstCommand}");
return firstCommand;
}
}
log.Error($"{Name} ({Guid}).GetCombatManeuver() - WCID: {WeenieClassId} | couldn't find {motionCommand} in MotionTable {MotionTableId:X8}");
return null;
}
//Console.WriteLine(motionCommand);
return motionCommand;
}
///
/// Shows debug info for this monster's combat maneuvers table
///
public void ShowCombatTable()
{
Console.WriteLine($"CombatManeuverTable ID: {CombatTable.Id:X8}");
CombatTable.ShowCombatTable();
/*for (var i = 0; i < CombatTable.CMT.Count; i++)
{
var maneuver = CombatTable.CMT[i];
Console.WriteLine($"{i} - {maneuver.Style} - {maneuver.Motion} - {maneuver.AttackHeight}");
}*/
}
private static readonly List<(float time, AttackHook attackHook)> defaultAttackFrames = new List<(float time, AttackHook attackHook)>() { ( 1.0f / 3.0f, null ) };
private static readonly ConcurrentDictionary missingAttackFrames = new ConcurrentDictionary();
private bool moveBit;
///
/// Perform the melee attack swing animation
///
public void DoSwingMotion(WorldObject target, MotionCommand motionCommand, out float animLength, out List<(float time, AttackHook attackHook)> attackFrames)
{
if (!moveBit)
{
SendUpdatePosition(true);
moveBit = true;
}
//Console.WriteLine($"{maneuver.Style} - {maneuver.Motion} - {maneuver.AttackHeight}");
var baseSpeed = GetAnimSpeed();
var animSpeedMod = IsDualWieldAttack ? 1.2f : 1.0f; // dual wield swing animation 20% faster
var animSpeed = baseSpeed * animSpeedMod;
animLength = MotionTable.GetAnimationLength(MotionTableId, CurrentMotionState.Stance, motionCommand, animSpeed);
attackFrames = MotionTable.GetAttackFrames(MotionTableId, CurrentMotionState.Stance, motionCommand);
if (attackFrames.Count == 0)
{
var attackFrameParams = new AttackFrameParams(MotionTableId, CurrentMotionState.Stance, motionCommand);
if (!missingAttackFrames.ContainsKey(attackFrameParams))
{
// only show warning message once for each combo
log.Warn($"{Name} ({Guid}) - no attack frames for MotionTable {MotionTableId:X8}, {CurrentMotionState.Stance}, {motionCommand}, using defaults");
missingAttackFrames.TryAdd(attackFrameParams, true);
}
attackFrames = defaultAttackFrames;
}
var motion = new Motion(this, motionCommand, animSpeed);
motion.MotionState.TurnSpeed = 2.25f;
if (!AiImmobile)
motion.MotionFlags |= MotionFlags.StickToObject;
motion.TargetGuid = target.Guid;
CurrentMotionState = motion;
EnqueueBroadcastMotion(motion);
}
///
/// Returns base damage range for next monster attack
///
public BaseDamageMod GetBaseDamage(PropertiesBodyPart attackPart)
{
if (CurrentAttack == CombatType.Missile && GetMissileAmmo() != null)
return GetMissileDamage();
// use weapon damage for every attack?
var weapon = GetEquippedMeleeWeapon();
if (weapon != null)
{
var weaponDamage = weapon.GetDamageMod(this);
//Console.WriteLine($"{Name} using weapon damage: {weaponDamage}");
return weaponDamage;
}
var maxDamage = attackPart.DVal;
var variance = attackPart.DVar;
var baseDamage = new BaseDamage(maxDamage, variance);
return new BaseDamageMod(baseDamage);
}
///
/// Returns the creature armor for a body part
///
public List GetArmorLayers(Player target, BodyPart bodyPart)
{
//Console.WriteLine("BodyPart: " + bodyPart);
//Console.WriteLine("===");
var coverageMask = BodyParts.GetCoverageMask(bodyPart);
var equipped = target.EquippedObjects.Values.Where(e => e is Clothing && (e.ClothingPriority & coverageMask) != 0).ToList();
return equipped;
}
///
/// Returns the percent of damage absorbed by layered armor + clothing
///
/// The list of armor/clothing covering the targeted body part
public float GetArmorMod(Creature defender, DamageType damageType, List armors, WorldObject weapon, float armorRendingMod = 1.0f)
{
var ignoreMagicArmor = (weapon?.IgnoreMagicArmor ?? false) || IgnoreMagicArmor;
var ignoreMagicResist = (weapon?.IgnoreMagicResist ?? false) || IgnoreMagicResist;
var effectiveAL = 0.0f;
foreach (var armor in armors)
effectiveAL += GetArmorMod(armor, damageType, ignoreMagicArmor);
// life spells
// additive: armor/imperil
var bodyArmorMod = defender.EnchantmentManager.GetBodyArmorMod();
if (ignoreMagicResist)
bodyArmorMod = IgnoreMagicResistScaled(bodyArmorMod);
// handle armor rending mod here?
//if (bodyArmorMod > 0)
//bodyArmorMod *= armorRendingMod;
//Console.WriteLine("==");
//Console.WriteLine("Armor Self: " + bodyArmorMod);
effectiveAL += bodyArmorMod;
// Armor Rending reduces physical armor too?
if (effectiveAL > 0)
effectiveAL *= armorRendingMod;
var armorMod = SkillFormula.CalcArmorMod(effectiveAL);
//Console.WriteLine("Total AL: " + effectiveAL);
//Console.WriteLine("Armor mod: " + armorMod);
return armorMod;
}
public float IgnoreMagicArmorScaled(float enchantments)
{
if (!(this is Player))
return 0.0f;
var scalar = PropertyManager.GetDouble("ignore_magic_armor_pvp_scalar").Item;
if (scalar != 1.0)
return (float)(enchantments * (1.0 - scalar));
else
return 0.0f;
}
public int IgnoreMagicResistScaled(int enchantments)
{
if (!(this is Player))
return 0;
var scalar = PropertyManager.GetDouble("ignore_magic_resist_pvp_scalar").Item;
if (scalar != 1.0)
return (int)Math.Round(enchantments * (1.0 - scalar));
else
return 0;
}
///
/// Returns the effective AL for 1 piece of armor/clothing
///
/// A piece of armor or clothing
public float GetArmorMod(WorldObject armor, DamageType damageType, bool ignoreMagicArmor)
{
// get base armor/resistance level
var baseArmor = armor.GetProperty(PropertyInt.ArmorLevel) ?? 0;
//var armorType = armor.GetProperty(PropertyInt.ArmorType) ?? 0;
var resistance = GetResistance(armor, damageType);
/*Console.WriteLine(armor.Name);
Console.WriteLine("--");
Console.WriteLine("Base AL: " + baseArmor);
Console.WriteLine("Base RL: " + resistance);*/
// armor level additives
var armorMod = armor.EnchantmentManager.GetArmorMod();
if (ignoreMagicArmor)
armorMod = (int)Math.Round(IgnoreMagicArmorScaled(armorMod));
// Console.WriteLine("Impen: " + armorMod);
var effectiveAL = baseArmor + armorMod;
// resistance additives
var armorBane = armor.EnchantmentManager.GetArmorModVsType(damageType);
if (ignoreMagicArmor)
armorBane = IgnoreMagicArmorScaled(armorBane);
// Console.WriteLine("Bane: " + armorBane);
var effectiveRL = (float)(resistance + armorBane);
// resistance clamp
effectiveRL = Math.Clamp(effectiveRL, -2.0f, 2.0f);
// TODO: could brittlemail / lures send a piece of armor or clothing's AL into the negatives?
//if (effectiveAL < 0 && effectiveRL != 0)
//effectiveRL = 1.0f / effectiveRL;
/*Console.WriteLine("Effective AL: " + effectiveAL);
Console.WriteLine("Effective RL: " + effectiveRL);
Console.WriteLine();*/
return effectiveAL * effectiveRL;
}
///
/// Returns the natural resistance to DamageType for a piece of armor
///
public double GetResistance(WorldObject armor, DamageType damageType)
{
switch (damageType)
{
case DamageType.Slash:
return armor.GetProperty(PropertyFloat.ArmorModVsSlash) ?? 0.0;
case DamageType.Pierce:
return armor.GetProperty(PropertyFloat.ArmorModVsPierce) ?? 0.0;
case DamageType.Bludgeon:
return armor.GetProperty(PropertyFloat.ArmorModVsBludgeon) ?? 0.0;
case DamageType.Fire:
return armor.GetProperty(PropertyFloat.ArmorModVsFire) ?? 0.0;
case DamageType.Cold:
return armor.GetProperty(PropertyFloat.ArmorModVsCold) ?? 0.0;
case DamageType.Acid:
return armor.GetProperty(PropertyFloat.ArmorModVsAcid) ?? 0.0;
case DamageType.Electric:
return armor.GetProperty(PropertyFloat.ArmorModVsElectric) ?? 0.0;
case DamageType.Nether:
return armor.GetProperty(PropertyFloat.ArmorModVsNether) ?? 0.0;
default:
return 0.0;
}
}
///
/// Displays all of the natural resistances for a piece of armor
///
public void ShowResistance(WorldObject armor)
{
Console.WriteLine("Resistance:");
Console.WriteLine("Slashing: " + armor.GetProperty(PropertyFloat.ArmorModVsSlash));
Console.WriteLine("Piercing: " + armor.GetProperty(PropertyFloat.ArmorModVsPierce));
Console.WriteLine("Bludgeoning: " + armor.GetProperty(PropertyFloat.ArmorModVsBludgeon));
Console.WriteLine("Fire: " + armor.GetProperty(PropertyFloat.ArmorModVsFire));
Console.WriteLine("Ice: " + armor.GetProperty(PropertyFloat.ArmorModVsCold));
Console.WriteLine("Acid: " + armor.GetProperty(PropertyFloat.ArmorModVsAcid));
Console.WriteLine("Lightning: " + armor.GetProperty(PropertyFloat.ArmorModVsElectric));
Console.WriteLine("Nether: " + armor.GetProperty(PropertyFloat.ArmorModVsNether));
}
///
/// Returns the power range for the current melee attack
///
public virtual PowerAccuracy GetPowerRange()
{
return PowerAccuracy.Low; // always low for monsters?
}
///
/// Returns the monster body part performing the next attack
///
public KeyValuePair GetAttackPart(MotionCommand motionCommand, AttackHook attackHook)
{
List> parts = null;
// todo: speed up key lookup?
if (attackHook != null)
{
parts = Biota.PropertiesBodyPart.Where(b => (uint)b.Key == attackHook.AttackCone.PartIndex).ToList();
}
else if (motionCommand >= MotionCommand.SpecialAttack1 && motionCommand <= MotionCommand.SpecialAttack3)
{
//parts = Biota.BiotaPropertiesBodyPart.Where(b => b.DVal != 0 && b.BH == 0).ToList();
parts = Biota.PropertiesBodyPart.Where(b => b.Key == CombatBodyPart.Breath).ToList(); // always use Breath?
}
// added parts.Count check for monsters wielding weapons -- should we be getting a body part here?
if (parts == null || parts.Count == 0)
//parts = Biota.BiotaPropertiesBodyPart.Where(b => b.DVal != 0 && b.BH != 0).ToList();
parts = Biota.PropertiesBodyPart.Where(b => b.Value.DVal != 0 && b.Key != CombatBodyPart.Breath).ToList();
if (parts.Count == 0)
{
log.Warn($"{Name} ({Guid}.GetAttackPart({motionCommand}) failed");
log.Warn($"CombatTable: {CombatTableDID:X8}, MotionTable: {MotionTableId:X8}, CurrentStance: {CurrentMotionState.Stance}, AttackHeight: {AttackHeight}, AttackType: {AttackType}");
return new KeyValuePair();
}
var part = parts[ThreadSafeRandom.Next(0, parts.Count - 1)];
return part;
}
}
}