880 lines
44 KiB
C++
880 lines
44 KiB
C++
#include "AdventureCombatMgr.h"
|
|
#include "AdventureInteractionMgr.h"
|
|
#include "Group.h"
|
|
#include "LFG.h"
|
|
#include "AdventureCombatPolicy.h"
|
|
#include "AdventureRotationPolicy.h"
|
|
#include "AdventureGeneratorMgr.h"
|
|
#include "AdventureEncounterMgr.h"
|
|
#include "Pet.h"
|
|
#include "CreatureAI.h"
|
|
#include "ObjectMgr.h"
|
|
#include "AdventureBehaviorMgr.h"
|
|
#include "ClientlessLuaEngine.h"
|
|
#include "ClientlessPlayerMgr.h"
|
|
#include "ClientlessActionExecutorMgr.h"
|
|
#include "ClientlessTravelMgr.h"
|
|
#include "Creature.h"
|
|
#include "DBCStructure.h"
|
|
#include "Random.h"
|
|
#include "Corpse.h"
|
|
#include "Player.h"
|
|
#include "PlayerAI.h"
|
|
#include "ObjectAccessor.h"
|
|
#include "MotionMaster.h"
|
|
#include "MoveSpline.h"
|
|
#include "Map.h"
|
|
#include "Spell.h"
|
|
#include "SpellMgr.h"
|
|
#include "SpellAuras.h"
|
|
#include "WorldSession.h"
|
|
#include "WorldPacket.h"
|
|
#include "Opcodes.h"
|
|
#include "GameTime.h"
|
|
#include "Log.h"
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
|
|
namespace
|
|
{
|
|
bool AdventureTank(Unit* unit)
|
|
{
|
|
if(auto creature=unit->ToCreature())
|
|
return creature->IsVianoAdventurer() && creature->GetScriptName().find("_tank")!=std::string::npos;
|
|
auto player=unit->ToPlayer();if(!player)return false;
|
|
if(auto group=player->GetGroup())if(group->GetLfgRoles(player->GetGUID())&lfg::PLAYER_ROLE_TANK)return true;
|
|
auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
|
|
if(profile)return profile->Role=="tank";
|
|
auto spec=PlayerAI::GetPlayerSpec(player);
|
|
return (player->getClass()==CLASS_WARRIOR && spec==2) || (player->getClass()==CLASS_PALADIN && spec==1) ||
|
|
(player->getClass()==CLASS_DEATH_KNIGHT && spec==0) || (player->getClass()==CLASS_DRUID && player->HasAura(5487));
|
|
}
|
|
std::list<Unit*> AdventureAllies(Player* player)
|
|
{
|
|
std::list<Unit*> result;player->GetPartyMembers(result);
|
|
if(auto group=player->GetGroup())for(auto ref=group->GetFirstMember();ref;ref=ref->next())
|
|
if(auto member=ref->GetSource())if(player->IsInMap(member))
|
|
{
|
|
result.push_back(member);
|
|
std::list<Unit*> subgroup;member->GetPartyMembers(subgroup);
|
|
result.splice(result.end(),subgroup);
|
|
}
|
|
result.sort([](Unit* a,Unit* b){return a->GetGUID()<b->GetGUID();});result.unique();return result;
|
|
}
|
|
}
|
|
// Native player spell checks, cooldowns, rage and autoattacks. No charmer/fake AI
|
|
// installation and no generated resources or synthetic damage.
|
|
class AdventureCombatMgr::Fighter : public PlayerAI
|
|
{
|
|
public:
|
|
explicit Fighter(Player* player):PlayerAI(player) { }
|
|
void UpdateAI(uint32) override { }
|
|
bool Ranged() const {return AdventureRotationPolicy::Ranged(me->getClass(),GetPlayerSpec(me));}
|
|
bool Resurrect(Unit* target)
|
|
{
|
|
uint32 spell=me->getClass()==CLASS_PRIEST?2006:me->getClass()==CLASS_PALADIN?7328:me->getClass()==CLASS_SHAMAN?2008:me->getClass()==CLASS_DRUID?50769:0;
|
|
return spell && Cast(spell,target);
|
|
}
|
|
bool Heal(Unit* target,unsigned injured=0)
|
|
{
|
|
if(me->HasUnitState(UNIT_STATE_CASTING))return true;
|
|
auto klass=me->getClass();
|
|
float mana=me->GetMaxPower(POWER_MANA)?100.0f*me->GetPower(POWER_MANA)/me->GetMaxPower(POWER_MANA):100;
|
|
if(mana<30 && target->GetHealthPct()>50)
|
|
{
|
|
uint32 recovery=klass==CLASS_DRUID?29166:klass==CLASS_SHAMAN?16190:klass==CLASS_PALADIN?54428:0;
|
|
if(recovery && Cast(recovery,me))return true;
|
|
}
|
|
if(target->GetHealthPct()>45 && injured>=3)
|
|
{
|
|
uint32 groupSpell=klass==CLASS_DRUID?48438:klass==CLASS_SHAMAN?1064:klass==CLASS_PRIEST?34861:klass==CLASS_PALADIN?85222:0;
|
|
if(groupSpell && Allowed({groupSpell},target) && Cast(groupSpell,target))return true;
|
|
if(klass==CLASS_PRIEST && Cast(596,target))return true;
|
|
}
|
|
if(target->GetHealthPct()>50)
|
|
{
|
|
bool harmful=false;
|
|
for(auto const& pair:target->GetAppliedAuras())
|
|
if(!pair.second->IsPositive() && pair.second->GetBase()->GetSpellInfo()->Dispel!=DISPEL_NONE){harmful=true;break;}
|
|
uint32 dispel=klass==CLASS_PRIEST?527:klass==CLASS_PALADIN?4987:klass==CLASS_DRUID?88423:klass==CLASS_SHAMAN?51886:0;
|
|
if(harmful && dispel && Cast(dispel,target))return true;
|
|
}
|
|
for(auto const& action:AdventureRotationPolicy::Healing(me->getClass(),GetPlayerSpec(me),target->GetHealthPct()))
|
|
if(Allowed(action,target) && Cast(action.Spell,target))return true;
|
|
return false;
|
|
}
|
|
void Tick(Unit* target,uint32)
|
|
{
|
|
me->SetFacingToObject(target);
|
|
if(me->HasUnitState(UNIT_STATE_CASTING))return;
|
|
if(auto pet=me->GetPet())
|
|
{
|
|
if(pet->IsAlive() && pet->GetVictim()!=target)pet->AI()->AttackStart(target);
|
|
if(!pet->IsAlive())
|
|
{
|
|
if(me->getClass()==CLASS_HUNTER && Cast(982,pet))return;
|
|
if(me->getClass()==CLASS_WARLOCK && Cast(688,me))return;
|
|
}
|
|
}
|
|
else if(me->getClass()==CLASS_WARLOCK)
|
|
{
|
|
if(GetPlayerSpec(me)==1 && Cast(30146,me))return;
|
|
if(Cast(688,me))return;
|
|
}
|
|
else if(me->getClass()==CLASS_DEATH_KNIGHT && GetPlayerSpec(me)==2)
|
|
{
|
|
if(Cast(46584,me))return;
|
|
}
|
|
else if(me->getClass()==CLASS_HUNTER && me->HasSpell(883))
|
|
{
|
|
if(auto creature=sObjectMgr->GetCreatureTemplate(299))
|
|
if(creature->type==CREATURE_TYPE_BEAST)
|
|
{
|
|
auto pet=me->SummonPet(0,me->GetPositionX(),me->GetPositionY(),me->GetPositionZ(),me->GetOrientation(),HUNTER_PET,0);
|
|
if(!pet)if(auto created=me->CreateTamedPetFrom(299))
|
|
{
|
|
if(me->GetMap()->AddToMap(created->ToCreature()))
|
|
{
|
|
me->SetMinion(created,true);created->InitTalentForLevel();
|
|
created->SavePetToDB(PET_SAVE_NEW_PET);me->PetSpellInitialize();
|
|
}
|
|
else delete created;
|
|
}
|
|
}
|
|
}
|
|
if(me->getClass()==CLASS_WARRIOR && !me->HasAura(6673) && Cast(6673,me))return;
|
|
if(me->getClass()==CLASS_HUNTER && !me->HasAura(13165) && Cast(13165,me))return;
|
|
if(me->getClass()==CLASS_PALADIN && !me->HasAura(31801) && !me->HasAura(20154))
|
|
if(Cast(31801,me) || Cast(20154,me))return;
|
|
bool ranged=Ranged();
|
|
if(me->GetVictim()!=target)me->Attack(target,!ranged);
|
|
if(me->getClass()==CLASS_HUNTER && !me->GetCurrentSpell(CURRENT_AUTOREPEAT_SPELL) && Cast(75,target))return;
|
|
if(!ranged && me->IsWithinMeleeRange(target))DoAutoAttackIfReady();
|
|
if(target->HasUnitState(UNIT_STATE_CASTING))
|
|
{
|
|
uint32 interrupt=0;
|
|
switch(me->getClass())
|
|
{
|
|
case CLASS_WARRIOR:interrupt=6552;break;case CLASS_PALADIN:interrupt=96231;break;
|
|
case CLASS_ROGUE:interrupt=1766;break;case CLASS_DEATH_KNIGHT:interrupt=47528;break;
|
|
case CLASS_SHAMAN:interrupt=57994;break;case CLASS_MAGE:interrupt=2139;break;
|
|
case CLASS_DRUID:interrupt=80965;break;default:break;
|
|
}
|
|
if(interrupt && Cast(interrupt,target))return;
|
|
}
|
|
auto manaMax=me->GetMaxPower(POWER_MANA);
|
|
float mana=manaMax?100.0f*me->GetPower(POWER_MANA)/manaMax:100;
|
|
if(me->getClass()==CLASS_MAGE && mana<20 && me->getAttackers().empty() && Cast(12051,me))return;
|
|
if(me->getClass()==CLASS_WARLOCK && mana<25 && me->GetHealthPct()>70 && Cast(1454,me))return;
|
|
if(me->getClass()==CLASS_PRIEST && mana<40 && Cast(34433,target))return;
|
|
bool boss=false;if(auto creature=target->ToCreature())boss=creature->IsDungeonBoss() || creature->isWorldBoss();
|
|
if(boss || target->GetHealth()>me->GetMaxHealth())
|
|
{
|
|
uint32 cooldown=0;auto spec=GetPlayerSpec(me);
|
|
switch(me->getClass())
|
|
{
|
|
case CLASS_WARRIOR:cooldown=spec==2?0:spec==1?12292:1719;break;
|
|
case CLASS_PALADIN:cooldown=31884;break;
|
|
case CLASS_HUNTER:cooldown=3045;break;
|
|
case CLASS_ROGUE:cooldown=spec==1?13750:0;break;
|
|
case CLASS_DEATH_KNIGHT:cooldown=spec==1?51271:0;break;
|
|
case CLASS_SHAMAN:cooldown=spec==0?16166:0;break;
|
|
case CLASS_WARLOCK:cooldown=spec==1?47241:77801;break;
|
|
case CLASS_DRUID:cooldown=spec==1?50334:0;break;
|
|
default:break;
|
|
}
|
|
if(cooldown && Cast(cooldown,me))return;
|
|
}
|
|
if(boss)
|
|
{
|
|
if(me->getClass()==CLASS_SHAMAN && Cast(me->GetTeam()==ALLIANCE?32182:2825,me))return;
|
|
if(me->getClass()==CLASS_MAGE && Cast(80353,me))return;
|
|
}
|
|
auto profile=sClientlessLuaEngine->GetAdventureProfile(me->GetGUID().GetCounter());
|
|
bool tank=me->GetGroup() && profile && profile->Role=="tank";
|
|
if(tank && target->GetVictim() && target->GetVictim()!=me)
|
|
{
|
|
// Never steal from another tank, including summoned DungeonBots.
|
|
bool otherTank=AdventureTank(target->GetVictim());
|
|
uint32 taunt=me->getClass()==CLASS_WARRIOR?355:me->getClass()==CLASS_PALADIN?62124:me->getClass()==CLASS_DEATH_KNIGHT?56222:me->getClass()==CLASS_DRUID?6795:0;
|
|
if(!otherTank && taunt && Cast(taunt,target))return;
|
|
}
|
|
std::vector<Creature*> nearby;me->GetCreatureListWithEntryInGrid(nearby,0,12);
|
|
unsigned engaged=0;
|
|
for(auto enemy:nearby)if(enemy->IsAlive() && enemy->IsInCombat() && me->IsHostileTo(enemy))++engaged;
|
|
if(tank && engaged>=2)
|
|
{
|
|
auto klass=me->getClass();
|
|
if(klass==CLASS_WARRIOR){if(Cast(6343,me) || Cast(46968,target))return;}
|
|
if(klass==CLASS_PALADIN){if(Cast(53595,target) || Cast(26573,me))return;}
|
|
if(klass==CLASS_DEATH_KNIGHT){if(Cast(48721,me) || Cast(50842,target))return;}
|
|
if(klass==CLASS_DRUID && me->HasAura(5487)){if(Cast(77758,me) || Cast(779,me))return;}
|
|
}
|
|
if(tank && me->getClass()==CLASS_DRUID)
|
|
{
|
|
if(!me->HasAura(5487) && Cast(5487,me))return;
|
|
if(me->GetHealthPct()<40 && Cast(61336,me))return;
|
|
for(uint32 id:{33878u,33745u,6807u})if(Cast(id,target))return;
|
|
return;
|
|
}
|
|
for(auto const& action:AdventureRotationPolicy::Damage(me->getClass(),GetPlayerSpec(me)))
|
|
{
|
|
Unit* recipient=action.Self?me:target;
|
|
if(Allowed(action,recipient) && Cast(action.Spell,recipient))return;
|
|
}
|
|
}
|
|
private:
|
|
bool Allowed(AdventureRotationPolicy::Action const& action,Unit* target) const
|
|
{
|
|
if(action.Combo && me->GetComboPoints()<action.Combo)return false;
|
|
if((action.Spell==85256 || action.Spell==53600 || action.Spell==85222) &&
|
|
me->GetPower(POWER_HOLY_POWER)<3 && !me->HasAura(90174))return false;
|
|
if(action.Spell==8042 && me->getClass()==CLASS_SHAMAN && GetPlayerSpec(me)==0)
|
|
{
|
|
auto aura=me->GetAura(324);if(!aura || aura->GetCharges()<7)return false;
|
|
}
|
|
if(action.Spell==403 && me->getClass()==CLASS_SHAMAN && GetPlayerSpec(me)==1)
|
|
{auto aura=me->GetAura(53817);if(!aura || aura->GetStackAmount()<5)return false;}
|
|
if(action.Spell==2912 && me->HasAura(67483))return false;
|
|
if(action.Spell==5176 && me->HasAura(67484))return false;
|
|
if(action.Proc && !me->HasAura(action.Proc))return false;
|
|
using C=AdventureRotationPolicy::Condition;
|
|
switch(action.When)
|
|
{
|
|
case C::MissingAura:return !target->GetAuraApplicationOfRankedSpell(action.Aura?action.Aura:action.Spell,me->GetGUID());
|
|
case C::Execute:return target->GetHealthPct()<=20;
|
|
case C::Injured:return me->GetHealthPct()<65;
|
|
case C::Emergency:return me->GetHealthPct()<35;
|
|
case C::Resource:
|
|
case C::LowResource:
|
|
{
|
|
auto type=me->GetPowerType();auto maximum=me->GetMaxPower(type);
|
|
if(!maximum)return false;
|
|
auto percent=me->GetPower(type)*100/maximum;
|
|
return action.When==C::Resource?percent>=70:percent<40;
|
|
}
|
|
case C::Interrupt:return target->HasUnitState(UNIT_STATE_CASTING);
|
|
default:return true;
|
|
}
|
|
}
|
|
bool Cast(uint32 id,Unit* target)
|
|
{
|
|
auto cast=VerifySpellCast(id,target);if(!cast)return false;
|
|
auto info=cast.first->GetSpellInfo();
|
|
uint32 actual=info->Id;
|
|
if(!info->IsPositive() && info->IsAffectingArea())
|
|
{
|
|
float radius=8;
|
|
for(auto const& effect:info->Effects)radius=std::max(radius,effect.CalcRadius(me));
|
|
std::vector<Creature*> nearby;me->GetCreatureListWithEntryInGrid(nearby,0,std::min(80.0f,radius+me->GetExactDist(target)));
|
|
for(auto enemy:nearby)
|
|
if(enemy!=target && enemy->IsAlive() && me->IsInPhase(enemy) && me->IsHostileTo(enemy) &&
|
|
!enemy->IsInCombat() && (enemy->GetExactDist(target)<radius || enemy->GetExactDist(me)<radius))
|
|
{delete cast.first;return false;}
|
|
}
|
|
SpellCastTargets targets;targets.SetUnitTarget(cast.second);
|
|
if(info->GetExplicitTargetMask()&TARGET_FLAG_DEST_LOCATION)targets.SetDst(cast.second->GetPosition());
|
|
auto result=cast.first->prepare(targets);
|
|
if(result==SPELL_CAST_OK)TC_LOG_INFO("server.adventurebot","COMBAT_CAST guid=%u target='%s' spell=%u",me->GetGUID().GetCounter(),target->GetGUID().ToString().c_str(),actual);
|
|
return result==SPELL_CAST_OK;
|
|
}
|
|
};
|
|
AdventureCombatMgr* AdventureCombatMgr::instance(){static AdventureCombatMgr mgr;return &mgr;}
|
|
bool AdventureCombatMgr::Valid(Player* p,Unit* c,bool training) const
|
|
{
|
|
if(!c || !c->IsAlive() || !c->IsInWorld() || !p->IsInPhase(c) ||
|
|
!p->CanSeeOrDetect(c) || !p->IsValidAttackTarget(c))return false;
|
|
if(auto creature=c->ToCreature())
|
|
return !creature->IsEvadingAttacks() && (!training || !creature->GetControllingPlayer()) &&
|
|
(training?(creature->GetScriptName()=="npc_training_dummy" && creature->getLevel()<=p->getLevel()+2 && !creature->isWorldBoss()):creature->GetScriptName()!="npc_training_dummy");
|
|
return !training && c->ToPlayer(); // Native PvP/sanctuary/duel checks above.
|
|
}
|
|
bool AdventureCombatMgr::Reachable(Player* p,Unit* c) const
|
|
{
|
|
if(!p->IsWithinLOSInMap(c))return false;
|
|
if(p->IsWithinMeleeRange(c))return true;
|
|
Position end;c->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,0.5f,c->GetAngle(p));
|
|
ClientlessPathDiagnostics path;
|
|
return sClientlessTravelMgr->AnalyzePath(p,end,path) && path.Complete && !path.Suspicious && path.PathLength<=100;
|
|
}
|
|
bool AdventureCombatMgr::Safe(Player* p,Creature* c) const
|
|
{
|
|
if(!Valid(p,c,false) || c->GetControllingPlayer() || p->GetExactDist(c)>40 || (c->GetVictim() && c->GetVictim()!=p))return false;
|
|
std::vector<Creature*> others;p->GetCreatureListWithEntryInGrid(others,0,35);
|
|
unsigned hostiles=0;
|
|
for(auto other:others)if(other->IsAlive() && p->IsInPhase(other) && p->IsHostileTo(other) &&
|
|
other->GetExactDist(c)<12 && !other->GetControllingPlayer())++hostiles;
|
|
// Include neutral quarry itself in the risk count.
|
|
if(!p->IsHostileTo(c))++hostiles;
|
|
auto maxMana=p->GetMaxPower(POWER_MANA);
|
|
return AdventureCombatPolicy::SafeFirstAttack(p->getLevel(),c->getLevel(),p->GetHealthPct(),
|
|
c->isElite(),c->isWorldBoss(),hostiles,p->GetPowerType()==POWER_MANA,
|
|
maxMana?100.0f*p->GetPower(POWER_MANA)/maxMana:0) && Reachable(p,c);
|
|
}
|
|
Creature* AdventureCombatMgr::Dummy(Player* p) const
|
|
{
|
|
if(!p->IsAlive() || p->IsInCombat() || p->GetTransport() || p->IsInFlight() || p->IsBeingTeleported())return nullptr;
|
|
Creature* best=nullptr;std::vector<Creature*> all;p->GetCreatureListWithEntryInGrid(all,0,60);
|
|
for(auto c:all)if(Valid(p,c,true) && (!best || p->GetExactDist(c)<p->GetExactDist(best)) && Reachable(p,c))best=c;
|
|
return best;
|
|
}
|
|
bool AdventureCombatMgr::TrainingAvailable(Player* p) const{return Dummy(p)!=nullptr;}
|
|
bool AdventureCombatMgr::Start(Player* p,Record& r,Unit* c,bool training,bool defending,time_t until)
|
|
{
|
|
if(!Valid(p,c,training) || !p->IsAlive() || p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return false;
|
|
sAdventureBehaviorMgr->InterruptForCombat(p);
|
|
sClientlessLuaEngine->DismountGroundMount(p,"combat-start");
|
|
p->AttackStop();p->CastStop();p->StopMoving();p->GetMotionMaster()->Clear();p->SetStandState(UNIT_STAND_STATE_STAND);
|
|
r.AI=std::make_shared<Fighter>(p);r.Target=c->GetGUID();r.Training=training;r.Defending=defending;
|
|
r.Suspended=true;r.Until=until;r.Timer=0;r.UnreachableMs=0;
|
|
p->SetSelection(c->GetGUID());
|
|
TC_LOG_INFO("server.adventurebot","COMBAT_START guid=%u target='%s' training=%u defending=%u until=%llu",p->GetGUID().GetCounter(),c->GetGUID().ToString().c_str(),training,defending,static_cast<unsigned long long>(until));
|
|
return true;
|
|
}
|
|
bool AdventureCombatMgr::Train(Player* p,uint32 durationMs,time_t deadline)
|
|
{
|
|
auto c=Dummy(p);if(!c)return false;
|
|
auto now=GameTime::GetGameTime();auto until=now+std::max(10u,std::min(durationMs/1000,180u));
|
|
if(deadline && deadline<until)until=deadline;
|
|
if(until<=now)return false;
|
|
return Start(p,_records[p->GetGUID().GetCounter()],c,true,false,until);
|
|
}
|
|
bool AdventureCombatMgr::Attack(Player* p,uint32 spawn)
|
|
{
|
|
auto c=p->GetMap()->GetCreatureBySpawnId(spawn);
|
|
return !p->IsInCombat() && Safe(p,c) && Start(p,_records[p->GetGUID().GetCounter()],c,false,false,0);
|
|
}
|
|
void AdventureCombatMgr::DamageTaken(Player* p,Unit* attacker,uint32 damage)
|
|
{
|
|
auto c=attacker;
|
|
if(!damage || !p->IsAlive() || !c)return;
|
|
auto& r=_records[p->GetGUID().GetCounter()];
|
|
if(std::find(r.Damagers.begin(),r.Damagers.end(),c->GetGUID())==r.Damagers.end())r.Damagers.push_back(c->GetGUID());
|
|
}
|
|
void AdventureCombatMgr::Finish(Player* p,Record& r)
|
|
{
|
|
TC_LOG_INFO("server.adventurebot","COMBAT_END guid=%u target='%s' training=%u",p->GetGUID().GetCounter(),r.Target.ToString().c_str(),r.Training);
|
|
bool training=r.Training;
|
|
if(!training && r.Helper)sAdventureInteractionMgr->CombatHelp(p,r.Helper);
|
|
r.Helper.Clear();
|
|
p->AttackStop();p->CastStop();p->StopMoving();p->GetMotionMaster()->Clear();p->SetSelection(ObjectGuid::Empty);
|
|
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
|
|
if(training && std::all_of(p->getAttackers().begin(),p->getAttackers().end(),[](Unit* u)
|
|
{auto c=u->ToCreature();return c && c->GetScriptName()=="npc_training_dummy";}))p->CombatStop(true); // Only the bounded dummy scene may end its own combat.
|
|
r.Fleeing=false;r.ThreatPositions.clear();
|
|
r.AI.reset();r.Target.Clear();r.Training=false;r.Defending=false;r.UnreachableMs=0;r.Cooldown=10000;
|
|
}
|
|
void AdventureCombatMgr::Stop(Player* p)
|
|
{
|
|
auto it=_records.find(p->GetGUID().GetCounter());if(it!=_records.end())Finish(p,it->second);
|
|
}
|
|
void AdventureCombatMgr::Offline(Player* p){sAdventureEncounterMgr->Offline(p);_records.erase(p->GetGUID().GetCounter());}
|
|
Unit* AdventureCombatMgr::PvPTarget(Player* p) const
|
|
{
|
|
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
|
if(!profile)return nullptr;
|
|
auto trait=[&](char const* key)
|
|
{auto it=profile->Personality.find(key);return it==profile->Personality.end()?0.0f:it->second;};
|
|
auto chance=AdventureCombatPolicy::PvPWillingness(sAdventureBehaviorMgr->CombatIntent(p),
|
|
trait("adventurousness"),trait("danger_tolerance"));
|
|
auto aggression=profile->Personality.find("aggression");
|
|
if(aggression!=profile->Personality.end())chance*=std::clamp(aggression->second,0.0f,1.0f);
|
|
if(chance<=0 || p->GetHealthPct()<AdventureCombatPolicy::MinimumHealth)return nullptr;
|
|
if(p->GetPowerType()==POWER_MANA && p->GetMaxPower(POWER_MANA) &&
|
|
100.0f*p->GetPower(POWER_MANA)/p->GetMaxPower(POWER_MANA)<AdventureCombatPolicy::MinimumMana)return nullptr;
|
|
Unit* best=nullptr;
|
|
auto consider=[&](Unit* target)
|
|
{
|
|
if(!Valid(p,target,false) || !p->IsHostileTo(target) ||
|
|
!AdventureCombatPolicy::PvPLevel(p->getLevel(),target->getLevel()) ||
|
|
p->GetExactDist(target)>AdventureCombatPolicy::PvPRadius ||
|
|
(best && p->GetExactDist(target)>=p->GetExactDist(best)))return;
|
|
if(auto player=target->ToPlayer())
|
|
{if(player->GetTeam()==p->GetTeam())return;}
|
|
else
|
|
{
|
|
auto creature=target->ToCreature();
|
|
auto faction=target->GetFactionTemplateEntry();
|
|
auto enemy=p->GetTeam()==ALLIANCE?FACTION_GROUP_MASK_HORDE:FACTION_GROUP_MASK_ALLIANCE;
|
|
if(!creature || creature->GetControllingPlayer() || !faction || !(faction->FactionGroup & enemy) ||
|
|
creature->isElite() || creature->isWorldBoss())return;
|
|
}
|
|
if(Reachable(p,target))best=target;
|
|
};
|
|
std::vector<Player*> players;p->GetPlayerListInGrid(players,AdventureCombatPolicy::PvPRadius);
|
|
for(auto target:players)consider(target);
|
|
std::vector<Creature*> creatures;p->GetCreatureListWithEntryInGrid(creatures,0,AdventureCombatPolicy::PvPRadius);
|
|
for(auto target:creatures)consider(target);
|
|
return best && frand(0,1)<chance?best:nullptr;
|
|
}
|
|
Creature* AdventureCombatMgr::Proactive(Player* p) const
|
|
{
|
|
auto intent=sAdventureBehaviorMgr->CombatIntent(p);
|
|
if(intent!="adventure" && intent!="profession")return nullptr;
|
|
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
|
if(intent=="profession" && (!profile || !profile->PrimaryProfessions.count("skinning")))return nullptr;
|
|
if(profile)
|
|
{
|
|
auto aggression=profile->Personality.find("aggression");
|
|
// Stable per-bot/per-target-window willingness, no repeated rolls each tick.
|
|
if(aggression!=profile->Personality.end())
|
|
{
|
|
auto window=uint32(GameTime::GetGameTime()/30);
|
|
auto value=(p->GetGUID().GetCounter()*2654435761u+window*2246822519u)%1000;
|
|
if(value>=std::clamp(aggression->second,0.0f,1.0f)*1000)return nullptr;
|
|
}
|
|
}
|
|
if(intent=="adventure" && p->movespline->Finalized())return nullptr;
|
|
Creature* best=nullptr;std::vector<Creature*> all;p->GetCreatureListWithEntryInGrid(all,0,AdventureCombatPolicy::SearchRadius);
|
|
for(auto c:all)
|
|
{
|
|
auto faction=c->GetFactionTemplateEntry();
|
|
if(faction && (faction->FactionGroup & (FACTION_GROUP_MASK_ALLIANCE|FACTION_GROUP_MASK_HORDE)))continue;
|
|
if(best && p->GetExactDist(c)>=p->GetExactDist(best))continue;
|
|
if(intent=="adventure")
|
|
{
|
|
if(!p->IsHostileTo(c))continue;
|
|
bool nearPath=false;auto path=sClientlessTravelMgr->GetActivePath(p->GetGUID().GetCounter());
|
|
if(!path)continue;
|
|
auto const& points=*path;
|
|
for(std::size_t i=1;i<points.size();++i)
|
|
if(AdventureCombatPolicy::SegmentDistance(c->GetPositionX(),c->GetPositionY(),c->GetPositionZ(),points[i-1].GetPositionX(),points[i-1].GetPositionY(),points[i-1].GetPositionZ(),points[i].GetPositionX(),points[i].GetPositionY(),points[i].GetPositionZ())<=AdventureCombatPolicy::PathCorridor){nearPath=true;break;}
|
|
if(!nearPath)continue;
|
|
}
|
|
else if(c->GetCreatureTemplate()->type!=CREATURE_TYPE_BEAST || !c->GetCreatureTemplate()->SkinLootId)continue;
|
|
if(Safe(p,c))best=c;
|
|
}
|
|
return best;
|
|
}
|
|
bool AdventureCombatMgr::Flee(Player* p,Record& r)
|
|
{
|
|
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
|
if(p->GetGroup() || (profile && !profile->AllowRetreat))
|
|
{
|
|
if(r.Fleeing)
|
|
{
|
|
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());p->StopMoving();
|
|
r.Fleeing=false;r.ThreatPositions.clear();
|
|
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_CANCEL guid=%u reason='%s'",p->GetGUID().GetCounter(),p->GetGroup()?"group":"profile");
|
|
}
|
|
return false;
|
|
}
|
|
std::vector<Unit*> threats;
|
|
auto add=[&](Unit* u)
|
|
{
|
|
if(u && u!=p && u->IsAlive() && u->IsInWorld() && p->IsInPhase(u) &&
|
|
p->CanSeeOrDetect(u) && std::find(threats.begin(),threats.end(),u)==threats.end())threats.push_back(u);
|
|
};
|
|
for(auto u:p->getAttackers())add(u);
|
|
for(auto id:r.Damagers)add(ObjectAccessor::GetUnit(*p,id));
|
|
if(r.Defending || (p->IsInCombat() && !r.Training))add(ObjectAccessor::GetUnit(*p,r.Target));
|
|
bool danger=false;
|
|
for(auto u:threats)
|
|
{
|
|
auto c=u->ToCreature();
|
|
if(AdventureCombatPolicy::ShouldFlee(false,p->GetHealthPct(),p->getLevel(),u->getLevel(),
|
|
c && c->isElite(),c && c->isWorldBoss()))danger=true;
|
|
}
|
|
if(!r.Fleeing && (!danger || r.Training))return false;
|
|
if(!r.Fleeing)
|
|
{
|
|
r.Fleeing=true;r.NextEscape=0;
|
|
sAdventureBehaviorMgr->InterruptForCombat(p);
|
|
sClientlessLuaEngine->DismountGroundMount(p,"combat-flee");
|
|
p->AttackStop();p->CastStop();p->StopMoving();p->GetMotionMaster()->Clear();
|
|
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_START guid=%u",p->GetGUID().GetCounter());
|
|
}
|
|
// Keep last known threat positions when attackers vanish from visibility.
|
|
if(!threats.empty())
|
|
{
|
|
r.ThreatPositions.clear();
|
|
for(auto u:threats)r.ThreatPositions.push_back(u->GetPosition());
|
|
}
|
|
auto clearance=[&](Position const& point)
|
|
{
|
|
float distance=1000;
|
|
for(auto const& threat:r.ThreatPositions)distance=std::min(distance,point.GetExactDist(threat));
|
|
return distance;
|
|
};
|
|
if(!p->IsInCombat() && clearance(p->GetPosition())>=50)
|
|
{
|
|
r.Fleeing=false;r.ThreatPositions.clear();r.Damagers.clear();
|
|
Finish(p,r);r.Cooldown=30000;
|
|
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_END guid=%u",p->GetGUID().GetCounter());
|
|
return true;
|
|
}
|
|
auto now=GameTime::GetGameTime();
|
|
if(now<r.NextEscape)return true;
|
|
r.NextEscape=now+2;
|
|
std::vector<Creature*> creatures;p->GetCreatureListWithEntryInGrid(creatures,0,80);
|
|
std::vector<Player*> players;p->GetPlayerListInGrid(players,80);
|
|
std::vector<Unit*> nearby;
|
|
for(auto c:creatures)nearby.push_back(c);
|
|
for(auto other:players)nearby.push_back(other);
|
|
Position best;bool found=false;float score=-1e30f;
|
|
float current=clearance(p->GetPosition());
|
|
for(float radius:{18.0f,30.0f})for(unsigned i=0;i<12;++i)
|
|
{
|
|
float angle=i*6.2831853f/12;
|
|
float x=p->GetPositionX()+radius*std::cos(angle),y=p->GetPositionY()+radius*std::sin(angle);
|
|
float z=p->GetMap()->GetHeight(p->GetPhaseShift(),x,y,p->GetPositionZ()+5,true,15);
|
|
if(z<=INVALID_HEIGHT)continue;
|
|
Position end;end.Relocate(x,y,z);
|
|
float distance=clearance(end);
|
|
if(distance<current+3)continue;
|
|
bool unsafe=false;
|
|
for(auto u:nearby)
|
|
{
|
|
if(!u->IsAlive() || !p->IsInPhase(u) || !p->CanSeeOrDetect(u) || !p->IsHostileTo(u) ||
|
|
std::find(threats.begin(),threats.end(),u)!=threats.end())continue;
|
|
if(AdventureCombatPolicy::SegmentDistance(u->GetPositionX(),u->GetPositionY(),u->GetPositionZ(),
|
|
p->GetPositionX(),p->GetPositionY(),p->GetPositionZ(),x,y,z)<12){unsafe=true;break;}
|
|
}
|
|
if(unsafe)continue;
|
|
ClientlessPathDiagnostics path;
|
|
if(!sClientlessTravelMgr->AnalyzePath(p,end,path) || !path.Complete || path.Suspicious || path.PathLength>60)continue;
|
|
float value=distance-0.15f*path.PathLength;
|
|
if(value>score){score=value;best=end;found=true;}
|
|
}
|
|
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
|
|
p->AttackStop();p->CastStop();
|
|
if(found && sClientlessTravelMgr->StartTravel(p,best))
|
|
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_PATH guid=%u clearance=%.2f",p->GetGUID().GetCounter(),clearance(best));
|
|
else
|
|
{
|
|
p->StopMoving();
|
|
auto target=ObjectAccessor::GetUnit(*p,r.Target);
|
|
if(r.AI && Valid(p,target,false) && p->IsWithinMeleeRange(target))r.AI->Tick(target,500);
|
|
TC_LOG_INFO("server.adventurebot","COMBAT_FLEE_BLOCKED guid=%u",p->GetGUID().GetCounter());
|
|
}
|
|
return true;
|
|
}
|
|
void AdventureCombatMgr::Revived(Player* player,time_t when)
|
|
{
|
|
auto& r=_records[player->GetGUID().GetCounter()];
|
|
r.DeathChain.Revived=when;r.Dead=false;r.ForceGraveyard=false;r.CemeteryPending=false;
|
|
r.Suspended=false;r.NextCorpseSearch=0;r.FailedCorpsePoints.clear();r.Cooldown=30000;r.Timer=0;
|
|
}
|
|
|
|
bool AdventureCombatMgr::Death(Player* p,Record& r)
|
|
{
|
|
if(!r.Dead)
|
|
{
|
|
r.ForceGraveyard=r.DeathChain.Died(GameTime::GetGameTime());
|
|
if(r.AI)Finish(p,r);
|
|
r.Fleeing=false;r.ThreatPositions.clear();
|
|
sAdventureBehaviorMgr->InterruptForCombat(p);r.Damagers.clear();r.Dead=true;r.Suspended=true;
|
|
TC_LOG_INFO("server.adventurebot","DEATH_RELEASE guid=%u",p->GetGUID().GetCounter());
|
|
}
|
|
if(p->InBattleground() || p->InArena() || p->IsBeingTeleported())return true;
|
|
if(p->IsResurrectRequested()){p->ResurrectUsingRequestData();return true;}
|
|
if(sAdventureEncounterMgr->Death(p))return true;
|
|
if(p->GetMap()->IsDungeon() && p->GetGroup())
|
|
{
|
|
std::list<Unit*> allies;p->GetPartyMembers(allies);
|
|
if(std::any_of(allies.begin(),allies.end(),[&](Unit* ally){return ally!=p && ally->IsAlive() && p->IsInMap(ally);}))return true;
|
|
}
|
|
if(!p->HasFlag(PLAYER_FLAGS,PLAYER_FLAGS_GHOST))
|
|
{
|
|
WorldPacket release(CMSG_REPOP_REQUEST,1);release<<uint8(0);p->GetSession()->HandleRepopRequestOpcode(release);
|
|
if(auto corpse=p->GetCorpse())corpse->SaveToDB();
|
|
p->SaveToDB();
|
|
return true;
|
|
}
|
|
if(r.CemeteryPending)
|
|
{
|
|
r.CemeteryPending=false;
|
|
p->ResurrectPlayer(0.5f,false);p->SpawnCorpseBones();
|
|
r.ForceGraveyard=false;
|
|
TC_LOG_INFO("server.adventurebot","DEATH_CEMETERY_REVIVE guid=%u",p->GetGUID().GetCounter());
|
|
return true;
|
|
}
|
|
if(r.ForceGraveyard)
|
|
{
|
|
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
|
|
r.CemeteryPending=true;p->RepopAtGraveyard();
|
|
TC_LOG_INFO("server.adventurebot","DEATH_CHAIN_GRAVEYARD guid=%u",p->GetGUID().GetCounter());
|
|
return true;
|
|
}
|
|
auto corpse=p->GetCorpse();if(!corpse)return true;
|
|
auto now=GameTime::GetGameTime();
|
|
if(corpse->IsWithinDistInMap(p,CORPSE_RECLAIM_RADIUS,true))
|
|
{
|
|
if(now<corpse->GetGhostTime()+p->GetCorpseReclaimDelay(corpse->GetType()==CORPSE_RESURRECTABLE_PVP))return true;
|
|
WorldPacket reclaim(CMSG_RECLAIM_CORPSE,8);reclaim<<corpse->GetGUID();p->GetSession()->HandleReclaimCorpseOpcode(reclaim);
|
|
if(p->IsAlive())TC_LOG_INFO("server.adventurebot","DEATH_RECLAIM guid=%u",p->GetGUID().GetCounter());
|
|
return true;
|
|
}
|
|
auto travel=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
|
|
if(travel && travel->State==ClientlessTravelState::Moving)return true;
|
|
if(travel && (travel->State==ClientlessTravelState::Stuck || travel->State==ClientlessTravelState::PathRejected))
|
|
{
|
|
r.FailedCorpsePoints.push_back(travel->Target);
|
|
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
|
|
}
|
|
if(now<r.NextCorpseSearch)return true;
|
|
r.NextCorpseSearch=now+5;
|
|
// Try a set of legal reclaim points, including near-side points around the
|
|
// body; an unreachable exact corpse position alone never triggers fallback.
|
|
bool pathFound=false;Position best;float shortest=1e30f;
|
|
if(p->GetMapId()==corpse->GetMapId() && p->GetInstanceId()==corpse->GetInstanceId())for(unsigned ring=0;ring<3;++ring)for(unsigned i=0;i<(ring?12u:1u);++i)
|
|
{
|
|
Position end;float radius=ring==1?20.0f:ring==2?32.0f:0.0f;float angle=i*6.2831853f/12;
|
|
float x=corpse->GetPositionX()+radius*std::cos(angle),y=corpse->GetPositionY()+radius*std::sin(angle);
|
|
float z=p->GetMap()->GetHeight(p->GetPhaseShift(),x,y,corpse->GetPositionZ()+3,true,8);
|
|
if(z<=INVALID_HEIGHT)continue;
|
|
end.Relocate(x,y,z);
|
|
if(end.GetExactDist(corpse)>CORPSE_RECLAIM_RADIUS-2)continue;
|
|
if(std::any_of(r.FailedCorpsePoints.begin(),r.FailedCorpsePoints.end(),[&](Position const& failed){return failed.GetExactDist(end)<3;}))continue;
|
|
ClientlessPathDiagnostics path;
|
|
if(sClientlessTravelMgr->AnalyzePath(p,end,path) && path.Complete && !path.Suspicious && path.PathLength<shortest)
|
|
{pathFound=true;best=end;shortest=path.PathLength;}
|
|
}
|
|
if(pathFound)
|
|
{
|
|
sClientlessTravelMgr->StartTravel(p,best);
|
|
TC_LOG_INFO("server.adventurebot","DEATH_CORPSE_RUN guid=%u path=%.2f",p->GetGUID().GetCounter(),shortest);
|
|
}
|
|
else if(AdventureCombatPolicy::CemeteryFallback(now,corpse->GetGhostTime(),true))
|
|
{
|
|
// Native nearest graveyard relocation. No healer interaction, sickness,
|
|
// durability repair/loss or resource cheat; native resurrection at 50%.
|
|
r.CemeteryPending=true;
|
|
p->RepopAtGraveyard();
|
|
}
|
|
return true;
|
|
}
|
|
bool AdventureCombatMgr::GroupTarget(Player* p,Record& r)
|
|
{
|
|
if(!p->GetGroup() || r.Training)return false;
|
|
auto allies=AdventureAllies(p);Unit* tank=nullptr;Player* human=nullptr;
|
|
for(auto ally:allies)
|
|
{
|
|
if(!ally->IsAlive() || !p->IsInMap(ally))continue;
|
|
if(AdventureTank(ally) && (!tank || ally->GetGUID()<tank->GetGUID()))tank=ally;
|
|
if(auto player=ally->ToPlayer())if(!player->GetSession()->IsClientless())
|
|
if(!human || player->GetGUID()==p->GetGroup()->GetLeaderGUID())human=player;
|
|
}
|
|
Unit* chosen=nullptr;bool isTank=AdventureTank(p);
|
|
if(isTank)
|
|
{
|
|
// Protect companions first, but do not steal another tank's targets.
|
|
for(auto ally:allies)if(ally->IsAlive() && !AdventureTank(ally))
|
|
for(auto attacker:ally->getAttackers())
|
|
if(Valid(p,attacker,false) && attacker->GetVictim()==ally && p->GetExactDist(attacker)<35 &&
|
|
(!chosen || p->GetExactDist(attacker)<p->GetExactDist(chosen)))chosen=attacker;
|
|
}
|
|
if(!chosen && tank && tank->IsInCombat() && Valid(p,tank->GetVictim(),false))chosen=tank->GetVictim();
|
|
if(!chosen && human && human->IsInCombat() && Valid(p,human->GetVictim(),false))chosen=human->GetVictim();
|
|
if(!chosen)for(auto ally:allies)
|
|
{
|
|
if(ally->IsInCombat() && Valid(p,ally->GetVictim(),false)){chosen=ally->GetVictim();break;}
|
|
for(auto attacker:ally->getAttackers())if(Valid(p,attacker,false)){chosen=attacker;break;}
|
|
if(chosen)break;
|
|
}
|
|
if(!chosen && isTank && tank==p && human && sAdventureEncounterMgr->Active(p) && !r.AI && !r.Cooldown)
|
|
{
|
|
bool ready=p->GetHealthPct()>=70 && human->IsAlive() && p->GetExactDist(human)<25;
|
|
for(auto ally:allies)if(ally->IsAlive())
|
|
{
|
|
if(p->GetExactDist(ally)>35)ready=false;
|
|
if(auto member=ally->ToPlayer())
|
|
{
|
|
auto profile=sClientlessLuaEngine->GetAdventureProfile(member->GetGUID().GetCounter());
|
|
if((profile && profile->Role=="healer") || PlayerAI::IsPlayerHealer(member))
|
|
if(member->GetMaxPower(POWER_MANA) && member->GetPower(POWER_MANA)*100/member->GetMaxPower(POWER_MANA)<35)ready=false;
|
|
}
|
|
if(auto creature=ally->ToCreature())
|
|
if(creature->IsVianoAdventurer() && creature->GetScriptName().find("_healer")!=std::string::npos &&
|
|
creature->GetMaxPower(POWER_MANA) && creature->GetPower(POWER_MANA)*100/creature->GetMaxPower(POWER_MANA)<35)ready=false;
|
|
}
|
|
if(ready)
|
|
{
|
|
auto selected=ObjectAccessor::GetUnit(*p,human->GetTarget());
|
|
if(selected && Valid(p,selected,false) && p->IsHostileTo(selected) && p->GetExactDist(selected)<25)chosen=selected;
|
|
if(!chosen && human->isMoving())
|
|
{
|
|
std::vector<Creature*> enemies;p->GetCreatureListWithEntryInGrid(enemies,0,20);
|
|
for(auto enemy:enemies)
|
|
if(Valid(p,enemy,false) && p->IsHostileTo(enemy) && !enemy->GetControllingPlayer() &&
|
|
p->HasInArc(3.14159265f,enemy) && (!chosen || p->GetExactDist(enemy)<p->GetExactDist(chosen)) && Reachable(p,enemy))chosen=enemy;
|
|
}
|
|
}
|
|
}
|
|
if(!chosen && isTank && tank && tank!=p && sAdventureEncounterMgr->Active(p) && !r.AI && !p->IsInCombat())
|
|
{
|
|
if(p->GetExactDist(tank)>4)
|
|
{
|
|
auto travel=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
|
|
if(!travel || travel->State!=ClientlessTravelState::Moving)
|
|
{
|
|
Position point;tank->GetNearPoint(p,point.m_positionX,point.m_positionY,point.m_positionZ,2,tank->GetAngle(p));
|
|
sClientlessTravelMgr->StartTravel(p,point);
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
if(chosen && p->GetExactDist(chosen)<=60 && (!r.AI || r.Target!=chosen->GetGUID()))
|
|
{
|
|
// Group role changes intentionally supersede solo first-damager focus.
|
|
if(r.AI)Finish(p,r);
|
|
Start(p,r,chosen,false,false,0);
|
|
}
|
|
return false;
|
|
}
|
|
bool AdventureCombatMgr::GroupHeal(Player* p,Record& r)
|
|
{
|
|
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
|
if(!p->GetGroup() || !profile)return false;
|
|
auto allies=AdventureAllies(p);
|
|
bool fighting=std::any_of(allies.begin(),allies.end(),[](Unit* ally){return ally->IsAlive() && ally->IsInCombat();});
|
|
if(!fighting && !p->HasUnitState(UNIT_STATE_CASTING))
|
|
for(auto ally:allies)if(auto player=ally->ToPlayer())
|
|
if(!player->IsAlive() && !player->IsResurrectRequested() && p->IsInMap(player) && p->IsWithinLOSInMap(player) && p->GetExactDist(player)<30)
|
|
{
|
|
if(!r.Support)r.Support=std::make_shared<Fighter>(p);
|
|
if(r.Support->Resurrect(player))return true;
|
|
}
|
|
if(profile->Role!="healer")return false;
|
|
Unit* best=nullptr;float priority=-1;
|
|
for(auto ally:allies)
|
|
{
|
|
if(!ally->IsAlive() || !p->IsInMap(ally) || !p->IsInPhase(ally) || !p->IsFriendlyTo(ally) ||
|
|
p->GetExactDist(ally)>60 || ally->GetHealthPct()>90)continue;
|
|
float score=100-ally->GetHealthPct()+std::min<size_t>(ally->getAttackers().size(),5)*8;
|
|
if(ally->GetHealthPct()<35)score+=150;
|
|
if(!best || score>priority){best=ally;priority=score;}
|
|
}
|
|
if(!best)return false;
|
|
if(p->GetExactDist(best)>35 || !p->IsWithinLOSInMap(best))
|
|
{
|
|
auto current=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
|
|
if(current && current->State==ClientlessTravelState::Moving)return true;
|
|
Position point;best->GetNearPoint(p,point.m_positionX,point.m_positionY,point.m_positionZ,15,best->GetAngle(p));
|
|
ClientlessPathDiagnostics path;
|
|
if(sClientlessTravelMgr->AnalyzePath(p,point,path) && path.Complete && !path.Suspicious)
|
|
return sClientlessTravelMgr->StartTravel(p,point);
|
|
return best->GetHealthPct()<70;
|
|
}
|
|
if(!r.Support)r.Support=std::make_shared<Fighter>(p);
|
|
if(p->HasUnitState(UNIT_STATE_CASTING))return true;
|
|
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());p->StopMoving();
|
|
unsigned injured=0;
|
|
for(auto ally:allies)if(ally->IsAlive() && ally->GetHealthPct()<80 && ally->GetExactDist(best)<15)++injured;
|
|
p->SetFacingToObject(best);
|
|
if(r.Support->Heal(best,injured))return true;
|
|
// A healer awaiting resources/cooldowns keeps its assignment rather than
|
|
// starting a damage rotation while somebody still needs substantial help.
|
|
return best->GetHealthPct()<70;
|
|
}
|
|
bool AdventureCombatMgr::Update(Player* p,uint32 diff)
|
|
{
|
|
if(!p->IsInWorld())return false;
|
|
auto& r=_records[p->GetGUID().GetCounter()];
|
|
r.Cooldown=r.Cooldown>diff?r.Cooldown-diff:0;
|
|
if(p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return r.Fleeing || r.AI || !p->IsAlive();
|
|
if(r.Timer>diff){r.Timer-=diff;return r.Fleeing || r.AI || !p->IsAlive() || r.Dead;}
|
|
r.Timer=500;
|
|
if(p->IsAlive() && sAdventureEncounterMgr->Update(p,500))return true;
|
|
if(!p->IsAlive())return Death(p,r);
|
|
if(r.Dead)
|
|
{
|
|
r.DeathChain.Revived=GameTime::GetGameTime();
|
|
r.Dead=false;r.ForceGraveyard=false;r.Suspended=false;r.NextCorpseSearch=0;r.FailedCorpsePoints.clear();r.Cooldown=30000;r.Timer=0;
|
|
return false;
|
|
}
|
|
if(p->IsResurrectRequested())p->ClearResurrectRequestData();
|
|
if(!p->IsInCombat() && !p->HasUnitState(UNIT_STATE_CASTING) && !p->IsFlying() && !p->IsFalling() && GameTime::GetGameTime()>=r.NextRole)
|
|
{
|
|
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
|
if(profile)
|
|
{
|
|
unsigned desired=profile->Class?(p->GetGroup()?profile->Specialization:profile->SoloSpecialization):
|
|
AdventureRotationPolicy::SoloSpec(p->getClass(),p->GetGUID().GetCounter());
|
|
if(p->getLevel()>=10 && (PlayerAI::GetPlayerSpec(p)!=desired || r.PreparedMode!=int(p->GetGroup()!=nullptr)))
|
|
{
|
|
r.NextRole=GameTime::GetGameTime()+10;
|
|
std::string error;
|
|
if(!sAdventureGeneratorMgr->PrepareCombat(p,error))
|
|
TC_LOG_WARN("server.adventurebot","COMBAT_ROLE_PREPARE guid=%u error='%s'",p->GetGUID().GetCounter(),error.c_str());
|
|
else r.PreparedMode=int(p->GetGroup()!=nullptr);
|
|
}
|
|
}
|
|
}
|
|
if(GroupHeal(p,r))return true;
|
|
if(GroupTarget(p,r))return true;
|
|
// First actual damager wins, including interrupting a proactive/training
|
|
// target. Keep all subsequent damagers ordered for after that focus dies.
|
|
if(!r.Defending && !p->GetGroup())while(!r.Damagers.empty())
|
|
{
|
|
auto id=r.Damagers.front();r.Damagers.pop_front();auto c=ObjectAccessor::GetUnit(*p,id);
|
|
if(!Valid(p,c,false))continue;
|
|
if(r.AI)Finish(p,r);
|
|
Start(p,r,c,false,true,0);break;
|
|
}
|
|
if(Flee(p,r))return true;
|
|
if(!r.AI && (!p->GetGroup() || p->GetGroup()->GetLeaderGUID()==p->GetGUID()) && !p->IsInCombat() && !r.Cooldown)
|
|
{
|
|
auto now=GameTime::GetGameTime();
|
|
if(now>=r.NextPvP)
|
|
{
|
|
r.NextPvP=now+30;
|
|
if(auto target=PvPTarget(p))
|
|
{
|
|
TC_LOG_INFO("server.adventurebot","COMBAT_PVP_INITIATE guid=%u target='%s'",p->GetGUID().GetCounter(),target->GetGUID().ToString().c_str());
|
|
Start(p,r,target,false,false,0);
|
|
}
|
|
}
|
|
if(!r.AI && !p->GetGroup())if(auto c=Proactive(p))Start(p,r,c,false,false,0);
|
|
}
|
|
if(!r.AI)return false;
|
|
auto c=ObjectAccessor::GetUnit(*p,r.Target);
|
|
if(!Valid(p,c,r.Training) || (r.Until && GameTime::GetGameTime()>=r.Until) || p->GetExactDist(c)>80)
|
|
{Finish(p,r);return true;}
|
|
if(!Reachable(p,c) && !(r.AI->Ranged() && p->GetExactDist(c)<=25 && p->IsWithinLOSInMap(c)))
|
|
{
|
|
r.UnreachableMs+=500;
|
|
if(r.UnreachableMs>=20000){TC_LOG_WARN("server.adventurebot","COMBAT_TARGET_UNREACHABLE guid=%u",p->GetGUID().GetCounter());Finish(p,r);}
|
|
return true;
|
|
}
|
|
r.UnreachableMs=0;
|
|
bool inRange=r.AI->Ranged()?(p->GetExactDist(c)<=25 && p->IsWithinLOSInMap(c)):p->IsWithinMeleeRange(c);
|
|
if(!inRange)
|
|
{
|
|
auto nav=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
|
|
if(!nav || nav->State!=ClientlessTravelState::Moving || nav->Target.GetExactDist(c)>5)
|
|
{
|
|
float angle=c->GetAngle(p);
|
|
if(p->GetGroup() && !AdventureTank(p) && c->GetVictim()!=p && !r.AI->Ranged())angle=c->GetOrientation()+3.14159265f;
|
|
Position end;c->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,0.5f,angle);
|
|
sClientlessTravelMgr->StartTravel(p,end);
|
|
}
|
|
return true;
|
|
}
|
|
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
|
|
p->StopMoving();r.AI->Tick(c,500);
|
|
return true;
|
|
}
|
|
|
|
bool AdventureCombatMgr::Busy(Player* player) const
|
|
{
|
|
if(player->GetGroup())for(auto ally:AdventureAllies(player))if(ally->IsAlive() && ally->IsInCombat())return true;
|
|
auto it=_records.find(player->GetGUID().GetCounter());
|
|
return it!=_records.end() && (it->second.Fleeing || it->second.AI || it->second.Dead || !it->second.Damagers.empty());
|
|
}
|
|
|
|
bool AdventureCombatMgr::Assist(Player* p,Unit* target)
|
|
{
|
|
if(!p->GetGroup() || Busy(p) || !Valid(p,target,false) || p->GetExactDist(target)>40 ||
|
|
!Reachable(p,target) || p->GetHealthPct()<30)return false;
|
|
if(auto c=target->ToCreature())
|
|
if(c->isWorldBoss() || c->getLevel()>p->getLevel()+2)return false;
|
|
return Start(p,_records[p->GetGUID().GetCounter()],target,false,false,0);
|
|
}
|
|
|
|
void AdventureCombatMgr::SharedTargetDamage(Player* p,ObjectGuid helper,ObjectGuid target)
|
|
{
|
|
auto it=_records.find(p->GetGUID().GetCounter());
|
|
if(it!=_records.end() && it->second.Target==target && !it->second.Training)it->second.Helper=helper;
|
|
}
|