Files
Viano-Core/src/server/game/World/AdventureSocialActions.cpp
T
Nico 49712aadfe
GCC PCH / build (push) Waiting to run
clang PCH / build (push) Canceled after 0s
Improve AdventureBot travel recovery and destination eligibility
2026-10-29 11:10:27 +01:00

813 lines
37 KiB
C++

#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 <algorithm>
#include <cmath>
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<GameObject*> InspectionObjects(Player* player,AdventureSocialContext& c)
{
std::vector<GameObject*> 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<float> offsets=person?std::vector<float>{0,0.28f,-0.28f,0.55f,-0.55f}:
std::vector<float>{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;i<slots;++i)
{
float offset=info->size*(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<unsigned long long>(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 =
{
};
}