#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 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 || nowGetGUID());if(it!=r.Encounters.end() && nowsecond)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)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 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(r.LastActivity && now>=r.LastActivity?now-r.LastActivity:0), static_cast(r.AfkSince && now>=r.AfkSince?now-r.AfkSince:0),Sociability(p),Loyalty(p)); }