Auto do quest feature (new rpg strategy) (#1034)

* New rpg startup speed up and refactor

* New rpg do quest

* Fix invalid height in quest poi

* Add quest accept and reward limitation

* New rpg quest improvement

* Organize quest log, reward quests and fix grind target

* Quest dropped statistic and remove redundant code

* Decrease grind relevance lower than loot

* Fix new rpg drop quest

* Go to reward quest instead of innkeeper when quest completed

* Fix incorrect logic in do quest reward

* Fix reset quests in factory

* Fix crash on grind target value

Co-authored-by: SaW <swerkhoven@outlook.com>

* Fix a minor error in DoCompletedQuest

* Let bots get rid of impossible quests faster

* Increase loot fluency (especially for caster)

* Remove seasonal quests from auto accept

* Enhance quest accept condition check

* Add questgiver check (limit acceptation of quest 7946)

* Questgiver check and localization

* Near npc fix

* Fix quest item report

* Add lowPriorityQuest set for quests can not be done

* Improve gameobjects loot

* Do complete quest

* FIx move far to teleport check

* Accept or reward quest from game objects

* Fix possible crash in rpg game objects

* Fix ChooseNpcOrGameObjectToInteract crash

---------

Co-authored-by: SaW <swerkhoven@outlook.com>
This commit is contained in:
Yunfan Li
2025-03-14 21:31:33 +08:00
committed by GitHub
parent 88356bb507
commit 38912d4a8a
42 changed files with 2051 additions and 617 deletions

View File

@@ -2,18 +2,32 @@
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include "ChatHelper.h"
#include "G3D/Vector2.h"
#include "GossipDef.h"
#include "IVMapMgr.h"
#include "NewRpgInfo.h"
#include "NewRpgStrategy.h"
#include "Object.h"
#include "ObjectAccessor.h"
#include "ObjectDefines.h"
#include "ObjectGuid.h"
#include "ObjectMgr.h"
#include "PathGenerator.h"
#include "Player.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "Position.h"
#include "QuestDef.h"
#include "Random.h"
#include "RandomPlayerbotMgr.h"
#include "SharedDefines.h"
#include "StatsWeightCalculator.h"
#include "Timer.h"
#include "TravelMgr.h"
#include "BroadcastHelper.h"
#include "World.h"
bool TellRpgStatusAction::Execute(Event event)
@@ -26,118 +40,156 @@ bool TellRpgStatusAction::Execute(Event event)
return true;
}
bool StartRpgDoQuestAction::Execute(Event event)
{
Player* owner = event.getOwner();
if (!owner)
return false;
std::string const text = event.getParam();
PlayerbotChatHandler ch(owner);
uint32 questId = ch.extractQuestId(text);
const Quest* quest = sObjectMgr->GetQuestTemplate(questId);
if (quest)
{
botAI->rpgInfo.ChangeToDoQuest(questId, quest);
bot->Whisper("Start to do quest " + std::to_string(questId), LANG_UNIVERSAL, owner);
return true;
}
bot->Whisper("Invalid quest " + text, LANG_UNIVERSAL, owner);
return false;
}
bool NewRpgStatusUpdateAction::Execute(Event event)
{
NewRpgInfo& info = botAI->rpgInfo;
/// @TODO: Refactor by transition probability
switch (info.status)
{
case NewRpgStatus::IDLE:
case RPG_IDLE:
{
uint32 roll = urand(1, 100);
// IDLE -> NEAR_NPC
// if ((!info.lastNearNpc || info.lastNearNpc + setNpcInterval < getMSTime()) && roll <= 30)
if (roll <= 30)
{
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible rpg targets");
if (!possibleTargets.empty())
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible new rpg targets");
if (possibleTargets.size() >= 3)
{
info.Reset();
info.lastNearNpc = getMSTime();
info.status = NewRpgStatus::NEAR_NPC;
info.ChangeToNearNpc();
return true;
}
}
// IDLE -> GO_INNKEEPER
else if (roll <= 45)
{
WorldPosition pos = SelectRandomInnKeeperPos();
WorldPosition pos = SelectRandomInnKeeperPos(bot);
if (pos != WorldPosition() && bot->GetExactDist(pos) > 50.0f)
{
info.Reset();
info.lastGoInnKeeper = getMSTime();
info.status = NewRpgStatus::GO_INNKEEPER;
info.innKeeperPos = pos;
info.ChangeToGoInnkeeper(pos);
return true;
}
}
// IDLE -> GO_GRIND
else if (roll <= 90)
else if (roll <= 100)
{
WorldPosition pos = SelectRandomGrindPos();
if (roll >= 60)
{
std::vector<uint32> availableQuests;
for (uint8 slot = 0; slot < MAX_QUEST_LOG_SIZE; ++slot)
{
uint32 questId = bot->GetQuestSlotQuestId(slot);
if (botAI->lowPriorityQuest.find(questId) != botAI->lowPriorityQuest.end())
continue;
std::vector<POIInfo> poiInfo;
if (GetQuestPOIPosAndObjectiveIdx(questId, poiInfo, true))
{
availableQuests.push_back(questId);
}
}
if (availableQuests.size())
{
uint32 questId = availableQuests[urand(0, availableQuests.size() - 1)];
const Quest* quest = sObjectMgr->GetQuestTemplate(questId);
if (quest)
{
// IDLE -> DO_QUEST
info.ChangeToDoQuest(questId, quest);
return true;
}
}
}
WorldPosition pos = SelectRandomGrindPos(bot);
if (pos != WorldPosition())
{
info.Reset();
info.lastGoGrind = getMSTime();
info.status = NewRpgStatus::GO_GRIND;
info.grindPos = pos;
info.ChangeToGoGrind(pos);
return true;
}
}
// IDLE -> REST
info.Reset();
info.status = NewRpgStatus::REST;
info.lastRest = getMSTime();
info.ChangeToRest();
bot->SetStandState(UNIT_STAND_STATE_SIT);
return true;
}
case NewRpgStatus::GO_GRIND:
case RPG_GO_GRIND:
{
WorldPosition& originalPos = info.grindPos;
assert(info.grindPos != WorldPosition());
WorldPosition& originalPos = info.go_grind.pos;
assert(info.go_grind.pos != WorldPosition());
// GO_GRIND -> NEAR_RANDOM
if (bot->GetExactDist(originalPos) < 10.0f)
{
info.Reset();
info.status = NewRpgStatus::NEAR_RANDOM;
info.lastNearRandom = getMSTime();
info.grindPos = WorldPosition();
info.ChangeToNearRandom();
return true;
}
break;
}
case NewRpgStatus::GO_INNKEEPER:
case RPG_GO_INNKEEPER:
{
WorldPosition& originalPos = info.innKeeperPos;
assert(info.innKeeperPos != WorldPosition());
WorldPosition& originalPos = info.go_innkeeper.pos;
assert(info.go_innkeeper.pos != WorldPosition());
// GO_INNKEEPER -> NEAR_NPC
if (bot->GetExactDist(originalPos) < 10.0f)
{
info.Reset();
info.lastNearNpc = getMSTime();
info.status = NewRpgStatus::NEAR_NPC;
info.innKeeperPos = WorldPosition();
info.ChangeToNearNpc();
return true;
}
break;
}
case NewRpgStatus::NEAR_RANDOM:
case RPG_NEAR_RANDOM:
{
// NEAR_RANDOM -> IDLE
if (info.lastNearRandom + statusNearRandomDuration < getMSTime())
if (info.HasStatusPersisted(statusNearRandomDuration))
{
info.Reset();
info.status = NewRpgStatus::IDLE;
info.ChangeToIdle();
return true;
}
break;
}
case NewRpgStatus::NEAR_NPC:
case RPG_DO_QUEST:
{
if (info.lastNearNpc + statusNearNpcDuration < getMSTime())
// DO_QUEST -> IDLE
if (info.HasStatusPersisted(statusDoQuestDuration))
{
info.Reset();
info.status = NewRpgStatus::IDLE;
info.ChangeToIdle();
return true;
}
break;
}
case NewRpgStatus::REST:
case RPG_NEAR_NPC:
{
if (info.HasStatusPersisted(statusNearNpcDuration))
{
info.ChangeToIdle();
return true;
}
break;
}
case RPG_REST:
{
// REST -> IDLE
if (info.lastRest + statusRestDuration < getMSTime())
if (info.HasStatusPersisted(statusRestDuration))
{
info.Reset();
info.status = NewRpgStatus::IDLE;
info.ChangeToIdle();
return true;
}
break;
@@ -148,258 +200,259 @@ bool NewRpgStatusUpdateAction::Execute(Event event)
return false;
}
WorldPosition NewRpgStatusUpdateAction::SelectRandomGrindPos()
bool NewRpgGoGrindAction::Execute(Event event)
{
const std::vector<WorldLocation>& locs = sRandomPlayerbotMgr->locsPerLevelCache[bot->GetLevel()];
std::vector<WorldLocation> lo_prepared_locs, hi_prepared_locs;
for (auto& loc : locs)
{
if (bot->GetMapId() != loc.GetMapId())
continue;
if (SearchQuestGiverAndAcceptOrReward())
return true;
if (bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()) !=
bot->GetZoneId())
continue;
if (bot->GetExactDist(loc) < 500.0f)
{
hi_prepared_locs.push_back(loc);
}
if (bot->GetExactDist(loc) < 2500.0f)
{
lo_prepared_locs.push_back(loc);
}
}
WorldPosition dest{};
if (urand(1, 100) <= 50 && !hi_prepared_locs.empty())
{
uint32 idx = urand(0, hi_prepared_locs.size() - 1);
dest = hi_prepared_locs[idx];
}
else if (!lo_prepared_locs.empty())
{
uint32 idx = urand(0, lo_prepared_locs.size() - 1);
dest = lo_prepared_locs[idx];
}
LOG_DEBUG("playerbots", "[New Rpg] Bot {} select random grind pos Map:{} X:{} Y:{} Z:{} ({}+{} available in {})",
bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
hi_prepared_locs.size(), lo_prepared_locs.size() - hi_prepared_locs.size(), locs.size());
return dest;
return MoveFarTo(botAI->rpgInfo.go_grind.pos);
}
WorldPosition NewRpgStatusUpdateAction::SelectRandomInnKeeperPos()
bool NewRpgGoInnKeeperAction::Execute(Event event)
{
const std::vector<WorldLocation>& locs = IsAlliance(bot->getRace())
? sRandomPlayerbotMgr->allianceStarterPerLevelCache[bot->GetLevel()]
: sRandomPlayerbotMgr->hordeStarterPerLevelCache[bot->GetLevel()];
std::vector<WorldLocation> prepared_locs;
for (auto& loc : locs)
{
if (bot->GetMapId() != loc.GetMapId())
continue;
if (bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()) !=
bot->GetZoneId())
continue;
float range = bot->GetLevel() <= 5 ? 500.0f : 2500.0f;
if (bot->GetExactDist(loc) < range)
{
prepared_locs.push_back(loc);
}
}
WorldPosition dest{};
if (!prepared_locs.empty())
{
uint32 idx = urand(0, prepared_locs.size() - 1);
dest = prepared_locs[idx];
}
LOG_DEBUG("playerbots", "[New Rpg] Bot {} select random inn keeper pos Map:{} X:{} Y:{} Z:{} ({} available in {})",
bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
prepared_locs.size(), locs.size());
return dest;
if (SearchQuestGiverAndAcceptOrReward())
return true;
return MoveFarTo(botAI->rpgInfo.go_innkeeper.pos);
}
bool NewRpgGoFarAwayPosAction::MoveFarTo(WorldPosition dest)
{
if (dest == WorldPosition())
return false;
float dis = bot->GetExactDist(dest);
if (dis < pathFinderDis)
{
return MoveTo(dest.getMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), false, false,
false, true);
}
// performance optimization
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
{
return false;
}
// stuck check
float disToDest = bot->GetDistance(dest);
if (disToDest + 1.0f < botAI->rpgInfo.nearestMoveFarDis)
{
botAI->rpgInfo.nearestMoveFarDis = disToDest;
botAI->rpgInfo.stuckTs = getMSTime();
botAI->rpgInfo.stuckAttempts = 0;
}
else if (++botAI->rpgInfo.stuckAttempts >= 10 && botAI->rpgInfo.stuckTs + stuckTime < getMSTime())
{
// Unfortunately we've been stuck here for over 5 mins, fallback to teleporting directly to the destination
botAI->rpgInfo.stuckTs = getMSTime();
botAI->rpgInfo.stuckAttempts = 0;
const AreaTableEntry* entry = sAreaTableStore.LookupEntry(bot->GetZoneId());
std::string zone_name = PlayerbotAI::GetLocalizedAreaName(entry);
LOG_DEBUG("playerbots", "[New Rpg] Teleport {} from ({},{},{},{}) to ({},{},{},{}) as it stuck when moving far - Zone: {} ({})", bot->GetName(),
bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId(),
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.getMapId(), bot->GetZoneId(), zone_name);
return bot->TeleportTo(dest);
}
float minDelta = M_PI;
const float x = bot->GetPositionX();
const float y = bot->GetPositionY();
const float z = bot->GetPositionZ();
float rx, ry, rz;
bool found = false;
int attempt = 3;
while (--attempt)
{
float angle = bot->GetAngle(&dest);
float delta = urand(1, 100) <= 75 ? (rand_norm() - 0.5) * M_PI * 0.5 : (rand_norm() - 0.5) * M_PI * 2;
angle += delta;
float dis = rand_norm() * pathFinderDis;
float dx = x + cos(angle) * dis;
float dy = y + sin(angle) * dis;
float dz = z + 0.5f;
PathGenerator path(bot);
path.CalculatePath(dx, dy, dz);
PathType type = path.GetPathType();
uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
bool canReach = !(type & (~typeOk));
if (canReach && fabs(delta) <= minDelta)
{
found = true;
const G3D::Vector3& endPos = path.GetActualEndPosition();
rx = endPos.x;
ry = endPos.y;
rz = endPos.z;
minDelta = fabs(delta);
}
}
if (found)
{
return MoveTo(bot->GetMapId(), rx, ry, rz, false, false, false, true);
}
return false;
}
bool NewRpgGoGrindAction::Execute(Event event) { return MoveFarTo(botAI->rpgInfo.grindPos); }
bool NewRpgGoInnKeeperAction::Execute(Event event) { return MoveFarTo(botAI->rpgInfo.innKeeperPos); }
bool NewRpgMoveRandomAction::Execute(Event event)
{
float distance = rand_norm() * moveStep;
Map* map = bot->GetMap();
const float x = bot->GetPositionX();
const float y = bot->GetPositionY();
const float z = bot->GetPositionZ();
int attempts = 5;
while (--attempts)
{
float angle = (float)rand_norm() * 2 * static_cast<float>(M_PI);
float dx = x + distance * cos(angle);
float dy = y + distance * sin(angle);
float dz = z;
if (!map->CheckCollisionAndGetValidCoords(bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(),
dx, dy, dz))
continue;
if (map->IsInWater(bot->GetPhaseMask(), dx, dy, dz, bot->GetCollisionHeight()))
continue;
bool moved = MoveTo(bot->GetMapId(), dx, dy, dz, false, false, false, true);
if (moved)
return true;
}
return false;
if (SearchQuestGiverAndAcceptOrReward())
return true;
return MoveRandomNear();
}
bool NewRpgMoveNpcAction::Execute(Event event)
{
NewRpgInfo& info = botAI->rpgInfo;
if (!info.npcPos)
if (!info.near_npc.npc)
{
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible rpg targets");
if (possibleTargets.empty())
return false;
int idx = urand(0, possibleTargets.size() - 1);
ObjectGuid guid = possibleTargets[idx];
Unit* unit = botAI->GetUnit(guid);
if (unit)
// No npc can be found, switch to IDLE
ObjectGuid npc = ChooseNpcOrGameObjectToInteract();
if (npc.IsEmpty())
{
info.npcPos = GuidPosition(unit);
info.lastReachNpc = 0;
info.ChangeToIdle();
return true;
}
else
return false;
info.near_npc.npc = npc;
info.near_npc.lastReach = 0;
return true;
}
if (bot->GetDistance(info.npcPos) <= INTERACTION_DISTANCE)
Unit* unit = botAI->GetUnit(info.near_npc.npc);
if (unit && bot->GetDistance(unit) <= INTERACTION_DISTANCE)
{
if (!info.lastReachNpc)
if (!info.near_npc.lastReach)
{
info.lastReachNpc = getMSTime();
info.near_npc.lastReach = getMSTime();
InteractWithNpcOrGameObjectForQuest(info.near_npc.npc);
return true;
}
if (info.lastReachNpc && info.lastReachNpc + stayTime > getMSTime())
if (info.near_npc.lastReach && GetMSTimeDiffToNow(info.near_npc.lastReach) < npcStayTime)
return false;
info.npcPos = GuidPosition();
info.lastReachNpc = 0;
// has reached the npc for more than `npcStayTime`, select the next target
info.near_npc.npc = ObjectGuid();
info.near_npc.lastReach = 0;
}
else
{
assert(info.npcPos);
Unit* unit = botAI->GetUnit(info.npcPos);
if (!unit)
return false;
float x = unit->GetPositionX();
float y = unit->GetPositionY();
float z = unit->GetPositionZ();
float mapId = unit->GetMapId();
float angle = 0.f;
if (bot->IsWithinLOS(x, y, z))
{
if (!unit->isMoving())
angle = unit->GetAngle(bot) + (M_PI * irand(-25, 25) / 100.0); // Closest 45 degrees towards the target
else
angle = unit->GetOrientation() +
(M_PI * irand(-25, 25) / 100.0); // 45 degrees infront of target (leading it's movement)
}
else
angle = 2 * M_PI * rand_norm(); // A circle around the target.
float rnd = rand_norm();
x += cos(angle) * INTERACTION_DISTANCE * rnd;
y += sin(angle) * INTERACTION_DISTANCE * rnd;
// bool exact = true;
if (!unit->GetMap()->CheckCollisionAndGetValidCoords(unit, unit->GetPositionX(), unit->GetPositionY(),
unit->GetPositionZ(), x, y, z))
{
x = unit->GetPositionX();
y = unit->GetPositionY();
z = unit->GetPositionZ();
// exact = false;
}
return MoveTo(mapId, x, y, z, false, false, false, true);
return MoveNpcTo(info.near_npc.npc);
}
return true;
}
}
bool NewRpgDoQuestAction::Execute(Event event)
{
if (SearchQuestGiverAndAcceptOrReward())
return true;
NewRpgInfo& info = botAI->rpgInfo;
uint32 questId = RPG_INFO(quest, questId);
const Quest* quest = RPG_INFO(quest, quest);
uint8 questStatus = bot->GetQuestStatus(questId);
switch (questStatus)
{
case QUEST_STATUS_INCOMPLETE:
return DoIncompleteQuest();
case QUEST_STATUS_COMPLETE:
return DoCompletedQuest();
default:
break;
}
botAI->rpgInfo.ChangeToIdle();
return true;
}
bool NewRpgDoQuestAction::DoIncompleteQuest()
{
uint32 questId = RPG_INFO(do_quest, questId);
if (botAI->rpgInfo.do_quest.pos != WorldPosition())
{
/// @TODO: extract to a new function
int32 currentObjective = botAI->rpgInfo.do_quest.objectiveIdx;
// check if the objective has completed
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId);
bool completed = true;
if (currentObjective < QUEST_OBJECTIVES_COUNT)
{
if (q_status.CreatureOrGOCount[currentObjective] < quest->RequiredNpcOrGoCount[currentObjective])
completed = false;
}
else if (currentObjective < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT)
{
if (q_status.ItemCount[currentObjective - QUEST_OBJECTIVES_COUNT] <
quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT])
completed = false;
}
// the current objective is completed, clear and find a new objective later
if (completed)
{
botAI->rpgInfo.do_quest.lastReachPOI = 0;
botAI->rpgInfo.do_quest.pos = WorldPosition();
botAI->rpgInfo.do_quest.objectiveIdx = 0;
}
}
if (botAI->rpgInfo.do_quest.pos == WorldPosition())
{
std::vector<POIInfo> poiInfo;
if (!GetQuestPOIPosAndObjectiveIdx(questId, poiInfo))
{
// can't find a poi pos to go, stop doing quest for now
botAI->rpgInfo.ChangeToIdle();
return true;
}
uint32 rndIdx = urand(0, poiInfo.size() - 1);
G3D::Vector2 nearestPoi = poiInfo[rndIdx].pos;
int32 objectiveIdx = poiInfo[rndIdx].objectiveIdx;
float dx = nearestPoi.x, dy = nearestPoi.y;
// z = MAX_HEIGHT as we do not know accurate z
float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy));
// double check for GetQuestPOIPosAndObjectiveIdx
if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE)
return false;
WorldPosition pos(bot->GetMapId(), dx, dy, dz);
botAI->rpgInfo.do_quest.lastReachPOI = 0;
botAI->rpgInfo.do_quest.pos = pos;
botAI->rpgInfo.do_quest.objectiveIdx = objectiveIdx;
}
if (bot->GetDistance(botAI->rpgInfo.do_quest.pos) > 10.0f && !botAI->rpgInfo.do_quest.lastReachPOI)
{
return MoveFarTo(botAI->rpgInfo.do_quest.pos);
}
// Now we are near the quest objective
// kill mobs and looting quest should be done automatically by grind strategy
if (!botAI->rpgInfo.do_quest.lastReachPOI)
{
botAI->rpgInfo.do_quest.lastReachPOI = getMSTime();
return true;
}
// stayed at this POI for more than 5 minutes
if (GetMSTimeDiffToNow(botAI->rpgInfo.do_quest.lastReachPOI) >= poiStayTime)
{
bool hasProgression = false;
int32 currentObjective = botAI->rpgInfo.do_quest.objectiveIdx;
// check if the objective has progression
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId);
if (currentObjective < QUEST_OBJECTIVES_COUNT)
{
if (q_status.CreatureOrGOCount[currentObjective] != 0 && quest->RequiredNpcOrGoCount[currentObjective])
hasProgression = true;
}
else if (currentObjective < QUEST_OBJECTIVES_COUNT + QUEST_ITEM_OBJECTIVES_COUNT)
{
if (q_status.ItemCount[currentObjective - QUEST_OBJECTIVES_COUNT] != 0 &&
quest->RequiredItemCount[currentObjective - QUEST_OBJECTIVES_COUNT])
hasProgression = true;
}
if (!hasProgression)
{
// we has reach the poi for more than 5 mins but no progession
// may not be able to complete this quest, marked as abandoned
/// @TODO: It may be better to make lowPriorityQuest a global set shared by all bots (or saved in db)
botAI->lowPriorityQuest.insert(questId);
botAI->rpgStatistic.questAbandoned++;
LOG_DEBUG("playerbots", "[New rpg] {} marked as abandoned quest {}", bot->GetName(), questId);
botAI->rpgInfo.ChangeToIdle();
return true;
}
// clear and select another poi later
botAI->rpgInfo.do_quest.lastReachPOI = 0;
botAI->rpgInfo.do_quest.pos = WorldPosition();
botAI->rpgInfo.do_quest.objectiveIdx = 0;
return true;
}
return MoveRandomNear(20.0f);
}
bool NewRpgDoQuestAction::DoCompletedQuest()
{
uint32 questId = RPG_INFO(quest, questId);
const Quest* quest = RPG_INFO(quest, quest);
if (RPG_INFO(quest, objectiveIdx) != -1)
{
// if quest is completed, back to poi with -1 idx to reward
BroadcastHelper::BroadcastQuestUpdateComplete(botAI, bot, quest);
botAI->rpgStatistic.questCompleted++;
std::vector<POIInfo> poiInfo;
if (!GetQuestPOIPosAndObjectiveIdx(questId, poiInfo, true))
{
// can't find a poi pos to reward, stop doing quest for now
botAI->rpgInfo.ChangeToIdle();
return false;
}
assert(poiInfo.size() > 0);
// now we get the place to get rewarded
float dx = poiInfo[0].pos.x, dy = poiInfo[0].pos.y;
// z = MAX_HEIGHT as we do not know accurate z
float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy));
// double check for GetQuestPOIPosAndObjectiveIdx
if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE)
return false;
WorldPosition pos(bot->GetMapId(), dx, dy, dz);
botAI->rpgInfo.do_quest.lastReachPOI = 0;
botAI->rpgInfo.do_quest.pos = pos;
botAI->rpgInfo.do_quest.objectiveIdx = -1;
}
if (botAI->rpgInfo.do_quest.pos == WorldPosition())
return false;
if (bot->GetDistance(botAI->rpgInfo.do_quest.pos) > 10.0f && !botAI->rpgInfo.do_quest.lastReachPOI)
return MoveFarTo(botAI->rpgInfo.do_quest.pos);
// Now we are near the qoi of reward
// the quest should be rewarded by SearchQuestGiverAndAcceptOrReward
if (!botAI->rpgInfo.do_quest.lastReachPOI)
{
botAI->rpgInfo.do_quest.lastReachPOI = getMSTime();
return true;
}
// stayed at this POI for more than 5 minutes
if (GetMSTimeDiffToNow(botAI->rpgInfo.do_quest.lastReachPOI) >= poiStayTime)
{
// e.g. Can not reward quest to gameobjects
/// @TODO: It may be better to make lowPriorityQuest a global set shared by all bots (or saved in db)
botAI->lowPriorityQuest.insert(questId);
botAI->rpgStatistic.questAbandoned++;
LOG_DEBUG("playerbots", "[New rpg] {} marked as abandoned quest {}", bot->GetName(), questId);
botAI->rpgInfo.ChangeToIdle();
return true;
}
return false;
}

View File

@@ -3,9 +3,15 @@
#include "Duration.h"
#include "MovementActions.h"
#include "NewRpgInfo.h"
#include "NewRpgStrategy.h"
#include "Object.h"
#include "ObjectDefines.h"
#include "ObjectGuid.h"
#include "QuestDef.h"
#include "TravelMgr.h"
#include "PlayerbotAI.h"
#include "NewRpgBaseAction.h"
class TellRpgStatusAction : public Action
{
@@ -15,65 +21,78 @@ public:
bool Execute(Event event) override;
};
class NewRpgStatusUpdateAction : public Action
class StartRpgDoQuestAction : public Action
{
public:
NewRpgStatusUpdateAction(PlayerbotAI* botAI) : Action(botAI, "new rpg status update") {}
StartRpgDoQuestAction(PlayerbotAI* botAI) : Action(botAI, "start rpg do quest") {}
bool Execute(Event event) override;
};
class NewRpgStatusUpdateAction : public NewRpgBaseAction
{
public:
NewRpgStatusUpdateAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg status update")
{
// int statusCount = RPG_STATUS_END - 1;
// transitionMat.resize(statusCount, std::vector<int>(statusCount, 0));
// transitionMat[RPG_IDLE][RPG_GO_GRIND] = 20;
// transitionMat[RPG_IDLE][RPG_GO_INNKEEPER] = 15;
// transitionMat[RPG_IDLE][RPG_NEAR_NPC] = 30;
// transitionMat[RPG_IDLE][RPG_DO_QUEST] = 35;
}
bool Execute(Event event) override;
protected:
// const int32 setGrindInterval = 5 * 60 * 1000;
// const int32 setNpcInterval = 1 * 60 * 1000;
// static NewRpgStatusTransitionProb transitionMat;
const int32 statusNearNpcDuration = 5 * 60 * 1000;
const int32 statusNearRandomDuration = 5 * 60 * 1000;
const int32 statusRestDuration = 30 * 1000;
WorldPosition SelectRandomGrindPos();
WorldPosition SelectRandomInnKeeperPos();
const int32 statusDoQuestDuration = 30 * 60 * 1000;
};
class NewRpgGoFarAwayPosAction : public MovementAction
class NewRpgGoGrindAction : public NewRpgBaseAction
{
public:
NewRpgGoFarAwayPosAction(PlayerbotAI* botAI, std::string name) : MovementAction(botAI, name) {}
// bool Execute(Event event) override;
bool MoveFarTo(WorldPosition dest);
protected:
// WorldPosition dest;
const float pathFinderDis = 70.0f; // path finder
const uint32 stuckTime = 5 * 60 * 1000;
};
class NewRpgGoGrindAction : public NewRpgGoFarAwayPosAction
{
public:
NewRpgGoGrindAction(PlayerbotAI* botAI) : NewRpgGoFarAwayPosAction(botAI, "new rpg go grind") {}
NewRpgGoGrindAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg go grind") {}
bool Execute(Event event) override;
};
class NewRpgGoInnKeeperAction : public NewRpgGoFarAwayPosAction
class NewRpgGoInnKeeperAction : public NewRpgBaseAction
{
public:
NewRpgGoInnKeeperAction(PlayerbotAI* botAI) : NewRpgGoFarAwayPosAction(botAI, "new rpg go innkeeper") {}
NewRpgGoInnKeeperAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg go innkeeper") {}
bool Execute(Event event) override;
};
class NewRpgMoveRandomAction : public MovementAction
class NewRpgMoveRandomAction : public NewRpgBaseAction
{
public:
NewRpgMoveRandomAction(PlayerbotAI* botAI) : MovementAction(botAI, "new rpg move random") {}
NewRpgMoveRandomAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg move random") {}
bool Execute(Event event) override;
};
class NewRpgMoveNpcAction : public NewRpgBaseAction
{
public:
NewRpgMoveNpcAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg move npcs") {}
bool Execute(Event event) override;
const uint32 npcStayTime = 8 * 1000;
};
class NewRpgDoQuestAction : public NewRpgBaseAction
{
public:
NewRpgDoQuestAction(PlayerbotAI* botAI) : NewRpgBaseAction(botAI, "new rpg do quest") {}
bool Execute(Event event) override;
protected:
const float moveStep = 50.0f;
};
class NewRpgMoveNpcAction : public MovementAction
{
public:
NewRpgMoveNpcAction(PlayerbotAI* botAI) : MovementAction(botAI, "new rpg move npcs") {}
bool Execute(Event event) override;
protected:
const uint32 stayTime = 8 * 1000;
bool DoIncompleteQuest();
bool DoCompletedQuest();
const uint32 poiStayTime = 5 * 60 * 1000;
};
#endif

View File

@@ -0,0 +1,801 @@
#include "NewRpgBaseAction.h"
#include "ChatHelper.h"
#include "G3D/Vector2.h"
#include "GameObject.h"
#include "GossipDef.h"
#include "GridTerrainData.h"
#include "IVMapMgr.h"
#include "NewRpgInfo.h"
#include "NewRpgStrategy.h"
#include "Object.h"
#include "ObjectAccessor.h"
#include "ObjectDefines.h"
#include "ObjectGuid.h"
#include "ObjectMgr.h"
#include "PathGenerator.h"
#include "Player.h"
#include "PlayerbotAI.h"
#include "Playerbots.h"
#include "Position.h"
#include "QuestDef.h"
#include "Random.h"
#include "RandomPlayerbotMgr.h"
#include "SharedDefines.h"
#include "StatsWeightCalculator.h"
#include "Timer.h"
#include "TravelMgr.h"
#include "BroadcastHelper.h"
bool NewRpgBaseAction::MoveFarTo(WorldPosition dest)
{
if (dest == WorldPosition())
return false;
if (dest != botAI->rpgInfo.moveFarPos)
{
// clear stuck information if it's a new dest
botAI->rpgInfo.SetMoveFarTo(dest);
}
float dis = bot->GetExactDist(dest);
if (dis < pathFinderDis)
{
return MoveTo(dest.getMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), false, false,
false, true);
}
// performance optimization
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
{
return false;
}
// stuck check
float disToDest = bot->GetDistance(dest);
if (disToDest + 1.0f < botAI->rpgInfo.nearestMoveFarDis)
{
botAI->rpgInfo.nearestMoveFarDis = disToDest;
botAI->rpgInfo.stuckTs = getMSTime();
botAI->rpgInfo.stuckAttempts = 0;
}
else if (++botAI->rpgInfo.stuckAttempts >= 10 && GetMSTimeDiffToNow(botAI->rpgInfo.stuckTs) >= stuckTime)
{
// Unfortunately we've been stuck here for over 5 mins, fallback to teleporting directly to the destination
botAI->rpgInfo.stuckTs = getMSTime();
botAI->rpgInfo.stuckAttempts = 0;
const AreaTableEntry* entry = sAreaTableStore.LookupEntry(bot->GetZoneId());
std::string zone_name = PlayerbotAI::GetLocalizedAreaName(entry);
LOG_DEBUG("playerbots", "[New Rpg] Teleport {} from ({},{},{},{}) to ({},{},{},{}) as it stuck when moving far - Zone: {} ({})", bot->GetName(),
bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetMapId(),
dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(), dest.getMapId(), bot->GetZoneId(), zone_name);
return bot->TeleportTo(dest);
}
float minDelta = M_PI;
const float x = bot->GetPositionX();
const float y = bot->GetPositionY();
const float z = bot->GetPositionZ();
float rx, ry, rz;
bool found = false;
int attempt = 3;
while (--attempt)
{
float angle = bot->GetAngle(&dest);
float delta = urand(1, 100) <= 75 ? (rand_norm() - 0.5) * M_PI * 0.5 : (rand_norm() - 0.5) * M_PI * 2;
angle += delta;
float dis = rand_norm() * pathFinderDis;
float dx = x + cos(angle) * dis;
float dy = y + sin(angle) * dis;
float dz = z + 0.5f;
PathGenerator path(bot);
path.CalculatePath(dx, dy, dz);
PathType type = path.GetPathType();
uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
bool canReach = !(type & (~typeOk));
if (canReach && fabs(delta) <= minDelta)
{
found = true;
const G3D::Vector3& endPos = path.GetActualEndPosition();
rx = endPos.x;
ry = endPos.y;
rz = endPos.z;
minDelta = fabs(delta);
}
}
if (found)
{
return MoveTo(bot->GetMapId(), rx, ry, rz, false, false, false, true);
}
return false;
}
bool NewRpgBaseAction::MoveNpcTo(ObjectGuid guid, float distance)
{
if (IsWaitingForLastMove(MovementPriority::MOVEMENT_NORMAL))
{
return false;
}
Unit* unit = botAI->GetUnit(guid);
if (!unit)
return false;
float x = unit->GetPositionX();
float y = unit->GetPositionY();
float z = unit->GetPositionZ();
float mapId = unit->GetMapId();
float angle = 0.f;
if (!unit->isMoving())
angle = unit->GetAngle(bot) + (M_PI * irand(-25, 25) / 100.0); // Closest 45 degrees towards the target
else
angle = unit->GetOrientation() +
(M_PI * irand(-25, 25) / 100.0); // 45 degrees infront of target (leading it's movement)
float rnd = rand_norm();
x += cos(angle) * distance * rnd;
y += sin(angle) * distance * rnd;
if (!unit->GetMap()->CheckCollisionAndGetValidCoords(unit, unit->GetPositionX(), unit->GetPositionY(),
unit->GetPositionZ(), x, y, z))
{
x = unit->GetPositionX();
y = unit->GetPositionY();
z = unit->GetPositionZ();
}
return MoveTo(mapId, x, y, z, false, false, false, true);
}
bool NewRpgBaseAction::MoveRandomNear(float moveStep, MovementPriority priority)
{
if (IsWaitingForLastMove(priority))
{
return false;
}
float distance = rand_norm() * moveStep;
Map* map = bot->GetMap();
const float x = bot->GetPositionX();
const float y = bot->GetPositionY();
const float z = bot->GetPositionZ();
int attempts = 5;
while (--attempts)
{
float angle = (float)rand_norm() * 2 * static_cast<float>(M_PI);
float dx = x + distance * cos(angle);
float dy = y + distance * sin(angle);
float dz = z;
PathGenerator path(bot);
path.CalculatePath(dx, dy, dz);
PathType type = path.GetPathType();
uint32 typeOk = PATHFIND_NORMAL | PATHFIND_INCOMPLETE | PATHFIND_FARFROMPOLY;
bool canReach = !(type & (~typeOk));
if (!canReach)
continue;
if (!map->CanReachPositionAndGetValidCoords(bot, dx, dy, dz))
continue;
if (map->IsInWater(bot->GetPhaseMask(), dx, dy, dz, bot->GetCollisionHeight()))
continue;
bool moved = MoveTo(bot->GetMapId(), dx, dy, dz, false, false, false, true, priority);
if (moved)
return true;
}
return false;
}
bool NewRpgBaseAction::ForceToWait(uint32 duration, MovementPriority priority)
{
AI_VALUE(LastMovement&, "last movement").Set(bot->GetMapId(),
bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), bot->GetOrientation(), duration, priority);
return true;
}
/// @TODO: Fix redundant code
/// Quest related method refer to TalkToQuestGiverAction.h
bool NewRpgBaseAction::InteractWithNpcOrGameObjectForQuest(ObjectGuid guid)
{
WorldObject* object = ObjectAccessor::GetWorldObject(*bot, guid);
if (!object || !bot->CanInteractWithQuestGiver(object))
return false;
// Creature* creature = bot->GetNPCIfCanInteractWith(guid, UNIT_NPC_FLAG_NONE);
// if (creature)
// {
// WorldPacket packet(CMSG_GOSSIP_HELLO);
// packet << guid;
// bot->GetSession()->HandleGossipHelloOpcode(packet);
// }
bot->PrepareQuestMenu(guid);
const QuestMenu &menu = bot->PlayerTalkClass->GetQuestMenu();
if (menu.Empty())
return true;
for (uint8 idx = 0; idx < menu.GetMenuItemCount(); idx++)
{
const QuestMenuItem &item = menu.GetItem(idx);
const Quest* quest = sObjectMgr->GetQuestTemplate(item.QuestId);
if (!quest)
continue;
const QuestStatus &status = bot->GetQuestStatus(item.QuestId);
if (status == QUEST_STATUS_NONE && bot->CanTakeQuest(quest, false) &&
bot->CanAddQuest(quest, false) && IsQuestWorthDoing(quest) && IsQuestCapableDoing(quest))
{
AcceptQuest(quest, guid);
if (botAI->GetMaster())
botAI->TellMasterNoFacing("Quest accepted " + ChatHelper::FormatQuest(quest));
BroadcastHelper::BroadcastQuestAccepted(botAI, bot, quest);
botAI->rpgStatistic.questAccepted++;
LOG_DEBUG("playerbots", "[New rpg] {} accept quest {}", bot->GetName(), quest->GetQuestId());
}
if (status == QUEST_STATUS_COMPLETE && bot->CanRewardQuest(quest, 0, false))
{
TurnInQuest(quest, guid);
if (botAI->GetMaster())
botAI->TellMasterNoFacing("Quest rewarded " + ChatHelper::FormatQuest(quest));
BroadcastHelper::BroadcastQuestTurnedIn(botAI, bot, quest);
botAI->rpgStatistic.questRewarded++;
LOG_DEBUG("playerbots", "[New rpg] {} turned in quest {}", bot->GetName(), quest->GetQuestId());
}
}
return true;
}
bool NewRpgBaseAction::AcceptQuest(Quest const* quest, ObjectGuid guid)
{
WorldPacket p(CMSG_QUESTGIVER_ACCEPT_QUEST);
uint32 unk1 = 0;
p << guid << quest->GetQuestId() << unk1;
p.rpos(0);
bot->GetSession()->HandleQuestgiverAcceptQuestOpcode(p);
return true;
}
bool NewRpgBaseAction::TurnInQuest(Quest const* quest, ObjectGuid guid)
{
uint32 questID = quest->GetQuestId();
if (bot->GetQuestRewardStatus(questID))
{
return false;
}
if (!bot->CanRewardQuest(quest, false))
{
return false;
}
bot->PlayDistanceSound(621);
WorldPacket p(CMSG_QUESTGIVER_CHOOSE_REWARD);
p << guid << quest->GetQuestId();
if (quest->GetRewChoiceItemsCount() <= 1)
{
p << 0;
bot->GetSession()->HandleQuestgiverChooseRewardOpcode(p);
}
else
{
uint32 bestId = BestReward(quest);
p << bestId;
bot->GetSession()->HandleQuestgiverChooseRewardOpcode(p);
}
return true;
}
uint32 NewRpgBaseAction::BestReward(Quest const* quest)
{
ItemIds returnIds;
ItemUsage bestUsage = ITEM_USAGE_NONE;
if (quest->GetRewChoiceItemsCount() <= 1)
return 0;
else
{
for (uint8 i = 0; i < quest->GetRewChoiceItemsCount(); ++i)
{
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", quest->RewardChoiceItemId[i]);
if (usage == ITEM_USAGE_EQUIP || usage == ITEM_USAGE_REPLACE)
bestUsage = ITEM_USAGE_EQUIP;
else if (usage == ITEM_USAGE_BAD_EQUIP && bestUsage != ITEM_USAGE_EQUIP)
bestUsage = usage;
else if (usage != ITEM_USAGE_NONE && bestUsage == ITEM_USAGE_NONE)
bestUsage = usage;
}
StatsWeightCalculator calc(bot);
uint32 best = 0;
float bestScore = 0;
for (uint8 i = 0; i < quest->GetRewChoiceItemsCount(); ++i)
{
ItemUsage usage = AI_VALUE2(ItemUsage, "item usage", quest->RewardChoiceItemId[i]);
if (usage == bestUsage || usage == ITEM_USAGE_REPLACE)
{
float score = calc.CalculateItem(quest->RewardChoiceItemId[i]);
if (score > bestScore)
{
bestScore = score;
best = i;
}
}
}
return best;
}
}
bool NewRpgBaseAction::IsQuestWorthDoing(Quest const* quest)
{
bool isLowLevelQuest = bot->GetLevel() > (bot->GetQuestLevel(quest) + sWorld->getIntConfig(CONFIG_QUEST_LOW_LEVEL_HIDE_DIFF));
if (isLowLevelQuest)
return false;
if (quest->IsRepeatable())
return false;
if (quest->IsSeasonal())
return false;
return true;
}
bool NewRpgBaseAction::IsQuestCapableDoing(Quest const* quest)
{
bool highLevelQuest = bot->GetLevel() + 3 < bot->GetQuestLevel(quest);
if (highLevelQuest)
return false;
// Elite quest and dungeon quest etc
if (quest->GetType() != 0)
return false;
// now we only capable of doing solo quests
if (quest->GetSuggestedPlayers() >= 2)
return false;
return true;
}
bool NewRpgBaseAction::OrganizeQuestLog()
{
int32 freeSlotNum = 0;
for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i)
{
uint32 questId = bot->GetQuestSlotQuestId(i);
if (!questId)
freeSlotNum++;
}
// it's ok if we have two more free slots
if (freeSlotNum >= 2)
return false;
int32 dropped = 0;
// remove quests that not worth doing or not capable of doing
for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i)
{
uint32 questId = bot->GetQuestSlotQuestId(i);
if (!questId)
continue;
const Quest* quest = sObjectMgr->GetQuestTemplate(questId);
if (!IsQuestWorthDoing(quest) ||
!IsQuestCapableDoing(quest) ||
bot->GetQuestStatus(questId) == QUEST_STATUS_FAILED)
{
LOG_DEBUG("playerbots", "[New rpg] {} drop quest {}", bot->GetName(), questId);
WorldPacket packet(CMSG_QUESTLOG_REMOVE_QUEST);
packet << (uint8)i;
bot->GetSession()->HandleQuestLogRemoveQuest(packet);
if (botAI->GetMaster())
botAI->TellMasterNoFacing("Quest dropped " + ChatHelper::FormatQuest(quest));
botAI->rpgStatistic.questDropped++;
dropped++;
}
}
// drop more than 8 quests at once to avoid repeated accept and drop
if (dropped >= 8)
return true;
// remove festival/class quests and quests in different zone
for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i)
{
uint32 questId = bot->GetQuestSlotQuestId(i);
if (!questId)
continue;
const Quest* quest = sObjectMgr->GetQuestTemplate(questId);
if (quest->GetZoneOrSort() < 0 ||
(quest->GetZoneOrSort() > 0 && quest->GetZoneOrSort() != bot->GetZoneId()))
{
LOG_DEBUG("playerbots", "[New rpg] {} drop quest {}", bot->GetName(), questId);
WorldPacket packet(CMSG_QUESTLOG_REMOVE_QUEST);
packet << (uint8)i;
bot->GetSession()->HandleQuestLogRemoveQuest(packet);
if (botAI->GetMaster())
botAI->TellMasterNoFacing("Quest dropped " + ChatHelper::FormatQuest(quest));
botAI->rpgStatistic.questDropped++;
dropped++;
}
}
if (dropped >= 8)
return true;
// clear quests log
for (uint16 i = 0; i < MAX_QUEST_LOG_SIZE; ++i)
{
uint32 questId = bot->GetQuestSlotQuestId(i);
if (!questId)
continue;
const Quest* quest = sObjectMgr->GetQuestTemplate(questId);
LOG_DEBUG("playerbots", "[New rpg] {} drop quest {}", bot->GetName(), questId);
WorldPacket packet(CMSG_QUESTLOG_REMOVE_QUEST);
packet << (uint8)i;
bot->GetSession()->HandleQuestLogRemoveQuest(packet);
if (botAI->GetMaster())
botAI->TellMasterNoFacing("Quest dropped " + ChatHelper::FormatQuest(quest));
botAI->rpgStatistic.questDropped++;
}
return true;
}
bool NewRpgBaseAction::SearchQuestGiverAndAcceptOrReward()
{
OrganizeQuestLog();
if (ObjectGuid npc = ChooseNpcOrGameObjectToInteract(true, 80.0f))
{
const WorldObject* object = ObjectAccessor::GetWorldObject(*bot, npc);
if (bot->GetDistance(object) <= INTERACTION_DISTANCE)
{
InteractWithNpcOrGameObjectForQuest(npc);
ForceToWait(5000);
return true;
}
return MoveNpcTo(npc);
}
return false;
}
ObjectGuid NewRpgBaseAction::ChooseNpcOrGameObjectToInteract(bool questgiverOnly, float distanceLimit)
{
GuidVector possibleTargets = AI_VALUE(GuidVector, "possible new rpg targets");
GuidVector possibleGameObjects = AI_VALUE(GuidVector, "possible new rpg game objects");
if (possibleTargets.empty() && possibleGameObjects.empty())
return ObjectGuid();
WorldObject* nearestObject = nullptr;
for (ObjectGuid& guid: possibleTargets)
{
WorldObject* object = ObjectAccessor::GetWorldObject(*bot, guid);
if (!object || !object->IsInWorld())
continue;
if (distanceLimit && bot->GetDistance(object) > distanceLimit)
continue;
if (HasQuestToAcceptOrReward(object))
{
if (!nearestObject || bot->GetExactDist(nearestObject) > bot->GetExactDist(object))
nearestObject = object;
break;
}
}
for (ObjectGuid& guid: possibleGameObjects)
{
WorldObject* object = ObjectAccessor::GetWorldObject(*bot, guid);
if (!object || !object->IsInWorld())
continue;
if (distanceLimit && bot->GetDistance(object) > distanceLimit)
continue;
if (HasQuestToAcceptOrReward(object))
{
if (!nearestObject || bot->GetExactDist(nearestObject) > bot->GetExactDist(object))
nearestObject = object;
break;
}
}
if (nearestObject)
return nearestObject->GetGUID();
// No questgiver to accept or reward
if (questgiverOnly)
return ObjectGuid();
if (possibleTargets.empty())
return ObjectGuid();
int idx = urand(0, possibleTargets.size() - 1);
ObjectGuid guid = possibleTargets[idx];
WorldObject* object = ObjectAccessor::GetCreatureOrPetOrVehicle(*bot, guid);
if (!object)
object = ObjectAccessor::GetGameObject(*bot, guid);
if (object && object->IsInWorld())
{
return object->GetGUID();
}
return ObjectGuid();
}
bool NewRpgBaseAction::HasQuestToAcceptOrReward(WorldObject* object)
{
ObjectGuid guid = object->GetGUID();
bot->PrepareQuestMenu(guid);
const QuestMenu &menu = bot->PlayerTalkClass->GetQuestMenu();
if (menu.Empty())
return false;
for (uint8 idx = 0; idx < menu.GetMenuItemCount(); idx++)
{
const QuestMenuItem &item = menu.GetItem(idx);
const Quest* quest = sObjectMgr->GetQuestTemplate(item.QuestId);
if (!quest)
continue;
const QuestStatus &status = bot->GetQuestStatus(item.QuestId);
if (status == QUEST_STATUS_COMPLETE && bot->CanRewardQuest(quest, 0, false))
{
return true;
}
}
for (uint8 idx = 0; idx < menu.GetMenuItemCount(); idx++)
{
const QuestMenuItem &item = menu.GetItem(idx);
const Quest* quest = sObjectMgr->GetQuestTemplate(item.QuestId);
if (!quest)
continue;
const QuestStatus &status = bot->GetQuestStatus(item.QuestId);
if (status == QUEST_STATUS_NONE && bot->CanTakeQuest(quest, false) &&
bot->CanAddQuest(quest, false) && IsQuestWorthDoing(quest) && IsQuestCapableDoing(quest))
{
return true;
}
}
return false;
}
static std::vector<float> GenerateRandomWeights(int n) {
std::vector<float> weights(n);
float sum = 0.0;
for (int i = 0; i < n; ++i) {
weights[i] = rand_norm();
sum += weights[i];
}
for (int i = 0; i < n; ++i) {
weights[i] /= sum;
}
return weights;
}
bool NewRpgBaseAction::GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo> &poiInfo, bool toComplete)
{
Quest const* quest = sObjectMgr->GetQuestTemplate(questId);
if (!quest)
return false;
const QuestPOIVector* poiVector = sObjectMgr->GetQuestPOIVector(questId);
if (!poiVector)
{
return false;
}
const QuestStatusData& q_status = bot->getQuestStatusMap().at(questId);
if (toComplete && q_status.Status == QUEST_STATUS_COMPLETE)
{
for (const QuestPOI &qPoi : *poiVector)
{
if (qPoi.MapId != bot->GetMapId())
continue;
// not the poi pos to reward quest
if (qPoi.ObjectiveIndex != -1)
continue;
if (qPoi.points.size() == 0)
continue;
float dx = 0, dy = 0;
std::vector<float> weights = GenerateRandomWeights(qPoi.points.size());
for (size_t i = 0; i < qPoi.points.size(); i++)
{
const QuestPOIPoint &point = qPoi.points[i];
dx += point.x * weights[i];
dy += point.y * weights[i];
}
if (bot->GetDistance2d(dx, dy) >= 1500.0f)
continue;
float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy));
if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE)
continue;
if (bot->GetZoneId() != bot->GetMap()->GetZoneId(bot->GetPhaseMask(), dx, dy, dz))
continue;
poiInfo.push_back({{dx, dy}, qPoi.ObjectiveIndex});
}
if (poiInfo.empty())
return false;
return true;
}
if (q_status.Status != QUEST_STATUS_INCOMPLETE)
return false;
// Get incomplete quest objective index
std::vector<int32> incompleteObjectiveIdx;
for (int i = 0; i < QUEST_OBJECTIVES_COUNT; i++)
{
int32 npcOrGo = quest->RequiredNpcOrGo[i];
if (!npcOrGo)
continue;
if (q_status.CreatureOrGOCount[i] < quest->RequiredNpcOrGoCount[i])
incompleteObjectiveIdx.push_back(i);
}
for (int i = 0; i < QUEST_ITEM_OBJECTIVES_COUNT; i++)
{
uint32 itemId = quest->RequiredItemId[i];
if (!itemId)
continue;
if (q_status.ItemCount[i] < quest->RequiredItemCount[i])
incompleteObjectiveIdx.push_back(QUEST_OBJECTIVES_COUNT + i);
}
// Get POIs to go
for (const QuestPOI &qPoi : *poiVector)
{
if (qPoi.MapId != bot->GetMapId())
continue;
bool inComplete = false;
for (uint32 objective : incompleteObjectiveIdx)
{
if (qPoi.ObjectiveIndex == objective)
{
inComplete = true;
break;
}
}
if (!inComplete)
continue;
if (qPoi.points.size() == 0)
continue;
float dx = 0, dy = 0;
std::vector<float> weights = GenerateRandomWeights(qPoi.points.size());
for (size_t i = 0; i < qPoi.points.size(); i++)
{
const QuestPOIPoint &point = qPoi.points[i];
dx += point.x * weights[i];
dy += point.y * weights[i];
}
if (bot->GetDistance2d(dx, dy) >= 1500.0f)
continue;
float dz = std::max(bot->GetMap()->GetHeight(dx, dy, MAX_HEIGHT), bot->GetMap()->GetWaterLevel(dx, dy));
if (dz == INVALID_HEIGHT || dz == VMAP_INVALID_HEIGHT_VALUE)
continue;
if (bot->GetZoneId() != bot->GetMap()->GetZoneId(bot->GetPhaseMask(), dx, dy, dz))
continue;
poiInfo.push_back({{dx, dy}, qPoi.ObjectiveIndex});
}
if (poiInfo.size() == 0) {
// LOG_DEBUG("playerbots", "[New rpg] {}: No available poi can be found for quest {}", bot->GetName(), questId);
return false;
}
return true;
}
WorldPosition NewRpgBaseAction::SelectRandomGrindPos(Player* bot)
{
const std::vector<WorldLocation>& locs = sRandomPlayerbotMgr->locsPerLevelCache[bot->GetLevel()];
float hiRange = 500.0f;
float loRange = 2500.0f;
if (bot->GetLevel() < 5)
{
hiRange /= 10;
loRange /= 10;
}
std::vector<WorldLocation> lo_prepared_locs, hi_prepared_locs;
for (auto& loc : locs)
{
if (bot->GetMapId() != loc.GetMapId())
continue;
if (bot->GetExactDist(loc) > 2500.0f)
continue;
if (bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()) !=
bot->GetZoneId())
continue;
if (bot->GetExactDist(loc) < 500.0f)
{
hi_prepared_locs.push_back(loc);
}
if (bot->GetExactDist(loc) < 2500.0f)
{
lo_prepared_locs.push_back(loc);
}
}
WorldPosition dest{};
if (urand(1, 100) <= 50 && !hi_prepared_locs.empty())
{
uint32 idx = urand(0, hi_prepared_locs.size() - 1);
dest = hi_prepared_locs[idx];
}
else if (!lo_prepared_locs.empty())
{
uint32 idx = urand(0, lo_prepared_locs.size() - 1);
dest = lo_prepared_locs[idx];
}
LOG_DEBUG("playerbots", "[New Rpg] Bot {} select random grind pos Map:{} X:{} Y:{} Z:{} ({}+{} available in {})",
bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
hi_prepared_locs.size(), lo_prepared_locs.size() - hi_prepared_locs.size(), locs.size());
return dest;
}
WorldPosition NewRpgBaseAction::SelectRandomInnKeeperPos(Player* bot)
{
const std::vector<WorldLocation>& locs = IsAlliance(bot->getRace())
? sRandomPlayerbotMgr->allianceStarterPerLevelCache[bot->GetLevel()]
: sRandomPlayerbotMgr->hordeStarterPerLevelCache[bot->GetLevel()];
std::vector<WorldLocation> prepared_locs;
for (auto& loc : locs)
{
if (bot->GetMapId() != loc.GetMapId())
continue;
float range = bot->GetLevel() <= 5 ? 500.0f : 2500.0f;
if (bot->GetExactDist(loc) > range)
continue;
if (bot->GetMap()->GetZoneId(bot->GetPhaseMask(), loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()) !=
bot->GetZoneId())
continue;
prepared_locs.push_back(loc);
}
WorldPosition dest{};
if (!prepared_locs.empty())
{
uint32 idx = urand(0, prepared_locs.size() - 1);
dest = prepared_locs[idx];
}
LOG_DEBUG("playerbots", "[New Rpg] Bot {} select random inn keeper pos Map:{} X:{} Y:{} Z:{} ({} available in {})",
bot->GetName(), dest.GetMapId(), dest.GetPositionX(), dest.GetPositionY(), dest.GetPositionZ(),
prepared_locs.size(), locs.size());
return dest;
}

View File

@@ -0,0 +1,58 @@
#ifndef _PLAYERBOT_NEWRPGBASEACTION_H
#define _PLAYERBOT_NEWRPGBASEACTION_H
#include "Duration.h"
#include "LastMovementValue.h"
#include "MovementActions.h"
#include "NewRpgStrategy.h"
#include "Object.h"
#include "ObjectDefines.h"
#include "ObjectGuid.h"
#include "QuestDef.h"
#include "TravelMgr.h"
#include "PlayerbotAI.h"
struct POIInfo {
G3D::Vector2 pos;
int32 objectiveIdx;
};
/// A base (composition) class for all new rpg actions
/// All functions that may be shared by multiple actions should be declared here
/// And we should make all actions composable instead of inheritable
class NewRpgBaseAction : public MovementAction
{
public:
NewRpgBaseAction(PlayerbotAI* botAI, std::string name) : MovementAction(botAI, name) {}
protected:
// MOVEMENT RELATED
bool MoveFarTo(WorldPosition dest);
bool MoveNpcTo(ObjectGuid guid, float distance = INTERACTION_DISTANCE);
bool MoveRandomNear(float moveStep = 50.0f, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
bool ForceToWait(uint32 duration, MovementPriority priority = MovementPriority::MOVEMENT_NORMAL);
// QUEST RELATED
bool SearchQuestGiverAndAcceptOrReward();
ObjectGuid ChooseNpcOrGameObjectToInteract(bool questgiverOnly = false, float distanceLimit = 0.0f);
bool HasQuestToAcceptOrReward(WorldObject* object);
bool InteractWithNpcOrGameObjectForQuest(ObjectGuid guid);
bool AcceptQuest(Quest const* quest, ObjectGuid guid);
bool TurnInQuest(Quest const* quest, ObjectGuid guid);
uint32 BestReward(Quest const* quest);
bool IsQuestWorthDoing(Quest const* quest);
bool IsQuestCapableDoing(Quest const* quest);
bool OrganizeQuestLog();
protected:
bool GetQuestPOIPosAndObjectiveIdx(uint32 questId, std::vector<POIInfo> &poiInfo, bool toComplete = false);
static WorldPosition SelectRandomGrindPos(Player* bot);
static WorldPosition SelectRandomInnKeeperPos(Player* bot);
protected:
// WorldPosition dest;
const float pathFinderDis = 70.0f; // path finder
const uint32 stuckTime = 5 * 60 * 1000;
};
#endif

View File

@@ -0,0 +1,119 @@
#include "NewRpgInfo.h"
#include "Timer.h"
void NewRpgInfo::ChangeToGoGrind(WorldPosition pos)
{
Reset();
status = RPG_GO_GRIND;
go_grind = GoGrind();
go_grind.pos = pos;
}
void NewRpgInfo::ChangeToGoInnkeeper(WorldPosition pos)
{
Reset();
status = RPG_GO_INNKEEPER;
go_innkeeper = GoInnkeeper();
go_innkeeper.pos = pos;
}
void NewRpgInfo::ChangeToNearNpc()
{
Reset();
status = RPG_NEAR_NPC;
near_npc = NearNpc();
}
void NewRpgInfo::ChangeToNearRandom()
{
Reset();
status = RPG_NEAR_RANDOM;
near_random = NearRandom();
}
void NewRpgInfo::ChangeToDoQuest(uint32 questId, const Quest* quest)
{
Reset();
status = RPG_DO_QUEST;
do_quest = DoQuest();
do_quest.questId = questId;
do_quest.quest = quest;
}
void NewRpgInfo::ChangeToRest()
{
Reset();
status = RPG_REST;
rest = Rest();
}
void NewRpgInfo::ChangeToIdle()
{
Reset();
status = RPG_IDLE;
}
bool NewRpgInfo::CanChangeTo(NewRpgStatus status)
{
return true;
}
void NewRpgInfo::Reset()
{
*this = NewRpgInfo();
startT = getMSTime();
}
void NewRpgInfo::SetMoveFarTo(WorldPosition pos)
{
nearestMoveFarDis = FLT_MAX;
stuckTs = 0;
stuckAttempts = 0;
moveFarPos = pos;
}
std::string NewRpgInfo::ToString()
{
std::stringstream out;
out << "Status: ";
switch (status)
{
case RPG_GO_GRIND:
out << "GO_GRIND";
out << "\nGrindPos: " << go_grind.pos.GetMapId() << " " << go_grind.pos.GetPositionX() << " " << go_grind.pos.GetPositionY() << " " << go_grind.pos.GetPositionZ();
out << "\nlastGoGrind: " << startT;
break;
case RPG_GO_INNKEEPER:
out << "GO_INNKEEPER";
out << "\nInnKeeperPos: " << go_innkeeper.pos.GetMapId() << " " << go_innkeeper.pos.GetPositionX() << " " << go_innkeeper.pos.GetPositionY() << " " << go_innkeeper.pos.GetPositionZ();
out << "\nlastGoInnKeeper: " << startT;
break;
case RPG_NEAR_NPC:
out << "NEAR_NPC";
out << "\nnpcEntry: " << near_npc.npc.GetCounter();
out << "\nlastNearNpc: " << startT;
out << "\nlastReachNpc: " << near_npc.lastReach;
break;
case RPG_NEAR_RANDOM:
out << "NEAR_RANDOM";
out << "\nlastNearRandom: " << startT;
break;
case RPG_IDLE:
out << "IDLE";
break;
case RPG_REST:
out << "REST";
out << "\nlastRest: " << startT;
break;
case RPG_DO_QUEST:
out << "DO_QUEST";
out << "\nquestId: " << do_quest.questId;
out << "\nobjectiveIdx: " << do_quest.objectiveIdx;
out << "\npoiPos: " << do_quest.pos.GetMapId() << " " << do_quest.pos.GetPositionX() << " " << do_quest.pos.GetPositionY() << " " << do_quest.pos.GetPositionZ();
out << "\nlastReachPOI: " << do_quest.lastReachPOI;
break;
default:
out << "UNKNOWN";
}
return out.str();
}

View File

@@ -0,0 +1,136 @@
#ifndef _PLAYERBOT_NEWRPGINFO_H
#define _PLAYERBOT_NEWRPGINFO_H
#include "Define.h"
#include "ObjectGuid.h"
#include "ObjectMgr.h"
#include "QuestDef.h"
#include "Strategy.h"
#include "Timer.h"
#include "TravelMgr.h"
enum NewRpgStatus: int
{
RPG_STATUS_START = 0,
// Going to far away place
RPG_GO_GRIND = 0,
RPG_GO_INNKEEPER = 1,
// Exploring nearby
RPG_NEAR_RANDOM = 2,
RPG_NEAR_NPC = 3,
// Do Quest (based on quest status)
RPG_DO_QUEST = 4,
// Taking a break
RPG_REST = 5,
// Initial status
RPG_IDLE = 6,
RPG_STATUS_END = 7
};
using NewRpgStatusTransitionProb = std::vector<std::vector<int>>;
struct NewRpgInfo
{
NewRpgInfo() {}
// RPG_GO_GRIND
struct GoGrind {
GoGrind() = default;
WorldPosition pos{};
};
// RPG_GO_INNKEEPER
struct GoInnkeeper {
GoInnkeeper() = default;
WorldPosition pos{};
};
// RPG_NEAR_NPC
struct NearNpc {
NearNpc() = default;
ObjectGuid npc{};
uint32 lastReach{0};
};
// RPG_NEAR_RANDOM
struct NearRandom {
NearRandom() = default;
};
// NewRpgStatus::QUESTING
struct DoQuest {
const Quest* quest{nullptr};
uint32 questId{0};
int32 objectiveIdx{0};
WorldPosition pos{};
uint32 lastReachPOI{0};
};
// RPG_REST
struct Rest {
Rest() = default;
};
struct Idle {
};
NewRpgStatus status{RPG_IDLE};
uint32 startT{0}; // start timestamp of the current status
// MOVE_FAR
float nearestMoveFarDis{FLT_MAX};
uint32 stuckTs{0};
uint32 stuckAttempts{0};
WorldPosition moveFarPos;
// END MOVE_FAR
union {
GoGrind go_grind;
GoInnkeeper go_innkeeper;
NearNpc near_npc;
NearRandom near_random;
DoQuest do_quest;
Rest rest;
DoQuest quest;
};
bool HasStatusPersisted(uint32 maxDuration) { return GetMSTimeDiffToNow(startT) > maxDuration; }
void ChangeToGoGrind(WorldPosition pos);
void ChangeToGoInnkeeper(WorldPosition pos);
void ChangeToNearNpc();
void ChangeToNearRandom();
void ChangeToDoQuest(uint32 questId, const Quest* quest);
void ChangeToRest();
void ChangeToIdle();
bool CanChangeTo(NewRpgStatus status);
void Reset();
void SetMoveFarTo(WorldPosition pos);
std::string ToString();
};
struct NewRpgStatistic
{
uint32 questAccepted{0};
uint32 questCompleted{0};
uint32 questAbandoned{0};
uint32 questRewarded{0};
uint32 questDropped{0};
NewRpgStatistic operator+(const NewRpgStatistic& other) const
{
NewRpgStatistic result;
result.questAccepted = this->questAccepted + other.questAccepted;
result.questCompleted = this->questCompleted + other.questCompleted;
result.questAbandoned = this->questAbandoned + other.questAbandoned;
result.questRewarded = this->questRewarded + other.questRewarded;
result.questDropped = this->questDropped + other.questDropped;
return result;
}
NewRpgStatistic& operator+=(const NewRpgStatistic& other)
{
this->questAccepted += other.questAccepted;
this->questCompleted += other.questCompleted;
this->questAbandoned += other.questAbandoned;
this->questRewarded += other.questRewarded;
this->questDropped += other.questDropped;
return *this;
}
};
// not sure is it necessary but keep it for now
#define RPG_INFO(x, y) botAI->rpgInfo.x.y
#endif

View File

@@ -11,22 +11,28 @@ NewRpgStrategy::NewRpgStrategy(PlayerbotAI* botAI) : Strategy(botAI) {}
NextAction** NewRpgStrategy::getDefaultActions()
{
return NextAction::array(0, new NextAction("new rpg status update", 5.0f), nullptr);
// the releavance should be greater than grind
return NextAction::array(0,
new NextAction("new rpg status update", 11.0f),
nullptr);
}
void NewRpgStrategy::InitTriggers(std::vector<TriggerNode*>& triggers)
{
triggers.push_back(
new TriggerNode("go grind status", NextAction::array(0, new NextAction("new rpg go grind", 1.0f), nullptr)));
new TriggerNode("go grind status", NextAction::array(0, new NextAction("new rpg go grind", 3.0f), nullptr)));
triggers.push_back(
new TriggerNode("go innkeeper status", NextAction::array(0, new NextAction("new rpg go innkeeper", 1.0f), nullptr)));
new TriggerNode("go innkeeper status", NextAction::array(0, new NextAction("new rpg go innkeeper", 3.0f), nullptr)));
triggers.push_back(
new TriggerNode("near random status", NextAction::array(0, new NextAction("new rpg move random", 1.0f), nullptr)));
new TriggerNode("near random status", NextAction::array(0, new NextAction("new rpg move random", 3.0f), nullptr)));
triggers.push_back(
new TriggerNode("near npc status", NextAction::array(0, new NextAction("new rpg move npc", 1.0f), nullptr)));
new TriggerNode("near npc status", NextAction::array(0, new NextAction("new rpg move npc", 3.0f), nullptr)));
triggers.push_back(
new TriggerNode("do quest status", NextAction::array(0, new NextAction("new rpg do quest", 3.0f), nullptr)));
}
void NewRpgStrategy::InitMultipliers(std::vector<Multiplier*>& multipliers)

View File

@@ -6,91 +6,12 @@
#ifndef _PLAYERBOT_NEWRPGSTRATEGY_H
#define _PLAYERBOT_NEWRPGSTRATEGY_H
#include <cstdint>
#include "Strategy.h"
#include "TravelMgr.h"
#include "NewRpgInfo.h"
class PlayerbotAI;
enum class NewRpgStatus
{
// Going to far away place
GO_GRIND,
GO_INNKEEPER,
// Exploring nearby
NEAR_RANDOM,
NEAR_NPC,
// Taking a break
REST,
// Initial status
IDLE
};
struct NewRpgInfo
{
NewRpgStatus status{NewRpgStatus::IDLE};
// NewRpgStatus::GO_GRIND
WorldPosition grindPos{};
uint32 lastGoGrind{0};
// NewRpgStatus::GO_INNKEEPER
WorldPosition innKeeperPos{};
uint32 lastGoInnKeeper{0};
// NewRpgStatus::NEAR_NPC
GuidPosition npcPos{};
uint32 lastNearNpc{0};
uint32 lastReachNpc{0};
// NewRpgStatus::NEAR_RANDOM
uint32 lastNearRandom{0};
// NewRpgStatus::REST
uint32 lastRest{0};
// MOVE_FAR
float nearestMoveFarDis{FLT_MAX};
uint32 stuckTs{0};
uint32 stuckAttempts{0};
std::string ToString()
{
std::stringstream out;
out << "Status: ";
switch (status)
{
case NewRpgStatus::GO_GRIND:
out << "GO_GRIND";
out << "\nGrindPos: " << grindPos.GetMapId() << " " << grindPos.GetPositionX() << " " << grindPos.GetPositionY() << " " << grindPos.GetPositionZ();
out << "\nlastGoGrind: " << lastGoGrind;
break;
case NewRpgStatus::GO_INNKEEPER:
out << "GO_INNKEEPER";
out << "\nInnKeeperPos: " << innKeeperPos.GetMapId() << " " << innKeeperPos.GetPositionX() << " " << innKeeperPos.GetPositionY() << " " << innKeeperPos.GetPositionZ();
out << "\nlastGoInnKeeper: " << lastGoInnKeeper;
break;
case NewRpgStatus::NEAR_NPC:
out << "NEAR_NPC";
out << "\nNpcPos: " << npcPos.GetMapId() << " " << npcPos.GetPositionX() << " " << npcPos.GetPositionY() << " " << npcPos.GetPositionZ();
out << "\nlastNearNpc: " << lastNearNpc;
out << "\nlastReachNpc: " << lastReachNpc;
break;
case NewRpgStatus::NEAR_RANDOM:
out << "NEAR_RANDOM";
out << "\nlastNearRandom: " << lastNearRandom;
break;
case NewRpgStatus::IDLE:
out << "IDLE";
break;
case NewRpgStatus::REST:
out << "REST";
out << "\nlastRest: " << lastRest;
break;
default:
out << "UNKNOWN";
}
return out.str();
}
void Reset()
{
*this = NewRpgInfo();
}
};
class NewRpgStrategy : public Strategy
{
public:

View File

@@ -7,7 +7,7 @@
class NewRpgStatusTrigger : public Trigger
{
public:
NewRpgStatusTrigger(PlayerbotAI* botAI, NewRpgStatus status = NewRpgStatus::IDLE)
NewRpgStatusTrigger(PlayerbotAI* botAI, NewRpgStatus status = RPG_IDLE)
: Trigger(botAI, "new rpg status"), status(status)
{
}