255 lines
14 KiB
C++
255 lines
14 KiB
C++
#include "AdventureInteractionMgr.h"
|
|
#include "AdventureInteractionPolicy.h"
|
|
#include "AdventureBehaviorMgr.h"
|
|
#include "AdventureCombatMgr.h"
|
|
#include "ClientlessLuaEngine.h"
|
|
#include "ClientlessPlayerMgr.h"
|
|
#include "ClientlessTravelMgr.h"
|
|
#include "ClientlessActionExecutorMgr.h"
|
|
#include "ClientlessTravelGraphMgr.h"
|
|
#include "Chat.h"
|
|
#include "Creature.h"
|
|
#include "GameTime.h"
|
|
#include "Group.h"
|
|
#include "Map.h"
|
|
#include "MotionMaster.h"
|
|
#include "ObjectAccessor.h"
|
|
#include "Opcodes.h"
|
|
#include "PartyPackets.h"
|
|
#include "Player.h"
|
|
#include "Random.h"
|
|
#include "WorldPacket.h"
|
|
#include "WorldSession.h"
|
|
#include "Log.h"
|
|
#include <vector>
|
|
|
|
namespace
|
|
{
|
|
float Sociability(Player* p)
|
|
{
|
|
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
|
if(profile){auto it=profile->Personality.find("sociability");if(it!=profile->Personality.end())return it->second;}
|
|
return 0.5f;
|
|
}
|
|
float Loyalty(Player* p)
|
|
{
|
|
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
|
if(profile){auto it=profile->Personality.find("group_loyalty");if(it!=profile->Personality.end())return std::max(0.0f,std::min(1.0f,it->second));}
|
|
return 0.25f;
|
|
}
|
|
bool German(Player* p)
|
|
{
|
|
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
|
|
return profile && profile->Locale=="deDE";
|
|
}
|
|
bool HostileEmote(uint32 emote)
|
|
{return emote==TEXT_EMOTE_RUDE || emote==TEXT_EMOTE_THREATEN || emote==TEXT_EMOTE_ROAR;}
|
|
bool FriendlyEmote(uint32 emote)
|
|
{
|
|
switch(emote)
|
|
{
|
|
case TEXT_EMOTE_GREET:case TEXT_EMOTE_HELLO:case TEXT_EMOTE_WAVE:case TEXT_EMOTE_BOW:
|
|
case TEXT_EMOTE_SALUTE:case TEXT_EMOTE_SMILE:case TEXT_EMOTE_THANK:return true;
|
|
default:return false;
|
|
}
|
|
}
|
|
}
|
|
AdventureInteractionMgr* AdventureInteractionMgr::instance(){static AdventureInteractionMgr mgr;return &mgr;}
|
|
bool AdventureInteractionMgr::Visible(Player* p,Player* other,float distance) const
|
|
{
|
|
return other && other!=p && other->IsAlive() && other->IsInWorld() && p->IsInMap(other) &&
|
|
p->IsInPhase(other) && p->CanSeeOrDetect(other) && other->CanSeeOrDetect(p) && p->GetExactDist(other)<=distance && p->IsWithinLOSInMap(other);
|
|
}
|
|
void AdventureInteractionMgr::PartySay(Player* p,char const* german,char const* english)
|
|
{
|
|
auto group=p->GetGroup();if(!group)return;
|
|
WorldPacket packet;
|
|
ChatHandler::BuildChatPacket(packet,CHAT_MSG_PARTY,p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH,p,nullptr,German(p)?german:english);
|
|
group->BroadcastPacket(&packet,false);
|
|
}
|
|
void AdventureInteractionMgr::Leave(Player* p,Record& r,char const* reason)
|
|
{
|
|
PartySay(p,"Ich muss weiter. Gute Reise!","I must be going. Safe travels!");
|
|
TC_LOG_INFO("server.adventurebot","INTERACTION_GROUP_LEAVE guid=%u reason='%s'",p->GetGUID().GetCounter(),reason);
|
|
p->GetGroup()->RemoveMember(p->GetGUID(),GROUP_REMOVEMETHOD_LEAVE);
|
|
sAdventureBehaviorMgr->InterruptForInteraction(p);
|
|
r.Grouped=false;r.Leader.Clear();r.Invite.Clear();r.Partner.Clear();r.SceneUntil=0;
|
|
}
|
|
void AdventureInteractionMgr::Emote(Player* p,Player* source,uint32 emote)
|
|
{
|
|
if(!Visible(p,source,25) || p->IsInCombat() || !p->IsAlive() || p->GetMap()->IsDungeon() ||
|
|
p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed())return;
|
|
auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID().GetCounter()];
|
|
if(r.Partner || now<r.NextSocial || (!FriendlyEmote(emote) && !HostileEmote(emote)))return;
|
|
auto it=r.Encounters.find(source->GetGUID());if(it!=r.Encounters.end() && now<it->second)return;
|
|
r.Partner=source->GetGUID();r.Reply=emote;r.ReplyAt=now+urand(1,3);r.SceneUntil=r.ReplyAt+2;
|
|
r.NextSocial=now+15;r.Encounters[source->GetGUID()]=now+120;
|
|
}
|
|
void AdventureInteractionMgr::Reply(Player* p,Player* other,uint32 emote)
|
|
{
|
|
p->SetStandState(UNIT_STAND_STATE_STAND);p->SetFacingToObject(other);
|
|
// Hostility alone never grants permission to attack. Native attack checks
|
|
// remain in combat; these are visual reactions without combat side effects.
|
|
bool friendly=p->IsFriendlyTo(other) && !HostileEmote(emote);
|
|
p->HandleEmoteCommand(friendly?(emote==TEXT_EMOTE_BOW?EMOTE_ONESHOT_BOW:EMOTE_ONESHOT_WAVE):EMOTE_ONESHOT_NO);
|
|
if(emote==TEXT_EMOTE_GREET && friendly && urand(0,2)==0)
|
|
p->Say(German(p)?"Gute Reise!":"Safe travels!",p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH,other);
|
|
TC_LOG_INFO("server.adventurebot","INTERACTION_EMOTE guid=%u other=%u friendly=%u",p->GetGUID().GetCounter(),other->GetGUID().GetCounter(),friendly);
|
|
}
|
|
void AdventureInteractionMgr::CombatHelp(Player* p,ObjectGuid helper)
|
|
{ _records[p->GetGUID().GetCounter()].Helper=helper; }
|
|
void AdventureInteractionMgr::Offline(Player* p){_records.erase(p->GetGUID().GetCounter());}
|
|
bool AdventureInteractionMgr::Update(Player* p,uint32 diff)
|
|
{
|
|
auto& r=_records[p->GetGUID().GetCounter()];auto now=GameTime::GetGameTime();
|
|
if(r.Timer>diff){r.Timer-=diff;return r.Grouped || r.SceneUntil>now;}
|
|
r.Timer=1000;
|
|
bool unsafe=!p->IsAlive() || p->IsInCombat() || sAdventureCombatMgr->Busy(p) ||
|
|
p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsCharmed();
|
|
if(unsafe){r.Partner.Clear();r.SceneUntil=0;return p->GetGroup()!=nullptr;}
|
|
for(auto it=r.Encounters.begin();it!=r.Encounters.end();)if(now>=it->second)it=r.Encounters.erase(it);else ++it;
|
|
if(r.Helper)
|
|
{
|
|
auto helper=ObjectAccessor::FindPlayer(r.Helper);r.Helper.Clear();
|
|
if(Visible(p,helper,25) && p->IsFriendlyTo(helper))
|
|
p->Say(German(p)?"Danke für die Hilfe!":"Thank you for the help!",p->GetTeam()==ALLIANCE?LANG_COMMON:LANG_ORCISH,helper);
|
|
}
|
|
if(auto invite=p->GetGroupInvite())
|
|
{
|
|
auto leader=invite->GetLeaderGUID();
|
|
if(r.Invite!=leader){r.Invite=leader;r.InviteAt=now+urand(2,4);}
|
|
if(now>=r.InviteAt)
|
|
{
|
|
auto inviter=ObjectAccessor::FindPlayer(leader);
|
|
bool accept=inviter && inviter->IsAlive() && p->IsFriendlyTo(inviter) && !p->GetMap()->IsDungeon() &&
|
|
!inviter->GetSession()->IsClientless() && frand(0,1)<AdventureInteractionPolicy::Willingness(sAdventureBehaviorMgr->InteractionIntent(p),Sociability(p));
|
|
WorldPackets::Party::PartyInviteResponse response{WorldPacket(CMSG_PARTY_INVITE_RESPONSE)};response.Accept=accept;
|
|
p->GetSession()->HandlePartyInviteResponseOpcode(response);r.Invite.Clear();
|
|
TC_LOG_INFO("server.adventurebot","INTERACTION_INVITE guid=%u leader=%u accepted=%u",p->GetGUID().GetCounter(),leader.GetCounter(),accept);
|
|
}
|
|
}
|
|
else r.Invite.Clear();
|
|
if(r.Partner)
|
|
{
|
|
auto other=ObjectAccessor::FindPlayer(r.Partner);
|
|
if(!Visible(p,other,25) || now>=r.SceneUntil){r.Partner.Clear();r.SceneUntil=0;}
|
|
else if(now>=r.ReplyAt)
|
|
{
|
|
sAdventureBehaviorMgr->InterruptForInteraction(p);Reply(p,other,r.Reply);r.ReplyAt=r.SceneUntil+1;
|
|
}
|
|
if(r.Partner)return true;
|
|
}
|
|
if(auto group=p->GetGroup())
|
|
{
|
|
auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());
|
|
if(!r.Grouped)
|
|
{
|
|
r.Grouped=true;r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);
|
|
r.Partner.Clear();r.SceneUntil=0;
|
|
sAdventureBehaviorMgr->InterruptForInteraction(p);
|
|
PartySay(p,"Ich begleite euch eine Weile.","I'll accompany you for a while.");
|
|
}
|
|
if(r.Leader!=group->GetLeaderGUID())
|
|
{
|
|
r.Leader=group->GetLeaderGUID();r.LastActivity=now;r.AfkSince=0;
|
|
if(leader){r.LeaderPosition.Relocate(leader);r.LeaderMap=leader->GetMapId();r.Selection=leader->GetTarget();}
|
|
}
|
|
if(leader)
|
|
{
|
|
r.LastActivity=std::max(r.LastActivity,sClientlessPlayerMgr->LastActivity(leader->GetGUID()));
|
|
if(r.LeaderMap!=leader->GetMapId() || r.LeaderPosition.GetExactDist(leader)>0.1f ||
|
|
r.LeaderPosition.GetOrientation()!=leader->GetOrientation() || r.Selection!=leader->GetTarget() ||
|
|
leader->IsInCombat() || leader->HasUnitState(UNIT_STATE_CASTING))r.LastActivity=now;
|
|
r.LeaderPosition.Relocate(leader);r.LeaderMap=leader->GetMapId();r.Selection=leader->GetTarget();
|
|
if(leader->isAFK()){if(!r.AfkSince)r.AfkSince=now;}else r.AfkSince=0;
|
|
}
|
|
if(AdventureInteractionPolicy::CanLeave(p->IsAlive(),p->IsInCombat(),p->GetMap()->IsDungeon(),p->GetTransport() || p->IsInFlight()))
|
|
{
|
|
if(r.Schedule!=sAdventureBehaviorMgr->InteractionSchedule(p))
|
|
{
|
|
r.Schedule=sAdventureBehaviorMgr->InteractionSchedule(p);
|
|
if(frand(0,1)>=Loyalty(p)){Leave(p,r,"intent-change");return false;}
|
|
}
|
|
if(AdventureInteractionPolicy::LeaderExpired(now,r.AfkSince?r.AfkSince:r.LastActivity)){Leave(p,r,"leader-afk");return false;}
|
|
}
|
|
// Existing warrior executor supplies damage assistance. Roles/healing
|
|
// deliberately remain a later profile-driven extension.
|
|
for(auto member=group->GetFirstMember();member;member=member->next())
|
|
{
|
|
auto ally=member->GetSource();if(!ally || !ally->IsAlive() || !p->IsInMap(ally) || p->GetExactDist(ally)>40)continue;
|
|
auto target=ally->IsInCombat()?ally->GetVictim():nullptr;
|
|
if(!target)for(auto attacker:ally->getAttackers())if(attacker->GetVictim()==ally){target=attacker;break;}
|
|
if(target && sAdventureCombatMgr->Assist(p,target))return true;
|
|
}
|
|
if(!leader || leader==p || !leader->IsAlive())return true;
|
|
auto execution=sClientlessActionExecutorMgr->GetStatus(p->GetGUID().GetCounter());
|
|
if(execution && (execution->State==ClientlessActionExecutionState::Moving || execution->State==ClientlessActionExecutionState::SelectingTarget))return true;
|
|
if(!p->IsInMap(leader) || p->GetExactDist(leader)>=100)
|
|
{
|
|
if(now>=r.NextRoute)
|
|
{
|
|
r.NextRoute=now+15;
|
|
std::string node;float distance=0;
|
|
sClientlessTravelGraphMgr->EnsureLoaded();
|
|
if(sClientlessTravelGraphMgr->FindNearestNodeToPosition(leader->GetMapId(),leader->GetPositionX(),leader->GetPositionY(),leader->GetPositionZ(),node,distance) && distance<1000)
|
|
{
|
|
ClientlessAction action;action.Name="travel_to_node";action.Parameters["node"]=node;
|
|
sClientlessPlayerMgr->QueueAction(p->GetGUID().GetCounter(),std::move(action));
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
if(!p->IsInPhase(leader) || !p->CanSeeOrDetect(leader))return true;
|
|
if(p->GetExactDist(leader)>5 && p->GetExactDist(leader)<100)
|
|
{
|
|
auto nav=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter());
|
|
if(!nav || nav->State!=ClientlessTravelState::Moving || nav->Target.GetExactDist(leader)>5)
|
|
{
|
|
Position end;leader->GetNearPoint(p,end.m_positionX,end.m_positionY,end.m_positionZ,3,leader->GetOrientation()+3.14159265f);
|
|
ClientlessPathDiagnostics path;
|
|
if(sClientlessTravelMgr->AnalyzePath(p,end,path) && path.Complete && !path.Suspicious && path.PathLength<150)
|
|
sClientlessTravelMgr->StartTravel(p,end);
|
|
}
|
|
}
|
|
else if(p->GetExactDist(leader)<=5)sClientlessTravelMgr->CancelTravel(p->GetGUID().GetCounter());
|
|
return true;
|
|
}
|
|
if(r.Grouped){r.Grouped=false;r.Leader.Clear();sAdventureBehaviorMgr->InterruptForInteraction(p);}
|
|
auto intent=sAdventureBehaviorMgr->InteractionIntent(p);
|
|
if(now>=r.NextEncounter && now>=r.NextSocial && !p->GetMap()->IsDungeon() && (intent=="social" || intent=="leisure" || intent=="rest"))
|
|
{
|
|
r.NextEncounter=now+urand(30,60);
|
|
std::vector<Player*> people;p->GetPlayerListInGrid(people,12);
|
|
for(auto other:people)
|
|
{
|
|
if(!Visible(p,other,12) || !sClientlessLuaEngine->GetAdventureProfile(other->GetGUID().GetCounter()) ||
|
|
p->GetGUID().GetCounter()>=other->GetGUID().GetCounter() || other->IsInCombat() || other->GetGroup())continue;
|
|
auto& peer=_records[other->GetGUID().GetCounter()];
|
|
auto peerIntent=sAdventureBehaviorMgr->InteractionIntent(other);
|
|
if((peerIntent!="social" && peerIntent!="leisure" && peerIntent!="rest") || other->HasUnitState(UNIT_STATE_CASTING))continue;
|
|
if(peer.Partner || peer.NextSocial>now || r.Encounters.count(other->GetGUID()) || frand(0,1)>Sociability(p))continue;
|
|
// One initiator reserves both participants; animation replies do
|
|
// not generate incoming text-emote events, preventing loops.
|
|
r.Partner=other->GetGUID();r.Reply=p->IsFriendlyTo(other)?TEXT_EMOTE_GREET:TEXT_EMOTE_THREATEN;
|
|
r.ReplyAt=now+1;r.SceneUntil=now+5;r.Encounters[other->GetGUID()]=now+300;
|
|
peer.Partner=p->GetGUID();peer.Reply=r.Reply;peer.ReplyAt=now+3;peer.SceneUntil=now+5;
|
|
peer.NextSocial=now+60;peer.Encounters[p->GetGUID()]=now+300;
|
|
return true;
|
|
}
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void AdventureInteractionMgr::Report(ChatHandler* handler,Player* p) const
|
|
{
|
|
auto it=_records.find(p->GetGUID().GetCounter());
|
|
if(it==_records.end()){handler->SendSysMessage("No interaction state yet.");return;}
|
|
auto const& r=it->second;auto now=GameTime::GetGameTime();
|
|
handler->PSendSysMessage("Interactions guid=%u grouped=%u leader=%u partner=%u intent='%s' schedule='%s'",
|
|
p->GetGUID().GetCounter(),p->GetGroup()?1:0,r.Leader.GetCounter(),r.Partner.GetCounter(),
|
|
sAdventureBehaviorMgr->InteractionIntent(p).c_str(),r.Schedule.c_str());
|
|
handler->PSendSysMessage(" leaderIdleSeconds=%llu afkSeconds=%llu sociability=%.2f loyalty=%.2f",
|
|
static_cast<unsigned long long>(r.LastActivity && now>=r.LastActivity?now-r.LastActivity:0),
|
|
static_cast<unsigned long long>(r.AfkSince && now>=r.AfkSince?now-r.AfkSince:0),Sociability(p),Loyalty(p));
|
|
}
|