// Copyright (c) CypherCore All rights reserved. // Licensed under the GNU GENERAL PUBLIC LICENSE. See LICENSE file in the project root for full license information. using Framework.Constants; using Game.AI; using Game.Entities; using System.Collections.Generic; using System.Linq; namespace Game.Combat { public class CombatManager { Unit _owner; Dictionary _pveRefs = new(); Dictionary _pvpRefs = new(); public CombatManager(Unit owner) { _owner = owner; } public static bool CanBeginCombat(Unit a, Unit b) { // Checks combat validity before initial reference creation. // For the combat to be valid... // ...the two units need to be different if (a == b) return false; // ...the two units need to be in the world if (!a.IsInWorld || !b.IsInWorld) return false; // ...the two units need to both be alive if (!a.IsAlive() || !b.IsAlive()) return false; // ...the two units need to be on the same map if (a.GetMap() != b.GetMap()) return false; // ...the two units need to be in the same phase if (!WorldObject.InSamePhase(a, b)) return false; if (a.HasUnitState(UnitState.Evade) || b.HasUnitState(UnitState.Evade)) return false; if (a.HasUnitState(UnitState.InFlight) || b.HasUnitState(UnitState.InFlight)) return false; // ... both units must be allowed to enter combat if (a.IsCombatDisallowed() || b.IsCombatDisallowed()) return false; if (a.IsFriendlyTo(b) || b.IsFriendlyTo(a)) return false; Player playerA = a.GetCharmerOrOwnerPlayerOrPlayerItself(); Player playerB = b.GetCharmerOrOwnerPlayerOrPlayerItself(); // ...neither of the two units must be (owned by) a player with .gm on if ((playerA != null && playerA.IsGameMaster()) || (playerB != null && playerB.IsGameMaster())) return false; return true; } public void Update(uint tdiff) { foreach(var pair in _pvpRefs.ToList()) { PvPCombatReference refe = pair.Value; if (refe.first == _owner && !refe.Update(tdiff)) // only update if we're the first unit involved (otherwise double decrement) { _pvpRefs.Remove(pair.Key); refe.EndCombat(); // this will remove it from the other side } } } public bool HasPvECombat() { foreach (var (_, refe) in _pveRefs) if (!refe.IsSuppressedFor(_owner)) return true; return false; } public bool HasPvECombatWithPlayers() { foreach (var reference in _pveRefs) if (!reference.Value.IsSuppressedFor(_owner) && reference.Value.GetOther(_owner).IsPlayer()) return true; return false; } public bool HasPvPCombat() { foreach (var pair in _pvpRefs) if (!pair.Value.IsSuppressedFor(_owner)) return true; return false; } public Unit GetAnyTarget() { foreach (var pair in _pveRefs) if (!pair.Value.IsSuppressedFor(_owner)) return pair.Value.GetOther(_owner); foreach (var pair in _pvpRefs) if (!pair.Value.IsSuppressedFor(_owner)) return pair.Value.GetOther(_owner); return null; } public bool SetInCombatWith(Unit who, bool addSecondUnitSuppressed = false) { // Are we already in combat? If yes, refresh pvp combat var existingPvpRef = _pvpRefs.LookupByKey(who.GetGUID()); if (existingPvpRef != null) { existingPvpRef.RefreshTimer(); existingPvpRef.Refresh(); return true; } var existingPveRef = _pveRefs.LookupByKey(who.GetGUID()); if (existingPveRef != null) { existingPveRef.Refresh(); return true; } // Otherwise, check validity... if (!CanBeginCombat(_owner, who)) return false; // ...then create new reference CombatReference refe; if (_owner.IsControlledByPlayer() && who.IsControlledByPlayer()) refe = new PvPCombatReference(_owner, who); else refe = new CombatReference(_owner, who); if (addSecondUnitSuppressed) refe.Suppress(who); // ...and insert it into both managers PutReference(who.GetGUID(), refe); who.GetCombatManager().PutReference(_owner.GetGUID(), refe); // now, sequencing is important - first we update the combat state, which will set both units in combat and do non-AI combat start stuff bool needSelfAI = UpdateOwnerCombatState(); bool needOtherAI = who.GetCombatManager().UpdateOwnerCombatState(); // then, we finally notify the AI (if necessary) and let it safely do whatever it feels like if (needSelfAI) NotifyAICombat(_owner, who); if (needOtherAI) NotifyAICombat(who, _owner); return IsInCombatWith(who); } public bool IsInCombatWith(ObjectGuid guid) { return _pveRefs.ContainsKey(guid) || _pvpRefs.ContainsKey(guid); } public bool IsInCombatWith(Unit who) { return IsInCombatWith(who.GetGUID()); } public void InheritCombatStatesFrom(Unit who) { CombatManager mgr = who.GetCombatManager(); foreach (var refe in mgr._pveRefs) { if (!IsInCombatWith(refe.Key)) { Unit target = refe.Value.GetOther(who); if ((_owner.IsImmuneToPC() && target.HasUnitFlag(UnitFlags.PlayerControlled)) || (_owner.IsImmuneToNPC() && !target.HasUnitFlag(UnitFlags.PlayerControlled))) continue; SetInCombatWith(target); } } foreach (var refe in mgr._pvpRefs) { Unit target = refe.Value.GetOther(who); if ((_owner.IsImmuneToPC() && target.HasUnitFlag(UnitFlags.PlayerControlled)) || (_owner.IsImmuneToNPC() && !target.HasUnitFlag(UnitFlags.PlayerControlled))) continue; SetInCombatWith(target); } } public void EndCombatBeyondRange(float range, bool includingPvP) { foreach (var pair in _pveRefs.ToList()) { CombatReference refe = pair.Value; if (!refe.first.IsWithinDistInMap(refe.second, range)) { _pveRefs.Remove(pair.Key); refe.EndCombat(); } } if (!includingPvP) return; foreach (var pair in _pvpRefs.ToList()) { CombatReference refe = pair.Value; if (!refe.first.IsWithinDistInMap(refe.second, range)) { _pvpRefs.Remove(pair.Key); refe.EndCombat(); } } } public void SuppressPvPCombat() { foreach (var pair in _pvpRefs) pair.Value.Suppress(_owner); if (UpdateOwnerCombatState()) { UnitAI ownerAI = _owner.GetAI(); if (ownerAI != null) ownerAI.JustExitedCombat(); } } public void EndAllPvECombat() { // cannot have threat without combat _owner.GetThreatManager().RemoveMeFromThreatLists(); _owner.GetThreatManager().ClearAllThreat(); while (!_pveRefs.Empty()) _pveRefs.First().Value.EndCombat(); } public void RevalidateCombat() { foreach(var (guid, refe) in _pveRefs.ToList()) { if (!CanBeginCombat(_owner, refe.GetOther(_owner))) { _pveRefs.Remove(guid); // erase manually here to avoid iterator invalidation refe.EndCombat(); } } foreach (var (guid, refe) in _pvpRefs.ToList()) { if (!CanBeginCombat(_owner, refe.GetOther(_owner))) { _pvpRefs.Remove(guid); // erase manually here to avoid iterator invalidation refe.EndCombat(); } } } void EndAllPvPCombat() { while (!_pvpRefs.Empty()) _pvpRefs.First().Value.EndCombat(); } public static void NotifyAICombat(Unit me, Unit other) { UnitAI ai = me.GetAI(); if (ai != null) ai.JustEnteredCombat(other); } void PutReference(ObjectGuid guid, CombatReference refe) { if (refe._isPvP) { Cypher.Assert(!_pvpRefs.ContainsKey(guid), "Duplicate combat state detected!"); _pvpRefs[guid] = (PvPCombatReference)refe; } else { Cypher.Assert(!_pveRefs.ContainsKey(guid), "Duplicate combat state detected!"); _pveRefs[guid] = refe; } } public void PurgeReference(ObjectGuid guid, bool pvp) { if (pvp) _pvpRefs.Remove(guid); else _pveRefs.Remove(guid); } public bool UpdateOwnerCombatState() { bool combatState = HasCombat(); if (combatState == _owner.IsInCombat()) return false; if (combatState) { _owner.SetUnitFlag(UnitFlags.InCombat); _owner.AtEnterCombat(); if (!_owner.IsCreature()) _owner.AtEngage(GetAnyTarget()); } else { _owner.RemoveUnitFlag(UnitFlags.InCombat); _owner.AtExitCombat(); if (!_owner.IsCreature()) _owner.AtDisengage(); } Unit master = _owner.GetCharmerOrOwner(); if (master != null) master.UpdatePetCombatState(); return true; } public Unit GetOwner() { return _owner; } public bool HasCombat() { return HasPvECombat() || HasPvPCombat(); } public Dictionary GetPvECombatRefs() { return _pveRefs; } public Dictionary GetPvPCombatRefs() { return _pvpRefs; } public void EndAllCombat() { EndAllPvECombat(); EndAllPvPCombat(); } } public class CombatReference { public Unit first; public Unit second; public bool _isPvP; bool _suppressFirst; bool _suppressSecond; public CombatReference(Unit a, Unit b, bool pvp = false) { first = a; second = b; _isPvP = pvp; } public void EndCombat() { // sequencing matters here - AI might do nasty stuff, so make sure refs are in a consistent state before you hand off! // first, get rid of any threat that still exists... first.GetThreatManager().ClearThreat(second); second.GetThreatManager().ClearThreat(first); // ...then, remove the references from both managers... first.GetCombatManager().PurgeReference(second.GetGUID(), _isPvP); second.GetCombatManager().PurgeReference(first.GetGUID(), _isPvP); // ...update the combat state, which will potentially remove IN_COMBAT... bool needFirstAI = first.GetCombatManager().UpdateOwnerCombatState(); bool needSecondAI = second.GetCombatManager().UpdateOwnerCombatState(); // ...and if that happened, also notify the AI of it... if (needFirstAI) { UnitAI firstAI = first.GetAI(); if (firstAI != null) firstAI.JustExitedCombat(); } if (needSecondAI) { UnitAI secondAI = second.GetAI(); if (secondAI != null) secondAI.JustExitedCombat(); } } public void Refresh() { bool needFirstAI = false, needSecondAI = false; if (_suppressFirst) { _suppressFirst = false; needFirstAI = first.GetCombatManager().UpdateOwnerCombatState(); } if (_suppressSecond) { _suppressSecond = false; needSecondAI = second.GetCombatManager().UpdateOwnerCombatState(); } if (needFirstAI) CombatManager.NotifyAICombat(first, second); if (needSecondAI) CombatManager.NotifyAICombat(second, first); } public void SuppressFor(Unit who) { Suppress(who); if (who.GetCombatManager().UpdateOwnerCombatState()) { UnitAI ai = who.GetAI(); if (ai != null) ai.JustExitedCombat(); } } // suppressed combat refs do not generate a combat state for one side of the relation // (used by: vanish, feign death) public bool IsSuppressedFor(Unit who) { return (who == first) ? _suppressFirst : _suppressSecond; } public void Suppress(Unit who) { if (who == first) _suppressFirst = true; else _suppressSecond = true; } public Unit GetOther(Unit me) { return (first == me) ? second : first; } } public class PvPCombatReference : CombatReference { public static uint PVP_COMBAT_TIMEOUT = 5 * Time.InMilliseconds; uint _combatTimer = PVP_COMBAT_TIMEOUT; public PvPCombatReference(Unit first, Unit second) : base(first, second, true) { } public bool Update(uint tdiff) { if (_combatTimer <= tdiff) return false; _combatTimer -= tdiff; return true; } public void RefreshTimer() { _combatTimer = PVP_COMBAT_TIMEOUT; } } }