813 lines
37 KiB
C++
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 =
|
|
{
|
|
};
|
|
}
|