457 lines
15 KiB
C#
457 lines
15 KiB
C#
// Copyright (c) CypherCore <http://github.com/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<ObjectGuid, CombatReference> _pveRefs = new();
|
|
Dictionary<ObjectGuid, PvPCombatReference> _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<ObjectGuid, CombatReference> GetPvECombatRefs() { return _pveRefs; }
|
|
|
|
public Dictionary<ObjectGuid, PvPCombatReference> 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;
|
|
}
|
|
}
|
|
}
|