Add AdventureBot combat reactions, training and corpse recovery
This commit is contained in:
1 parent
e18a416bf4
commit
c0c799c4b6
37 files changed
+1568
-18
No files matched your search
@@ -0,0 +1,287 @@
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureCombatPolicy.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "Creature.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 "SpellAuras.h"
|
||||
#include "WorldSession.h"
|
||||
#include "WorldPacket.h"
|
||||
#include "Opcodes.h"
|
||||
#include "GameTime.h"
|
||||
#include "Log.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
// 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 { }
|
||||
void Tick(Creature* target,uint32 diff)
|
||||
{
|
||||
_rend=_rend>diff?_rend-diff:0;_filler=_filler>diff?_filler-diff:0;
|
||||
me->SetFacingToObject(target);
|
||||
if(me->HasUnitState(UNIT_STATE_CASTING))return;
|
||||
if(me->getClass()==CLASS_WARRIOR && !me->HasAura(2457) && Cast(2457,me))return;
|
||||
if(!me->IsWithinMeleeRange(target))return; // MMAP approach is owned by manager.
|
||||
if(me->GetVictim()!=target)me->Attack(target,true);
|
||||
DoAutoAttackIfReady();
|
||||
if(me->getClass()!=CLASS_WARRIOR)return;
|
||||
if(!me->HasAura(6673) && Cast(6673,me))return;
|
||||
if(Cast(12294,target))return;
|
||||
if(!_rend && !target->GetAuraApplicationOfRankedSpell(772,me->GetGUID()) && Cast(772,target))
|
||||
{_rend=12000;return;}
|
||||
if(!_filler)for(uint32 id:{7384u,1464u,78u})if(Cast(id,target)){_filler=3000;break;}
|
||||
}
|
||||
private:
|
||||
bool Cast(uint32 id,Unit* target)
|
||||
{
|
||||
auto cast=VerifySpellCast(id,target);if(!cast)return false;
|
||||
uint32 actual=cast.first->GetSpellInfo()->Id;
|
||||
SpellCastTargets targets;targets.SetUnitTarget(cast.second);
|
||||
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;
|
||||
}
|
||||
uint32 _rend=0,_filler=0;
|
||||
};
|
||||
AdventureCombatMgr* AdventureCombatMgr::instance(){static AdventureCombatMgr mgr;return &mgr;}
|
||||
bool AdventureCombatMgr::Valid(Player* p,Creature* c,bool training) const
|
||||
{
|
||||
return c && c->IsAlive() && c->IsInWorld() && p->IsInPhase(c) && p->CanSeeOrDetect(c) &&
|
||||
p->IsValidAttackTarget(c) && !c->GetControllingPlayer() && !c->IsEvadingAttacks() &&
|
||||
(training?(c->GetScriptName()=="npc_training_dummy" && c->getLevel()<=p->getLevel()+2 && !c->isWorldBoss()):c->GetScriptName()!="npc_training_dummy");
|
||||
}
|
||||
bool AdventureCombatMgr::Reachable(Player* p,Creature* 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) || 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,Creature* 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?attacker->ToCreature():nullptr;
|
||||
if(!damage || !p->IsAlive() || !c || c->GetControllingPlayer())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;
|
||||
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.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){_records.erase(p->GetGUID().GetCounter());}
|
||||
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(intent=="adventure" && p->movespline->Finalized())return nullptr;
|
||||
Creature* best=nullptr;std::vector<Creature*> all;p->GetCreatureListWithEntryInGrid(all,0,AdventureCombatPolicy::SearchRadius);
|
||||
for(auto c:all)
|
||||
{
|
||||
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::Death(Player* p,Record& r)
|
||||
{
|
||||
if(!r.Dead)
|
||||
{
|
||||
if(r.AI)Finish(p,r);
|
||||
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->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();
|
||||
TC_LOG_INFO("server.adventurebot","DEATH_CEMETERY_REVIVE 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::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.AI || !p->IsAlive();
|
||||
if(r.Timer>diff){r.Timer-=diff;return r.AI || !p->IsAlive() || r.Dead;}
|
||||
r.Timer=500;
|
||||
if(!p->IsAlive())return Death(p,r);
|
||||
if(r.Dead){r.Dead=false;r.Suspended=false;r.NextCorpseSearch=0;r.FailedCorpsePoints.clear();r.Cooldown=30000;r.Timer=0;return false;}
|
||||
// First actual damager wins, including interrupting a proactive/training
|
||||
// target. Keep all subsequent damagers ordered for after that focus dies.
|
||||
if(!r.Defending)while(!r.Damagers.empty())
|
||||
{
|
||||
auto id=r.Damagers.front();r.Damagers.pop_front();auto c=ObjectAccessor::GetCreature(*p,id);
|
||||
if(!Valid(p,c,false))continue;
|
||||
if(r.AI)Finish(p,r);
|
||||
Start(p,r,c,false,true,0);break;
|
||||
}
|
||||
if(!r.AI && !p->IsInCombat() && !r.Cooldown)
|
||||
if(auto c=Proactive(p))Start(p,r,c,false,false,0);
|
||||
if(!r.AI)return false;
|
||||
auto c=ObjectAccessor::GetCreature(*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.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;
|
||||
if(!p->IsWithinMeleeRange(c))
|
||||
{
|
||||
auto nav=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
|
||||
if(!nav || nav->State!=ClientlessTravelState::Moving || nav->Target.GetExactDist(c)>5)
|
||||
{
|
||||
Position end;c->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,0.5f,c->GetAngle(p));
|
||||
sClientlessTravelMgr->StartTravel(p,end);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
|
||||
p->StopMoving();r.AI->Tick(c,500);
|
||||
return true;
|
||||
}
|
||||
Reference in new issue
Block a user