#include "ClientlessPositioning.h" #include "AdventureSocialActions.h" #include "AdventureLocationMgr.h" #include "AdventureEconomyPolicy.h" #include "AdventureExplorationPolicy.h" #include "AdventureProfessionTargets.h" #include "AdventureBehaviorPolicy.h" #include "ClientlessPlayerMgr.h" #include "ClientlessPerceptionMgr.h" #include "ClientlessTravelMgr.h" #include "DBCStores.h" #include "Creature.h" #include "GameObject.h" #include "GameObjectData.h" #include "GameTime.h" #include "Log.h" #include "Map.h" #include "ObjectAccessor.h" #include "Opcodes.h" #include "Player.h" #include "SpellHistory.h" #include "SpellInfo.h" #include "SpellMgr.h" #include "WorldPacket.h" #include "WorldSession.h" #include #include namespace { using namespace AdventureBehaviorPolicy; using namespace AdventureSocialFlow; constexpr uint32 CookingFire = 818; bool InBounds(Player * player, WorldObject const * target, AdventureSocialContext const & c) { return target && target->IsInWorld() && target->GetMapId() == c.MapId && c.Anchor.GetExactDist(target) <= (c.Intent=="economy" || c.Intent=="adventure" || c.Intent=="profession"?200:30) && std::fabs(target->GetPositionZ() - c.Anchor.GetPositionZ()) <= (c.Intent=="economy" || c.Intent=="adventure" || c.Intent=="profession"?25:6) && player->IsInPhase(target); } bool Friendly(Player * player, Unit * unit, AdventureSocialContext const & c) { return unit != player && InBounds(player, unit, c) && unit->IsAlive() && ! unit->IsInCombat() && ! unit->IsInFlight() && ! unit->IsFlying() && ! unit->GetTransport() && player->IsFriendlyTo(unit) && ! unit->IsCritter() && ! unit->IsTotem() && unit->GetCharmerOrOwnerGUID().IsEmpty(); } bool ServiceMatches(Unit* unit, std::string const& kind) { return unit && AdventureEconomyPolicy::Matches(kind,unit->IsVendor(),unit->IsBanker(), unit->HasFlag(UNIT_NPC_FLAGS,UNIT_NPC_FLAG_AUCTIONEER),unit->IsArmorer()); } bool TargetVisible(Player* player, WorldObject* target, std::string const& kind) { if (!AdventureEconomyPolicy::Economic(kind) && !AdventureProfessionPolicy::Recognized(kind)) return player->IsWithinLOSInMap(target); // Counters can obscure the unit's ground/combat sphere while both // participants can see each other's head. Keep native VMAP geometry. return player->GetMap()->isInLineOfSight(player->GetPhaseShift(), player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+player->GetCollisionHeight(), target->GetPositionX(),target->GetPositionY(),target->GetPositionZ()+(AdventureProfessionPolicy::Recognized(kind) && target->ToGameObject()?std::max(1.5f,target->GetCollisionHeight()):target->GetCollisionHeight()), LINEOFSIGHT_ALL_CHECKS,VMAP::ModelIgnoreFlags::Nothing); } std::vector < Unit * > People(Player * player, std::string const & kind, AdventureSocialContext & c) { std::vector < Unit * > people; if (AdventureEconomyPolicy::Economic(kind)) { auto snapshot=sClientlessPerceptionMgr->GetSnapshot(player->GetGUID().GetCounter()); if (!snapshot || snapshot->Radius<100 || snapshot->ScanAgeMs>=1000) { sClientlessPerceptionMgr->ScanNow(player,100); snapshot=sClientlessPerceptionMgr->GetSnapshot(player->GetGUID().GetCounter()); } if (snapshot) for (auto const& entity:snapshot->Entities) { if (entity.Type!=ClientlessEntityType::Creature || !entity.Alive || !(entity.Capabilities & (CLIENTLESS_CAP_VENDOR|CLIENTLESS_CAP_BANKER|CLIENTLESS_CAP_AUCTIONEER|CLIENTLESS_CAP_REPAIR))) continue; auto unit=ObjectAccessor::GetCreature(*player,ObjectGuid(HighGuid::Unit,entity.Entry,entity.Guid)); if (Friendly(player,unit,c) && ServiceMatches(unit,kind) && (c.ServiceKind.empty() || ServiceMatches(unit,c.ServiceKind))) people.push_back(unit); } return people; } if (kind != "bot") { std::vector < Creature * > creatures; player->GetCreatureListWithEntryInGrid(creatures, 0, 25); for (auto unit : creatures) if (Friendly(player, unit, c) && (AdventureEconomyPolicy::Economic(kind) || player->IsWithinLOSInMap(unit)) && (AdventureEconomyPolicy::Economic(kind)?(ServiceMatches(unit,kind) && (c.ServiceKind.empty() || ServiceMatches(unit,c.ServiceKind))): ((kind != "counter" && kind != "order") || unit->IsInnkeeper()))) people.push_back(unit); } if (!AdventureEconomyPolicy::Economic(kind) && kind != "npc" && kind != "counter" && kind != "order") { std::vector < Player * > players; player->GetPlayerListInGrid(players, 25); for (auto unit : players) if (sClientlessPlayerMgr->IsManaged(unit->GetGUID().GetCounter()) && Friendly(player, unit, c) && player->IsWithinLOSInMap(unit) && ! unit->IsSplineEnabled()) people.push_back(unit); } return people; } std::vector < GameObject * > Objects(Player * player, GameobjectTypes type, AdventureSocialContext & c) { std::vector < GameObject * > all, result; player->GetGameObjectListWithEntryInGrid(all, 0, 25); for (auto go : all) if (go->isSpawned() && go->GetGoType() == type && InBounds(player, go, c)) result.push_back(go); return result; } std::vector InspectionObjects(Player* player,AdventureSocialContext& c) { std::vector all,result; player->GetGameObjectListWithEntryInGrid(all,0,60); for (auto go:all) { if (!go->isSpawned() || !InBounds(player,go,c) || !player->CanSeeOrDetect(go) || !player->IsWithinLOSInMap(go)) continue; // Examine visible props/books/signs; never activate them. Ignore // traps, transports, doors, chests and furniture as investigation targets. auto type=go->GetGoType(); if (type==GAMEOBJECT_TYPE_GENERIC || type==GAMEOBJECT_TYPE_GOOBER || type==GAMEOBJECT_TYPE_TEXT) result.push_back(go); } return result; } GameObject * Fire(Player * player, AdventureSocialContext & c) { std::vector < GameObject * > objects; player->GetGameObjectListWithEntryInGrid(objects, 0, 25); GameObject * best = nullptr; for (auto go : objects) { if (! go->isSpawned() || ! InBounds(player, go, c)) continue; auto name = Normalize(go->GetName()); if (go->GetSpellId()!=CookingFire && name.find("campfire") == std::string::npos && name.find("cookingfire") == std::string::npos && name.find("lagerfeuer") == std::string::npos && name.find("kochfeuer") == std::string::npos && name.find("fireplace") == std::string::npos) continue; if (! best || player->GetExactDist(go) < player->GetExactDist(best)) best = go; } return best; } bool CanMakeFire(Player * p) { if (!p->HasSpell(CookingFire) || !p->IsOutdoors() || p->IsInWater() || p->IsFalling() || p->GetSpellHistory()->HasCooldown(CookingFire) || p->IsNonMeleeSpellCast(false)) return false; // Enforce this for every intent and again immediately before casting. // Districts may inherit city flags from a parent or the enclosing zone. for (uint32 id : {p->GetAreaId(), p->GetZoneId()}) { for (uint32 depth = 0; ; ++depth) { auto const* area = sAreaTableStore.LookupEntry(id); if (!area || depth >= 16) return false; auto const flags = area->GetFlags(); if (flags.HasFlag(AreaFlags::LinkedChat) || flags.HasFlag(AreaFlags::AllowTradeChannel)) return false; if (!area->ParentAreaID) break; id = area->ParentAreaID; } } return true; } WorldObject * Resolve(Player * p, ObjectGuid guid) { if (guid.IsGameObject()) return p->GetMap()->GetGameObject(guid); if (guid.IsCreature()) return ObjectAccessor::GetCreature(* p, guid); if (guid.IsPlayer()) return ObjectAccessor::GetPlayer(* p, guid); return nullptr; } template < class T > T * Pick(Player * p, std::vector < T * > const & choices, AdventureSocialContext & c) { std::vector < double > weights; for (auto v : choices) weights.push_back(Novelty(c.TargetHistory, v->GetGUID().ToString()) /(1 + 0.08 * p->GetExactDist(v))); auto index = Choose(weights, c.Random); return index < choices.size() ? choices[index] : nullptr; } bool AtFront(Player* player,WorldObject* target) { return !target->ToUnit() || ConversationFront(target->GetPositionX(),target->GetPositionY(), target->GetOrientation(),player->GetPositionX(),player->GetPositionY()); } bool Approach(Player* player,AdventureSocialScene& scene,WorldObject* target,AdventureSocialContext& c) { if(!InBounds(player,target,c))return false; Position crowded;bool hasCrowded=false,already=false; if(player->GetExactDist(target)<=3.8f && (scene.Kind=="work_skinning" || AtFront(player,target)) && ((AdventureEconomyPolicy::Economic(scene.Kind) || AdventureProfessionPolicy::Recognized(scene.Kind))?TargetVisible(player,target,scene.Kind): player->IsWithinLOS(target->GetPositionX(),target->GetPositionY(),target->GetPositionZ()+1.5f))) { already=true; if(ClientlessPositioning::Clearance(player,*player)>=1.1f) {scene.Approaching=false;return true;} } // People are approached in front of their actual facing, not from the // bot's current bearing. Furniture may still be reached from any side. bool person=target->ToUnit()!=nullptr && scene.Kind!="work_skinning"; float bearing=person?target->GetOrientation():target->GetAngle(player); std::vector offsets=person?std::vector{0,0.28f,-0.28f,0.55f,-0.55f}: std::vector{0,0.7f,-0.7f,1.4f,-1.4f,3.1415926f}; for(float radius:{2.2f,3.2f})for(float offset:offsets) { Position p; p.Relocate(target->GetPositionX()+std::cos(bearing+offset)*radius, target->GetPositionY()+std::sin(bearing+offset)*radius,target->GetPositionZ()); float floor=player->GetMap()->GetHeight(player->GetPhaseShift(),p.GetPositionX(),p.GetPositionY(),target->GetPositionZ()+3,true,8); if(floor<=INVALID_HEIGHT+1||std::fabs(floor-target->GetPositionZ())>3)continue; p.m_positionZ=floor; if(!player->GetMap()->isInLineOfSight(player->GetPhaseShift(),p.GetPositionX(),p.GetPositionY(),floor+((AdventureEconomyPolicy::Economic(scene.Kind) || AdventureProfessionPolicy::Recognized(scene.Kind))?player->GetCollisionHeight():1.5f), target->GetPositionX(),target->GetPositionY(),target->GetPositionZ()+(AdventureProfessionPolicy::Recognized(scene.Kind) && target->ToGameObject()?std::max(1.5f,target->GetCollisionHeight()):AdventureEconomyPolicy::Economic(scene.Kind)?target->GetCollisionHeight():1.5f), LINEOFSIGHT_ALL_CHECKS,VMAP::ModelIgnoreFlags::Nothing))continue; if(ClientlessPositioning::Clearance(player,p)<1.1f) {if(!hasCrowded){crowded=p;hasCrowded=true;}continue;} if(!c.Move(p,(AdventureEconomyPolicy::Economic(scene.Kind) || AdventureExplorationPolicy::Inspection(scene.Kind) || AdventureProfessionPolicy::Recognized(scene.Kind))?350:55))continue; ClientlessPositioning::Reserve(player,p); scene.Destination=p;scene.Approaching=true;return true; } if(already){scene.Approaching=false;return true;} if(hasCrowded && c.Move(crowded,(AdventureEconomyPolicy::Economic(scene.Kind) || AdventureExplorationPolicy::Inspection(scene.Kind) || AdventureProfessionPolicy::Recognized(scene.Kind))?350:55)) {scene.Destination=crowded;scene.Approaching=true;return true;} return false; } bool OnChair(Player* player,GameObject* chair,float& error) { if(!chair||chair->GetGoType()!=GAMEOBJECT_TYPE_CHAIR)return false; auto info=chair->GetGOInfo(); uint32 slots=info->chair.chairslots; if(!slots||slots>5||info->chair.chairheight>2 || player->GetStandState()!=UNIT_STAND_STATE_SIT_LOW_CHAIR+info->chair.chairheight)return false; error=1000000; float side=chair->GetOrientation()+1.5707963f; for(uint32 i=0;isize*(float(i)-float(slots-1)*0.5f); float x=chair->GetPositionX()+std::cos(side)*offset,y=chair->GetPositionY()+std::sin(side)*offset; error=std::min(error,std::hypot(player->GetPositionX()-x,player->GetPositionY()-y)); } return error<=0.15f && std::fabs(player->GetPositionZ()-chair->GetPositionZ())<=0.25f && std::fabs(std::remainder(player->GetOrientation()-chair->GetOrientation(),6.2831853f))<=0.15f; } void Say(Player * player, std::string const & topic, AdventureSocialContext & c) { std::vector < AdventureDialog const * > choices; std::vector < double > weights; auto collect =[&](std::string const & locale) { for (auto const & line : sAdventureLocationMgr->Dialogs()) if (line.Intent == c.Intent && line.Locale == locale && line.Key.rfind(topic + "_", 0) == 0) { choices.push_back(& line); weights.push_back(line.Weight * Novelty(c.VariantHistory, "line:" + line.Key)); } }; collect(c.Locale); if (choices.empty()) collect("enUS"); auto n = Choose(weights, c.Random); if (n < choices.size()) { player->Say(choices[n]->Text, player->GetTeamId() == TEAM_ALLIANCE ? LANG_COMMON : LANG_ORCISH); Remember(c.VariantHistory, "line:" + choices[n]->Key); } } uint32 Reaction(AdventureSocialContext & c) { std::vector < uint32 > emotes { EMOTE_ONESHOT_LAUGH, EMOTE_ONESHOT_QUESTION, EMOTE_ONESHOT_EXCLAMATION, EMOTE_ONESHOT_YES }; std::vector < double > weights; for (auto id : emotes) weights.push_back(Novelty(c.VariantHistory, "gesture:" + std::to_string(id))); auto i = Choose(weights, c.Random); auto id = emotes[i]; Remember(c.VariantHistory, "gesture:" + std::to_string(id)); return id; } std::vector < Step > Steps(std::string const & kind, uint32 duration) { if (AdventureProfessionPolicy::Recognized(kind)) return AdventureProfessionPolicy::Steps(kind,duration); if (AdventureExplorationPolicy::Inspection(kind) || AdventureExplorationPolicy::Untargeted(kind)) return AdventureExplorationPolicy::Steps(kind,duration); if (AdventureEconomyPolicy::Economic(kind)) return AdventureEconomyPolicy::Steps(kind,duration); if (kind == "npc" || kind == "bot") return Conversation(); if (kind == "counter") return { { Cue::Look, 5000 }, { Cue::Listen, 8000 }, { Cue::Look, 6000 } }; if (kind == "order") return { { Cue::Greet, 2000 }, { Cue::Order, 4000 }, { Cue::Listen, 6000 }, { Cue::Drink, 6000 }, { Cue::Farewell, 2000 } }; if (kind == "greet") return { { Cue::Greet, 3000 }, { Cue::Look, 4000 } }; if (kind == "listen") return { { Cue::Listen, 6000 }, { Cue::Nod, 2500 }, { Cue::Listen, 7000 } }; if (kind == "react") return { { Cue::React, 3000 }, { Cue::Listen, 5000 } }; if (kind == "toast") return { { Cue::Toast, 3000 }, { Cue::Drink, 6000 } }; if (kind == "farewell") return { { Cue::Farewell, 4000 } }; if (kind == "comment") return { { Cue::Comment, 3500 }, { Cue::Listen, 5000 } }; if (kind == "seat") return { { Cue::Seat, duration } }; if (kind == "drink_seated") return { { Cue::Seat, 1000 }, { Cue::Drink, 7000 }, { Cue::Listen, duration } }; if (kind == "eat_seated") return { { Cue::Seat, 1000 }, { Cue::Eat, 7000 }, { Cue::Listen, duration } }; if (kind == "dance") return { { Cue::Dance, duration } }; if (kind == "make_fire") return { { Cue::Fire, 1000 }, { Cue::Look, 4000 } }; if (kind == "fire") return { { Cue::Fire, 1000 }, { Cue::Rest, duration / 2 }, { Cue::Drink, 6000 }, { Cue::Rest, duration / 2 } }; return { }; } } void AdventureSocialActions::Prepare(Player * player) { // Shared AdventureBot equipment, independent of profession/profile weights. if (! player->HasSpell(CookingFire) && sSpellMgr->GetSpellInfo(CookingFire)) player->LearnSpell(CookingFire, false); } bool AdventureSocialActions::Available(Player * p, AdventureMicroAction const & action, AdventureSocialContext & c) { auto const & kind = action.Requirement; if (AdventureProfessionPolicy::Recognized(kind)) return c.Intent=="profession" && AdventureProfessionPolicy::Allowed(kind,c.PrimaryProfessions) && (AdventureProfessionPolicy::Untargeted(kind)?!p->IsInWater() && !p->IsFalling():!AdventureProfessionTargets::Find(p,kind,c).empty()); if (AdventureExplorationPolicy::Inspection(kind)) return c.Intent=="adventure" && !InspectionObjects(p,c).empty(); if (AdventureExplorationPolicy::Untargeted(kind)) return c.Intent=="adventure" && (kind!="vista" || p->IsOutdoors()) && (kind!="entrance" || AdventureExplorationPolicy::Entrance(c.LocationCategory)); if (kind=="supplies") return c.Intent=="economy"; if (kind=="vendor_compare") return People(p,kind,c).size()>=2; if (kind=="service_finish") { for (auto unit:People(p,kind,c)) if (std::find(c.TargetHistory.begin(),c.TargetHistory.end(),unit->GetGUID().ToString())!=c.TargetHistory.end()) return true; return false; } if (kind == "seat" || kind == "drink_seated" || kind == "eat_seated") return ! Objects(p, GAMEOBJECT_TYPE_CHAIR, c).empty(); if (kind == "dance") return c.Festival || (c.Intent!="leisure" && ! People(p, "audience", c).empty()); if (kind == "fire") return Fire(p, c) || (c.AllowCreateFire && CanMakeFire(p)); if (kind == "make_fire") return c.AllowCreateFire && ! Fire(p, c) && CanMakeFire(p); if (kind == "farewell") { for (auto unit : People(p, kind, c)) if (std::find(c.TargetHistory.begin(), c.TargetHistory.end(), unit->GetGUID().ToString()) != c.TargetHistory.end()) return true; return false; } return ! People(p, kind, c).empty(); } bool AdventureSocialActions::Start(Player * p, AdventureSocialScene & s, AdventureMicroAction const & a, uint32 duration, AdventureSocialContext & c) { s = { }; s.Action = a.Key; s.Kind = a.Requirement; if(AdventureProfessionPolicy::Recognized(s.Kind) && !Available(p,a,c))return false; s.Origin.Relocate(p); s.PreviousSelection = p->GetTarget(); s.Facing=p->GetOrientation(); WorldObject * target = nullptr; bool approached=false; if (s.Kind == "seat" || s.Kind == "drink_seated" || s.Kind == "eat_seated") { auto chairs=Objects(p,GAMEOBJECT_TYPE_CHAIR,c); while(!chairs.empty()) { auto chair=Pick(p,chairs,c); if(!chair)break; if(!chair->GetGOInfo()->chair.triggeredEvent&&Approach(p,s,chair,c)) {target=chair;approached=true;break;} chairs.erase(std::remove(chairs.begin(),chairs.end(),chair),chairs.end()); } } else if (AdventureProfessionPolicy::Recognized(s.Kind) && !AdventureProfessionPolicy::Untargeted(s.Kind)) { auto targets=AdventureProfessionTargets::Find(p,s.Kind,c); while(!targets.empty()) { auto candidate=Pick(p,targets,c); if(!candidate)break; if(Approach(p,s,candidate,c)){target=candidate;approached=true;break;} targets.erase(std::remove(targets.begin(),targets.end(),candidate),targets.end()); } if(!target)return false; } else if (AdventureExplorationPolicy::Inspection(s.Kind)) { auto objects=InspectionObjects(p,c); while (!objects.empty()) { auto object=Pick(p,objects,c); if (!object) break; if (Approach(p,s,object,c)) {target=object;approached=true;break;} objects.erase(std::remove(objects.begin(),objects.end(),object),objects.end()); } if (!target) return false; } else if (s.Kind == "fire" || s.Kind == "make_fire") target = Fire(p, c); else if (s.Kind == "farewell") { auto people = People(p, s.Kind, c); for (auto i = c.TargetHistory.rbegin(); i != c.TargetHistory.rend() && ! target; ++ i) for (auto unit : people) if (unit->GetGUID().ToString() == * i) { target = unit; break; } } else if (AdventureEconomyPolicy::RequiredService(s.Kind)!=AdventureEconomyPolicy::Service::None) { auto people=People(p,s.Kind,c); if (s.Kind=="vendor_compare" && people.size()<2) return false; if (s.Kind=="service_finish") people.erase(std::remove_if(people.begin(),people.end(),[&](Unit* unit) {return std::find(c.TargetHistory.begin(),c.TargetHistory.end(),unit->GetGUID().ToString())==c.TargetHistory.end();}),people.end()); // A nearer counter may be blocked. Try other actual service NPCs before // declaring this visit unavailable; every approach still uses MMAP. while (!people.empty()) { auto unit=Pick(p,people,c); if (!unit) break; if (Approach(p,s,unit,c)) {target=unit;approached=true;break;} people.erase(std::remove(people.begin(),people.end(),unit),people.end()); } if (!target) return false; } else if (s.Kind != "dance" && s.Kind!="supplies" && !AdventureExplorationPolicy::Untargeted(s.Kind) && !AdventureProfessionPolicy::Untargeted(s.Kind)) target = Pick(p, People(p, s.Kind, c), c); if (target) { s.Target = target->GetGUID(); s.TargetKey = s.Target.ToString(); if (! approached && ! Approach(p, s, target, c)) return false; Remember(c.TargetHistory, s.TargetKey); } else if (s.Kind == "fire" || s.Kind == "make_fire") { if (! c.AllowCreateFire || ! CanMakeFire(p)) return false; // Native cast places the campfire beside the grounded caster. Require a // small flat open patch, not water, a ledge, doorway or a room interior. for (unsigned i = 0; i < 8; ++ i) { float angle = float(i) * 6.2831853f / 8; float x = p->GetPositionX() + std::cos(angle) * 2, y = p->GetPositionY() + std::sin(angle) * 2; float z = p->GetMap()->GetHeight(p->GetPhaseShift(), x, y, p->GetPositionZ() + 2, true, 6); if (z <= INVALID_HEIGHT + 1 || std::fabs(z - p->GetPositionZ()) > 1.0f || p->GetMap()->GetWaterLevel(p->GetPhaseShift(), x, y) > z + 0.2f || ! p->IsWithinLOS(x, y, z + 1)) return false; } } else if (s.Kind != "dance" && s.Kind!="supplies" && !AdventureExplorationPolicy::Untargeted(s.Kind) && !AdventureProfessionPolicy::Untargeted(s.Kind)) return false; s.ServiceDuration=s.Kind=="vendor_compare"?duration/2:duration; s.Sequence.Start(Steps(s.Kind, s.ServiceDuration)); s.Active = s.Sequence.Active(); TC_LOG_INFO("server.adventurebot", "SOCIAL_START guid=%u action='%s' target='%s' approach=%u", p->GetGUID().GetCounter(), s.Action.c_str(), s.Target.ToString().c_str(), s.Approaching ? 1 : 0); return s.Active; } bool AdventureSocialActions::Update(Player * p, AdventureSocialScene & s, uint32 diff, AdventureSocialContext & c) { if (! s.Active) return false; if(AdventureProfessionPolicy::Recognized(s.Kind) && (c.Intent!="profession" || !AdventureProfessionPolicy::Allowed(s.Kind,c.PrimaryProfessions)))return false; s.Elapsed += diff; if (s.Elapsed > (AdventureEconomyPolicy::Economic(s.Kind)?240000u:120000u)) return false; if (s.Departing) { auto travel=sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter()); if (travel && travel->State==ClientlessTravelState::Moving) return true; if (travel && travel->State==ClientlessTravelState::Arrived && p->GetExactDist(s.Destination)<=2) TC_LOG_INFO("server.adventurebot",AdventureProfessionPolicy::Recognized(s.Kind)?"PROFESSION_VISIT_COMPLETE guid=%u action='%s' visits=%u":"ECONOMY_VISIT_COMPLETE guid=%u action='%s' visits=%u", p->GetGUID().GetCounter(),s.Action.c_str(),s.ServiceVisits+1); return false; } auto target = Resolve(p, s.Target); if (! s.Target.IsEmpty()) { if (! InBounds(p, target, c)) return false; if(AdventureProfessionPolicy::Recognized(s.Kind) && !AdventureProfessionTargets::Valid(p,target,s.Kind,c))return false; if (AdventureProfessionPolicy::Recognized(s.Kind) && target->ToGameObject() && !s.Approaching && (p->GetExactDist(target)>4 || !TargetVisible(p,target,s.Kind))) { if (++s.Repaths>3 || !Approach(p,s,target,c)) return false; // Do not advance a work cue while returning to the workplace. return true; } if (AdventureExplorationPolicy::Inspection(s.Kind)) { auto object=target->ToGameObject(); if (!object || !object->isSpawned() || !p->CanSeeOrDetect(object)) return false; if (!s.Approaching && (p->GetExactDist(target)>4 || !TargetVisible(p,target,s.Kind))) { if (++s.Repaths>3 || !Approach(p,s,target,c)) return false; } } if (auto unit = target->ToUnit()) { if (s.Kind!="work_skinning" && ! Friendly(p, unit, c)) return false; if (AdventureEconomyPolicy::Economic(s.Kind) && !ServiceMatches(unit,s.Kind)) return false; if ((p->GetExactDist(unit) > 5 || (s.Kind!="work_skinning" && !AtFront(p,unit)) || !TargetVisible(p,unit,s.Kind)) && ! s.Approaching) { if (++ s.Repaths > 3 || ! Approach(p, s, unit, c)) return false; } } } if(target&&target->ToUnit()&&!s.Approaching&&!TargetVisible(p,target,s.Kind))return false; if (s.Approaching) { auto travel = sClientlessTravelMgr->GetStatus(p->GetGUID().GetCounter()); if (travel && travel->State == ClientlessTravelState::Moving) return true; if (! travel || travel->State != ClientlessTravelState::Arrived || ! target) return false; s.Approaching = false; if (p->GetExactDist(target) > 4 || (s.Kind!="work_skinning" && !AtFront(p,target)) || !TargetVisible(p,target,s.Kind)) { if (++ s.Repaths > 3 || ! Approach(p, s, target, c)) return false; return true; } } if (s.WaitingChair) { if (p->IsBeingTeleported()) return true; float error=0; if(!target||!OnChair(p,target->ToGameObject(),error))return false; s.WaitingChair=false;s.Seated=true; TC_LOG_INFO("server.adventurebot","SOCIAL_SEATED guid=%u chair='%s' position=(%.3f %.3f %.3f) slotError=%.3f stand=%u movementGuid=%llu", p->GetGUID().GetCounter(),s.Target.ToString().c_str(),p->GetPositionX(),p->GetPositionY(),p->GetPositionZ(),error, uint32(p->GetStandState()),static_cast(p->m_movementInfo.guid.GetCounter())); } if(s.Seated&&(s.Kind=="seat"||s.Kind=="drink_seated"||s.Kind=="eat_seated")) { float error=0; if(!target||!OnChair(p,target->ToGameObject(),error))return false; } if (s.Casting) { s.CastElapsed += diff; if (p->IsNonMeleeSpellCast(false)) return s.CastElapsed < 10000; if (auto fire = Fire(p, c)) { s.Casting = false; s.Target = fire->GetGUID(); s.TargetKey = s.Target.ToString(); TC_LOG_INFO("server.adventurebot", "SOCIAL_FIRE_CONFIRMED guid=%u spell=818 object='%s'", p->GetGUID().GetCounter(), s.TargetKey.c_str()); if (! Approach(p, s, fire, c)) return false; return true; } return s.CastElapsed < 10000; } if (target && ! s.Seated) { p->SetFacingToObject(target); if (target->ToUnit()) p->SetSelection(target->GetGUID()); } if (auto step = s.Sequence.Take()) { if(target&&target->ToUnit()) TC_LOG_INFO("server.adventurebot","SOCIAL_FRONT guid=%u target='%s' offset=%.3f distance=%.2f", p->GetGUID().GetCounter(),target->GetGUID().ToString().c_str(), std::remainder(target->GetAngle(p)-target->GetOrientation(),6.2831853f),p->GetExactDist(target)); p->SetUInt32Value(UNIT_NPC_EMOTESTATE, 0); switch (step->Action) { case Cue::Greet : p->HandleEmoteCommand(EMOTE_ONESHOT_WAVE); Say(p, "greet", c); break; case Cue::Talk : p->HandleEmoteCommand(EMOTE_ONESHOT_TALK); Say(p, "talk", c); break; case Cue::Order : p->HandleEmoteCommand(EMOTE_ONESHOT_TALK); Say(p, "order", c); break; case Cue::Comment : p->HandleEmoteCommand(EMOTE_ONESHOT_TALK); Say(p, "comment", c); break; case Cue::Work : { auto task=AdventureProfessionPolicy::Find(s.Kind); if(!task || !AdventureProfessionPolicy::Allowed(s.Kind,c.PrimaryProfessions))return false; uint32 emote=EMOTE_STATE_USE_STANDING; switch(task->Animation) { case AdventureProfessionPolicy::Motion::Mine:emote=EMOTE_STATE_WORK_MINING;break; case AdventureProfessionPolicy::Motion::Chop:emote=EMOTE_STATE_WORK_CHOPWOOD;break; case AdventureProfessionPolicy::Motion::Kneel:emote=EMOTE_STATE_KNEEL;break; default:break; } p->SetUInt32Value(UNIT_NPC_EMOTESTATE,emote); TC_LOG_INFO("server.adventurebot","PROFESSION_WORK guid=%u kind='%s' target='%s' emote=%u",p->GetGUID().GetCounter(),s.Kind.c_str(),s.TargetKey.c_str(),emote); break; } case Cue::SurveyLeft : p->SetFacingTo(s.Facing-0.75f); break; case Cue::SurveyRight : p->SetFacingTo(s.Facing+0.75f); break; case Cue::Question : p->HandleEmoteCommand(EMOTE_ONESHOT_QUESTION); break; case Cue::Point : p->HandleEmoteCommand(EMOTE_ONESHOT_POINT); break; case Cue::CheckGear : p->HandleEmoteCommand(EMOTE_ONESHOT_USE_STANDING); break; case Cue::Listen : break; case Cue::React : p->HandleEmoteCommand(Reaction(c)); break; case Cue::Nod : p->HandleEmoteCommand(EMOTE_ONESHOT_YES); break; case Cue::Farewell : p->HandleEmoteCommand(EMOTE_ONESHOT_WAVE); Say(p, "farewell", c); break; case Cue::Look : p->SetFacingTo(p->GetOrientation() + 0.3f); break; case Cue::Toast : p->HandleEmoteCommand(EMOTE_ONESHOT_CHEER); Say(p, "toast", c); break; case Cue::Drink : p->SendPlaySpellVisualKit(SPELL_VISUAL_KIT_DRINK, 0, 0); break; case Cue::Eat : p->SendPlaySpellVisualKit(SPELL_VISUAL_KIT_FOOD, 0, 0); break; case Cue::Dance : p->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_STATE_DANCE); break; case Cue::Rest : p->SetStandState(UNIT_STAND_STATE_SIT); s.Seated = true; break; case Cue::Fire : if (! Fire(p, c)) { if (! c.AllowCreateFire || ! CanMakeFire(p) || p->CastSpell(p, CookingFire, false) != SPELL_CAST_OK) return false; s.Casting = true; s.CastElapsed = 0; TC_LOG_INFO("server.adventurebot", "SOCIAL_FIRE_CAST guid=%u spell=818", p->GetGUID().GetCounter()); return true; } break; case Cue::Seat : { auto chair = target ? target->ToGameObject() : nullptr; if (! chair || chair->GetGoType() != GAMEOBJECT_TYPE_CHAIR || ! p->GetGameObjectIfCanInteractWith(chair->GetGUID()) || !p->IsWithinLOS(chair->GetPositionX(),chair->GetPositionY(),chair->GetPositionZ()+1.5f)) return false; auto info = chair->GetGOInfo(); if (info->chair.triggeredEvent) return false; // furniture with scripted events is not an RP prop WorldPacket packet(CMSG_GAMEOBJ_USE, 8); packet << chair->GetGUID(); p->GetSession()->HandleGameObjectUseOpcode(packet); s.WaitingChair = true; return true; } } TC_LOG_INFO("server.adventurebot", "SOCIAL_CUE guid=%u action='%s' cue=%u target='%s'", p->GetGUID().GetCounter(), s.Action.c_str(), uint32(step->Action), s.Target.ToString().c_str()); return true; } s.Sequence.Update(diff); if (s.Sequence.Active()) return true; if(AdventureProfessionPolicy::Recognized(s.Kind)) { if(auto task=AdventureProfessionPolicy::Find(s.Kind)) if(task->Gather && s.Kind!="ore_inspect")Remember(c.TargetHistory,"work:"+s.TargetKey); TC_LOG_INFO("server.adventurebot","PROFESSION_WORK_COMPLETE guid=%u kind='%s' target='%s'",p->GetGUID().GetCounter(),s.Kind.c_str(),s.TargetKey.c_str()); if(!target)return false; Position departure;departure.Relocate(p); float angle=target->GetAngle(p); departure.m_positionX+=std::cos(angle)*2;departure.m_positionY+=std::sin(angle)*2; float height=p->GetMap()->GetHeight(p->GetPhaseShift(),departure.GetPositionX(),departure.GetPositionY(),p->GetPositionZ()+3,true,8); if(height>INVALID_HEIGHT+1 && std::fabs(height-p->GetPositionZ())<=2) { departure.m_positionZ=height; if(c.Move(departure,20)){s.Destination=departure;s.Departing=true;return true;} } return false; } if (s.Kind=="vendor_compare" && !s.ServiceVisits) { auto people=People(p,s.Kind,c); people.erase(std::remove_if(people.begin(),people.end(),[&](Unit* unit) {return unit->GetGUID()==s.Target;}),people.end()); while (!people.empty()) { auto next=Pick(p,people,c); if (!next) break; if (Approach(p,s,next,c)) { s.Target=next->GetGUID();s.TargetKey=s.Target.ToString();++s.ServiceVisits; Remember(c.TargetHistory,s.TargetKey);s.Sequence.Start(Steps(s.Kind,s.ServiceDuration)); TC_LOG_INFO("server.adventurebot","ECONOMY_COMPARE_NEXT guid=%u target='%s'",p->GetGUID().GetCounter(),s.TargetKey.c_str()); return true; } people.erase(std::remove(people.begin(),people.end(),next),people.end()); } return false; } if (AdventureEconomyPolicy::RequiredService(s.Kind)!=AdventureEconomyPolicy::Service::None) { if (s.Kind=="repair" && !s.RepairDone) { // The visit is performative. No vendor packet, transaction or money // operation; the completed, validated scene restores durability. p->DurabilityRepairAll(false,1.0f,false); s.RepairDone=true; TC_LOG_INFO("server.adventurebot","ECONOMY_REPAIR_COMPLETE guid=%u target='%s'",p->GetGUID().GetCounter(),s.TargetKey.c_str()); } s.Destination=c.Anchor; if (!c.Move(s.Destination,350)) return false; s.Departing=true; TC_LOG_INFO("server.adventurebot","ECONOMY_DEPART guid=%u action='%s'",p->GetGUID().GetCounter(),s.Action.c_str()); return true; } return false; } void AdventureSocialActions::Reset(Player * p, AdventureSocialScene & s) { if (s.Active) { if (s.Casting) p->InterruptNonMeleeSpells(false, CookingFire); if (p->GetTarget() == s.Target) p->SetSelection(s.PreviousSelection); TC_LOG_INFO("server.adventurebot", "SOCIAL_END guid=%u action='%s' elapsedMs=%u", p->GetGUID().GetCounter(), s.Action.c_str(), s.Elapsed); } s = { }; }