using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using ACE.Common; using ACE.Database; using ACE.DatLoader; using ACE.DatLoader.FileTypes; using ACE.Entity.Enum; using ACE.Entity.Enum.Properties; using ACE.Entity.Models; using ACE.Server.Entity; using ACE.Server.Managers; using ACE.Server.Network.GameEvent.Events; using ACE.Server.Network.GameMessages.Messages; namespace ACE.Server.WorldObjects { /// /// Monster combat general functions /// partial class Creature { /// /// The current attack target for the monster /// public WorldObject AttackTarget; /// /// A monster chooses 1 attack height /// public AttackHeight? AttackHeight; /// /// The next type of attack (melee/range/magic) /// public CombatType? CurrentAttack; /// /// The maximum distance for the next attack /// public float MaxRange; /// /// The time when monster started its last attack /// public double PrevAttackTime { get; set; } /// /// The time when monster can perform its next attack /// public double NextAttackTime { get; set; } /// /// The time when monster can perform its next magic attack /// public double NextMagicAttackTime { get { // defaults to most common value found in py16 db var magicDelay = AiUseMagicDelay ?? 3.0f; return PrevAttackTime + magicDelay; } } /// /// Returns true if monster is dead /// public bool IsDead => Health.Current <= 0; /// /// A list of possible attack heights for this monster, /// as determined by the combat maneuvers table /// private List _attackHeights; public List AttackHeights { get { if (CombatTable == null) return null; if (_attackHeights == null) _attackHeights = CombatTable.CMT.Select(m => m.AttackHeight).Distinct().ToList(); return _attackHeights; } } /// /// Selects a random attack height for the next attack /// public AttackHeight ChooseAttackHeight() { var rng = ThreadSafeRandom.Next(0, AttackHeights.Count - 1); return AttackHeights[rng]; } public CombatType GetNextAttackType() { if (CombatTable == null) GetCombatTable(); // if caster, roll for spellcasting chance if (HasKnownSpells && TryRollSpell()) return CombatType.Magic; if (IsRanged) return CombatType.Missile; else return CombatType.Melee; } /// /// Reads the combat maneuvers table from the DAT file /// public void GetCombatTable() { if (CombatTableDID != null) CombatTable = DatManager.PortalDat.ReadFromDat(CombatTableDID.Value); } /// /// Switch to attack stance /// public void DoAttackStance() { var combatMode = IsRanged ? CombatMode.Missile : CombatMode.Melee; var stanceTime = SetCombatMode(combatMode); var nextTime = Timers.RunningTime + stanceTime; if (NextMoveTime > Timers.RunningTime) NextMoveTime += stanceTime; else NextMoveTime = nextTime; if (NextAttackTime > Timers.RunningTime) NextAttackTime += stanceTime; else NextAttackTime = nextTime; if (IsRanged) { PrevAttackTime = NextAttackTime + MissileDelay - (AiUseMagicDelay ?? 3.0f); NextAttackTime += MissileDelay; } if (NeverAttack) { PrevAttackTime = NextAttackTime = double.MaxValue - (AiUseMagicDelay ?? 3.0f); } if (DebugMove) Console.WriteLine($"[{Timers.RunningTime}] - {Name} ({Guid}) - DoAttackStance - stanceTime: {stanceTime}, isAnimating: {IsAnimating}"); PhysicsObj.StartTimer(); } public float GetMaxRange() { // FIXME var it = 0; bool? isVisible = null; while (CurrentAttack == CombatType.Magic) { // select a magic spell //CurrentSpell = GetRandomSpell(); if (CurrentSpell.IsProjectile) { if (isVisible == null) isVisible = IsDirectVisible(AttackTarget); // ensure direct los if (!isVisible.Value) { // reroll attack type CurrentAttack = GetNextAttackType(); it++; // max iterations to melee? if (it >= 10) { //log.Warn($"{Name} ({Guid}) reached max iterations"); CurrentAttack = CombatType.Melee; var powerupTime = (float)(PowerupTime ?? 1.0f); var failDelay = ThreadSafeRandom.Next(0.0f, powerupTime); NextMoveTime = Timers.RunningTime + failDelay; } continue; } } return GetSpellMaxRange(); } if (CurrentAttack == CombatType.Missile) { /*var weapon = GetEquippedWeapon(); if (weapon == null) return MaxMissileRange; var maxRange = weapon.GetProperty(PropertyInt.WeaponRange) ?? MaxMissileRange; return Math.Min(maxRange, MaxMissileRange); // in-game cap @ 80 yds.*/ return GetMaxMissileRange(); } else return MaxMeleeRange; // distance_to_target? } public bool MoveReady() { if (Timers.RunningTime < NextMoveTime) return false; PhysicsObj.update_object(); UpdatePosition_SyncLocation(); return !PhysicsObj.IsAnimating; } /// /// Returns TRUE if creature can perform its next attack /// /// public bool AttackReady() { var nextAttackTime = CurrentAttack == CombatType.Magic ? NextMagicAttackTime : NextAttackTime; if (Timers.RunningTime < nextAttackTime || !IsAttackRange()) return false; PhysicsObj.update_object(); UpdatePosition_SyncLocation(); return !PhysicsObj.IsAnimating; } /// /// Performs the current attack on the target /// public void Attack() { if (DebugMove) Console.WriteLine($"[{Timers.RunningTime}] - {Name} ({Guid}) - Attack"); switch (CurrentAttack) { case CombatType.Melee: MeleeAttack(); break; case CombatType.Missile: RangeAttack(); break; case CombatType.Magic: MagicAttack(); break; } EmoteManager.OnAttack(AttackTarget); ResetAttack(); } /// /// Called after attack has completed /// public void ResetAttack() { // wait for missile to strike //if (CurrentAttack == CombatType.Missile) //return; IsTurning = false; IsMoving = false; CurrentAttack = null; MaxRange = 0.0f; } public DamageType GetDamageType(PropertiesBodyPart attackPart, CombatType? combatType = null) { var weapon = GetEquippedWeapon(); if (weapon != null) return GetDamageType(false, combatType); else { var damageType = attackPart.DType; if (damageType.IsMultiDamage()) damageType = damageType.SelectDamageType(); return damageType; } } /// /// Simplified monster take damage over time function, only called for DoTs currently /// public virtual void TakeDamageOverTime(float amount, DamageType damageType) { if (IsDead) return; TakeDamage(null, damageType, amount); // splatter effects var hitSound = new GameMessageSound(Guid, Sound.HitFlesh1, 0.5f); //var splatter = (PlayScript)Enum.Parse(typeof(PlayScript), "Splatter" + playerSource.GetSplatterHeight() + playerSource.GetSplatterDir(this)); var splatter = new GameMessageScript(Guid, damageType == DamageType.Nether ? PlayScript.HealthDownVoid : PlayScript.DirtyFightingDamageOverTime); EnqueueBroadcast(hitSound, splatter); if (Health.Current <= 0) return; if (amount >= Health.MaxValue * 0.25f) { var painSound = (Sound)Enum.Parse(typeof(Sound), "Wound" + ThreadSafeRandom.Next(1, 3), true); EnqueueBroadcast(new GameMessageSound(Guid, painSound, 1.0f)); } } /// /// Notifies the damage over time (DoT) source player of the tick damage amount /// public void TakeDamageOverTime_NotifySource(Player source, DamageType damageType, float amount, bool aetheria = false) { if (!PropertyManager.GetBool("show_dot_messages").Item) return; var iAmount = (uint)Math.Round(amount); var notifyType = damageType == DamageType.Undef ? DamageType.Health : damageType; string verb = null, plural = null; var percent = amount / Health.MaxValue; Strings.GetAttackVerb(notifyType, percent, ref verb, ref plural); string msg = null; var type = ChatMessageType.CombatSelf; if (damageType == DamageType.Nether) { msg = $"You {verb} {Name} for {iAmount} points of periodic nether damage!"; type = ChatMessageType.Magic; } else if (aetheria) { msg = $"With Surge of Affliction you {verb} {iAmount} points of health from {Name}!"; type = ChatMessageType.Magic; } else { /*var skill = source.GetCreatureSkill(Skill.DirtyFighting); var attack = skill.AdvancementClass == SkillAdvancementClass.Specialized ? "Bleeding Assault" : "Bleeding Blow"; msg = $"With {attack} you {verb} {iAmount} points of health from {Name}!";*/ msg = $"You bleed {Name} for {iAmount} points of periodic health damage!"; type = ChatMessageType.CombatSelf; } source.SendMessage(msg, type); } /// /// Applies some amount of damage to this monster from source /// /// The attacker / source of damage /// The amount of damage rounded public virtual uint TakeDamage(WorldObject source, DamageType damageType, float amount, bool crit = false) { var tryDamage = (int)Math.Round(amount); var damage = -UpdateVitalDelta(Health, -tryDamage); // TODO: update monster stamina? // source should only be null for combined DoT ticks from multiple sources if (source != null) { if (damage >= 0) DamageHistory.Add(source, damageType, (uint)damage); else DamageHistory.OnHeal((uint)-damage); } if (Health.Current <= 0) { OnDeath(DamageHistory.LastDamager, damageType, crit); Die(); } return (uint)Math.Max(0, damage); } public void EmitSplatter(Creature target, float damage) { if (target.IsDead) return; target.EnqueueBroadcast(new GameMessageSound(target.Guid, Sound.HitFlesh1, 0.5f)); if (damage >= target.Health.MaxValue * 0.25f) { var painSound = (Sound)Enum.Parse(typeof(Sound), "Wound" + ThreadSafeRandom.Next(1, 3), true); target.EnqueueBroadcast(new GameMessageSound(target.Guid, painSound, 1.0f)); } var splatter = (PlayScript)Enum.Parse(typeof(PlayScript), "Splatter" + GetSplatterHeight() + GetSplatterDir(target)); target.EnqueueBroadcast(new GameMessageScript(target.Guid, splatter)); } public CombatStyle AiAllowedCombatStyle { get => (CombatStyle)(GetProperty(PropertyInt.AiAllowedCombatStyle) ?? 0); set { if (value == 0) RemoveProperty(PropertyInt.AiAllowedCombatStyle); else SetProperty(PropertyInt.AiAllowedCombatStyle, (int)value); } } private static readonly ConcurrentDictionary BPTableCache = new ConcurrentDictionary(); public static BodyPartTable GetBodyParts(uint wcid) { if (!BPTableCache.TryGetValue(wcid, out var bpTable)) { var weenie = DatabaseManager.World.GetCachedWeenie(wcid); /* bpTable = new BodyPartTable(weenie); BPTableCache[wcid] = bpTable; */ if (weenie == null) { // should never happen? log.Error($"Monster_Combat.GetBodyParts({wcid}) - unknown wcid"); return null; } try { bpTable = new BodyPartTable(weenie); } catch (Exception e) { log.Error(e); log.Error($"Monster_Combat.GetBodyParts({wcid}) - bad data for wcid {wcid}"); return null; } BPTableCache[wcid] = bpTable; } return bpTable; } /// /// Flag indicates if a monster will aggro, but not attack /// public bool NeverAttack { get => GetProperty(PropertyBool.NeverAttack) ?? false; set { if (!value) RemoveProperty(PropertyBool.NeverAttack); else SetProperty(PropertyBool.NeverAttack, value); } } } }