From 9917863ca1aa082ecf592f2c292215d526204026 Mon Sep 17 00:00:00 2001 From: Keleborn <22352763+Celandriel@users.noreply.github.com> Date: Sat, 27 Dec 2025 10:50:18 -0800 Subject: [PATCH] Feat. Add Fishing action and fish with master. (#1433) ### Update :Thank you to @notOrrytrout from prompting me to work on this. Its been a huge learning experience. With @notOrrytrout I started working on enabling bot fishing with master, but also on their own. The first commit didnt crash, showing that it was possible to have a bot cast when master does. Currently it compiles but crashes when you try to fish with a bot in the group, whether the bot has fishing or not. It makes me think that the check in FishingValues is broken somehow, but I cant figure out how. --------- Co-authored-by: bash <31279994+hermensbas@users.noreply.github.com> --- conf/playerbots.conf.dist | 19 + .../2025_12_27_ai_playerbot fishing text.sql | 15 + src/PlayerbotAIConfig.cpp | 5 + src/PlayerbotAIConfig.h | 4 + src/TravelMgr.cpp | 5 + src/TravelMgr.h | 1 + src/strategy/StrategyContext.h | 4 + src/strategy/actions/ActionContext.h | 11 + src/strategy/actions/EquipAction.cpp | 21 + src/strategy/actions/FishingAction.cpp | 492 ++++++++++++++++++ src/strategy/actions/FishingAction.h | 71 +++ src/strategy/actions/FollowActions.cpp | 2 + src/strategy/actions/SeeSpellAction.cpp | 12 + .../generic/LootNonCombatStrategy.cpp | 8 + src/strategy/generic/LootNonCombatStrategy.h | 9 + src/strategy/generic/NonCombatStrategy.cpp | 12 +- src/strategy/generic/NonCombatStrategy.h | 8 + .../generic/WorldPacketHandlerStrategy.cpp | 1 + src/strategy/triggers/FishingTriggers.cpp | 11 + src/strategy/triggers/FishingTriggers.h | 25 + src/strategy/triggers/TriggerContext.h | 5 + src/strategy/values/FishValues.cpp | 55 ++ src/strategy/values/FishValues.h | 47 ++ src/strategy/values/ValueContext.h | 8 + 24 files changed, 850 insertions(+), 1 deletion(-) create mode 100644 data/sql/playerbots/updates/2025_12_27_ai_playerbot fishing text.sql create mode 100644 src/strategy/actions/FishingAction.cpp create mode 100644 src/strategy/actions/FishingAction.h create mode 100644 src/strategy/triggers/FishingTriggers.cpp create mode 100644 src/strategy/triggers/FishingTriggers.h create mode 100644 src/strategy/values/FishValues.cpp create mode 100644 src/strategy/values/FishValues.h diff --git a/conf/playerbots.conf.dist b/conf/playerbots.conf.dist index 55cc97dd..4e17ea06 100644 --- a/conf/playerbots.conf.dist +++ b/conf/playerbots.conf.dist @@ -25,6 +25,7 @@ # CHEATS # SPELLS # FLIGHTPATH +# PROFESSIONS # RANDOMBOT-SPECIFIC SETTINGS # GENERAL # LEVELS @@ -579,6 +580,24 @@ AiPlayerbot.BotTaxiGapJitterMs = 100 # #################################################################################################### +#################################################################################################### +# PROFESSIONS +# Random bots currently do not get professions. +# + +# EnableFishingWithMaster automatically adds the 'master fishing' strategy to bots that can fish that can. +# Default: 1 (Enabled) +AiPlayerbot.EnableFishingWithMaster = 1 +#FishingDistance sets how far a bot without a master will search for water, while FishingDistanceFromMaster limits it to a closer range, and overrides the following distance to the same value. EndFishingWithMaster sets the distance from water a bot needs to have to automatically drop the 'master fishing' strategy. +AiPlayerbot.FishingDistanceFromMaster = 10.0 +AiPlayerbot.FishingDistance = 40.0 +AiPlayerbot.EndFishingWithMaster = 30.0 + +# +# +# +#################################################################################################### + ####################################### # # # RANDOMBOT-SPECIFIC SETTINGS # diff --git a/data/sql/playerbots/updates/2025_12_27_ai_playerbot fishing text.sql b/data/sql/playerbots/updates/2025_12_27_ai_playerbot fishing text.sql new file mode 100644 index 00000000..6fedd228 --- /dev/null +++ b/data/sql/playerbots/updates/2025_12_27_ai_playerbot fishing text.sql @@ -0,0 +1,15 @@ +DELETE FROM ai_playerbot_texts WHERE name IN ('no_fishing_pole_error'); +DELETE FROM ai_playerbot_texts_chance WHERE name IN ('no_fishing_pole_error'); + +INSERT INTO ai_playerbot_texts (id, name, text, say_type, reply_type, text_loc1, text_loc2, text_loc3, text_loc4, text_loc5, text_loc6, text_loc7, text_loc8) VALUES +(1736, 'no_fishing_pole_error', "I don't have a Fishing Pole", 0, 0, +"낚싯대가 없습니다", +"Je n’ai pas de canne à pêche", +"Ich habe keine Angelrute", +"我沒有釣魚竿", +"我没有钓鱼竿", +"No tengo una caña de pescar", +"No tengo una caña de pescar", +"У меня нет удочки"); + +INSERT INTO ai_playerbot_texts_chance (name, probability) VALUES ('no_fishing_pole_error', 100); diff --git a/src/PlayerbotAIConfig.cpp b/src/PlayerbotAIConfig.cpp index 6ceb9241..1b47c196 100644 --- a/src/PlayerbotAIConfig.cpp +++ b/src/PlayerbotAIConfig.cpp @@ -222,6 +222,11 @@ bool PlayerbotAIConfig::Initialize() EnableICCBuffs = sConfigMgr->GetOption("AiPlayerbot.EnableICCBuffs", true); + //////////////////////////// Professions + fishingDistanceFromMaster = sConfigMgr->GetOption("AiPlayerbot.FishingDistanceFromMaster", 10.0f); + endFishingWithMaster = sConfigMgr->GetOption("AiPlayerbot.EndFishingWithMaster", 30.0f); + fishingDistance = sConfigMgr->GetOption("AiPlayerbot.FishingDistance", 40.0f); + enableFishingWithMaster = sConfigMgr->GetOption("AiPlayerbot.EnableFishingWithMaster", true); //////////////////////////// CHAT enableBroadcasts = sConfigMgr->GetOption("AiPlayerbot.EnableBroadcasts", true); randomBotTalk = sConfigMgr->GetOption("AiPlayerbot.RandomBotTalk", false); diff --git a/src/PlayerbotAIConfig.h b/src/PlayerbotAIConfig.h index 83a6a20b..5013e412 100644 --- a/src/PlayerbotAIConfig.h +++ b/src/PlayerbotAIConfig.h @@ -145,6 +145,10 @@ public: // Cooldown (seconds) between reagent-missing RP warnings, per bot & per buff. Default: 30 int32 rpWarningCooldown; + // Professions + bool enableFishingWithMaster; + float fishingDistanceFromMaster, fishingDistance, endFishingWithMaster; + // chat bool randomBotTalk; bool randomBotEmote; diff --git a/src/TravelMgr.cpp b/src/TravelMgr.cpp index 8dfbbe45..043e5ab5 100644 --- a/src/TravelMgr.cpp +++ b/src/TravelMgr.cpp @@ -218,6 +218,11 @@ bool WorldPosition::isUnderWater() : false; }; +bool WorldPosition::IsValid() +{ + return !(GetMapId() == MAPID_INVALID && GetPositionX() == 0 && GetPositionY() == 0 && GetPositionZ() == 0); +} + WorldPosition WorldPosition::relPoint(WorldPosition* center) { return WorldPosition(GetMapId(), GetPositionX() - center->GetPositionX(), GetPositionY() - center->GetPositionY(), diff --git a/src/TravelMgr.h b/src/TravelMgr.h index c1c2ec0d..39a79a40 100644 --- a/src/TravelMgr.h +++ b/src/TravelMgr.h @@ -141,6 +141,7 @@ public: bool isOverworld(); bool isInWater(); bool isUnderWater(); + bool IsValid(); WorldPosition relPoint(WorldPosition* center); WorldPosition offset(WorldPosition* center); diff --git a/src/strategy/StrategyContext.h b/src/strategy/StrategyContext.h index e0a96797..0cc6855f 100644 --- a/src/strategy/StrategyContext.h +++ b/src/strategy/StrategyContext.h @@ -120,6 +120,8 @@ public: creators["formation"] = &StrategyContext::combat_formation; creators["move from group"] = &StrategyContext::move_from_group; creators["worldbuff"] = &StrategyContext::world_buff; + creators["use bobber"] = &StrategyContext::bobber_strategy; + creators["master fishing"] = &StrategyContext::master_fishing; } private: @@ -188,6 +190,8 @@ private: static Strategy* combat_formation(PlayerbotAI* botAI) { return new CombatFormationStrategy(botAI); } static Strategy* move_from_group(PlayerbotAI* botAI) { return new MoveFromGroupStrategy(botAI); } static Strategy* world_buff(PlayerbotAI* botAI) { return new WorldBuffStrategy(botAI); } + static Strategy* bobber_strategy(PlayerbotAI* botAI) { return new UseBobberStrategy(botAI); } + static Strategy* master_fishing(PlayerbotAI* botAI) { return new MasterFishingStrategy(botAI); } }; class MovementStrategyContext : public NamedObjectContext diff --git a/src/strategy/actions/ActionContext.h b/src/strategy/actions/ActionContext.h index 09c9145f..9f4e612e 100644 --- a/src/strategy/actions/ActionContext.h +++ b/src/strategy/actions/ActionContext.h @@ -64,6 +64,7 @@ #include "WorldBuffAction.h" #include "XpGainAction.h" #include "NewRpgAction.h" +#include "FishingAction.h" #include "CancelChannelAction.h" class PlayerbotAI; @@ -191,6 +192,11 @@ public: creators["buy tabard"] = &ActionContext::buy_tabard; creators["guild manage nearby"] = &ActionContext::guild_manage_nearby; creators["clean quest log"] = &ActionContext::clean_quest_log; + creators["move near water"] = &ActionContext::move_near_water; + creators["go fishing"] = &ActionContext::go_fishing; + creators["use fishing bobber"] = &ActionContext::use_fishing_bobber; + creators["end master fishing"] = &ActionContext::end_master_fishing; + creators["remove bobber strategy"] = &ActionContext::remove_bobber_strategy; creators["roll"] = &ActionContext::roll_action; creators["cancel channel"] = &ActionContext::cancel_channel; @@ -380,6 +386,11 @@ private: static Action* buy_tabard(PlayerbotAI* botAI) { return new BuyTabardAction(botAI); } static Action* guild_manage_nearby(PlayerbotAI* botAI) { return new GuildManageNearbyAction(botAI); } static Action* clean_quest_log(PlayerbotAI* botAI) { return new CleanQuestLogAction(botAI); } + static Action* move_near_water(PlayerbotAI* botAI) { return new MoveNearWaterAction(botAI); } + static Action* go_fishing(PlayerbotAI* botAI) { return new FishingAction(botAI);} + static Action* use_fishing_bobber(PlayerbotAI* botAI) { return new UseBobberAction(botAI);} + static Action* end_master_fishing(PlayerbotAI* botAI) { return new EndMasterFishingAction(botAI); } + static Action* remove_bobber_strategy(PlayerbotAI* botAI) { return new RemoveBobberStrategyAction(botAI); } static Action* roll_action(PlayerbotAI* botAI) { return new RollAction(botAI); } // BG Tactics diff --git a/src/strategy/actions/EquipAction.cpp b/src/strategy/actions/EquipAction.cpp index 71a2df01..9f4a67ca 100644 --- a/src/strategy/actions/EquipAction.cpp +++ b/src/strategy/actions/EquipAction.cpp @@ -344,6 +344,27 @@ bool EquipUpgradesAction::Execute(Event event) return false; } + if (event.GetSource() == "item push result") + { + WorldPacket p(event.getPacket()); + p.rpos(0); + ObjectGuid playerGuid; + uint32 received, created, sendChatMessage, itemSlot, itemId; + uint8 bagSlot; + + p >> playerGuid; + p >> received; + p >> created; + p >> sendChatMessage; + p >> bagSlot; + p >> itemSlot; + p >> itemId; + + ItemTemplate const* item = sObjectMgr->GetItemTemplate(itemId); + if (item->Class == ITEM_CLASS_TRADE_GOODS && item->SubClass == ITEM_SUBCLASS_MEAT) + return false; + } + CollectItemsVisitor visitor; IterateItems(&visitor, ITERATE_ITEMS_IN_BAGS); diff --git a/src/strategy/actions/FishingAction.cpp b/src/strategy/actions/FishingAction.cpp new file mode 100644 index 00000000..45228817 --- /dev/null +++ b/src/strategy/actions/FishingAction.cpp @@ -0,0 +1,492 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "FishingAction.h" +#include "FishValues.h" +#include "Event.h" + +#include "GridNotifiers.h" +#include "GridNotifiersImpl.h" +#include "ItemPackets.h" +#include "LastMovementValue.h" +#include "Map.h" +#include "MovementActions.h" +#include "Object.h" +#include "PlayerbotAI.h" +#include "PlayerbotTextMgr.h" +#include "Playerbots.h" +#include "Position.h" + +uint32 const FISHING_SPELL = 7620; +uint32 const FISHING_POLE = 6256; +uint32 const FISHING_BOBBER = 35591; +float const MIN_DISTANCE_TO_WATER = 10.0f; // Minimum spell distance +float const MAX_DISTANCE_TO_WATER = 20.0f; // Maximum spell distance +float const HEIGHT_ABOVE_WATER_TOLERANCE = 1.0f; // Can stand in up to 1 unit of water and still fish. +float const SEARCH_INCREMENT = 2.5f; +float const HEIGHT_SEARCH_BUFFER = 10.0f; // Height buffer to prevent potentially missing the model the bot is standing on. +float const SEARCH_LAND_BUFFER = 0.5f; +uint32 const FISHING_LOCATION_TIMEOUT = 180000; //Three minutes + +static bool IsFishingPole(Item* const item) +{ + if (!item) + return false; + const ItemTemplate* proto = item->GetTemplate(); + return proto && proto->Class == ITEM_CLASS_WEAPON && + proto->SubClass == ITEM_SUBCLASS_WEAPON_FISHING_POLE; +} + +float HasFishableWaterOrLand(float x, float y, float z, Map* map, uint32 phaseMask, bool checkForLand=false) +{ + if (!map) + return INVALID_HEIGHT; + + LiquidData const& liq = map->GetLiquidData(phaseMask, x, y, z+HEIGHT_ABOVE_WATER_TOLERANCE, DEFAULT_COLLISION_HEIGHT, MAP_ALL_LIQUIDS); + float ground = map->GetHeight(phaseMask, x, y, z + HEIGHT_SEARCH_BUFFER, true); + if (liq.Entry == MAP_LIQUID_TYPE_NO_WATER) + { + if (checkForLand) + return ground; + return INVALID_HEIGHT; + } + if (checkForLand) + { + if (ground > liq.Level - HEIGHT_ABOVE_WATER_TOLERANCE) + return ground; + return INVALID_HEIGHT; + } + + if (liq.Level + HEIGHT_ABOVE_WATER_TOLERANCE > ground) + { + if (abs(liq.DepthLevel) < 0.5f) // too shallow to fish in. + return INVALID_HEIGHT; + return liq.Level; + } + return INVALID_HEIGHT; +} + +bool HasLosToWater(Player* bot, float wx, float wy, float waterZ) +{ + float z = bot->GetCollisionHeight() + bot->GetPositionZ(); + return bot->GetMap()->isInLineOfSight( + bot->GetPositionX(), bot->GetPositionY(), z, + wx, wy, waterZ, + bot->GetPhaseMask(), + LINEOFSIGHT_ALL_CHECKS, + VMAP::ModelIgnoreFlags::Nothing); +} + +WorldPosition FindLandFromPosition(PlayerbotAI* botAI, float startDistance, float endDistance, float increment, float orientation, WorldPosition targetPos, float fishingSearchWindow, bool checkLOS = true) +{ + Player* bot = botAI->GetBot(); + Map* map = bot->GetMap(); + uint32 phaseMask = bot->GetPhaseMask(); + Player* master = botAI->GetMaster(); + + float targetX = targetPos.GetPositionX(); + float targetY = targetPos.GetPositionY(); + float targetZ = targetPos.GetPositionZ(); + + for (float dist = startDistance; dist <= endDistance; dist += increment) + { + //step backwards from position to bot to find edge of shore. + float checkX = targetX - dist * cos(orientation); + float checkY = targetY - dist * sin(orientation); + + float groundZ = map->GetHeight(phaseMask, checkX, checkY, targetZ + HEIGHT_SEARCH_BUFFER, true); + + if (groundZ == INVALID_HEIGHT) + continue; + + LiquidData const& liq = map->GetLiquidData(phaseMask, checkX, checkY, targetZ, DEFAULT_COLLISION_HEIGHT, MAP_ALL_LIQUIDS); + if (liq.Entry == MAP_LIQUID_TYPE_NO_WATER || groundZ > liq.DepthLevel + HEIGHT_ABOVE_WATER_TOLERANCE) + { + if (checkLOS) + { + bool hasLOS = map->isInLineOfSight(checkX, checkY, groundZ, targetX, targetY, targetZ, phaseMask, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing); + if (!hasLOS) + continue; + } + // Add a distance check for the position to prevent the bot from moving out of range to the master. + if (master && botAI->HasStrategy("follow", BOT_STATE_NON_COMBAT) && master->GetDistance(checkX, checkY, groundZ) > fishingSearchWindow - SEARCH_LAND_BUFFER) + continue; + + return WorldPosition(bot->GetMapId(), checkX, checkY, groundZ); + } + } + + return WorldPosition(); +} + +WorldPosition FindLandRadialFromPosition (PlayerbotAI* botAI, WorldPosition targetPos, float startDistance, float endDistance, float increment, float fishingSearchWindow, int angles = 16) +{ + Player* bot = botAI->GetBot(); + const int numDirections = angles; + std::vector boundaryPoints; + Player* master = botAI->GetMaster(); + if (!master) + return WorldPosition(); + + Map* map = bot->GetMap(); + uint32 phaseMask = bot->GetPhaseMask(); + + float targetX = targetPos.GetPositionX(); + float targetY = targetPos.GetPositionY(); + float targetZ = targetPos.GetPositionZ(); + + for (float dist = startDistance; dist <= endDistance; dist += increment) + { + for (int i = 0; i < numDirections; ++i) + { + float angle = (2.0f * M_PI * i) / numDirections; + float checkX = targetX - cos(angle) * dist; + float checkY = targetY - sin(angle) * dist; + + float groundZ = HasFishableWaterOrLand(checkX, checkY, targetZ, map, phaseMask, true); + + if (groundZ == INVALID_HEIGHT) + continue; + + if (map->isInLineOfSight(checkX, checkY, groundZ, targetX, targetY, targetZ, phaseMask, LINEOFSIGHT_ALL_CHECKS, VMAP::ModelIgnoreFlags::Nothing) && master->GetDistance(checkX, checkY, groundZ) > fishingSearchWindow - SEARCH_LAND_BUFFER) + continue; + + boundaryPoints.emplace_back(WorldPosition(bot->GetMapId(), checkX, checkY, groundZ)); + } + + if (!boundaryPoints.empty()) + break; + } + + if (boundaryPoints.empty()) + return WorldPosition(); + + if (boundaryPoints.size() == 1) + return boundaryPoints[0]; + + float minDistance = FLT_MAX; + WorldLocation closestPoint = WorldPosition(); + for (auto const& pos : boundaryPoints) + { + float distance = bot->GetExactDist2d(&pos); + if (distance < minDistance) + { + minDistance = distance; + closestPoint = pos; + } + } + return closestPoint; +} + +WorldPosition FindWaterRadial(Player* bot, float x, float y, float z, Map* map, uint32 phaseMask, float minDistance, float maxDistance, float increment, bool checkLOS, int numDirections) +{ + std::vector boundaryPoints; + + float dist = minDistance; + while (dist <= maxDistance) + { + for (int i = 0; i < numDirections; ++i) + { + float angle = (2.0f * M_PI * i) / numDirections; + float checkX = x + cos(angle) * dist; + float checkY = y + sin(angle) * dist; + + float waterZ = HasFishableWaterOrLand(checkX, checkY, z, map, phaseMask); + + if (waterZ == INVALID_HEIGHT) + continue; + + if (checkLOS && !HasLosToWater(bot, checkX, checkY, waterZ)) + continue; + + boundaryPoints.emplace_back(WorldPosition(bot->GetMapId(), checkX, checkY, waterZ)); + } + + if (!boundaryPoints.empty()) + break; + + dist += increment; + } + + if (boundaryPoints.empty()) + return WorldPosition(); + + if (boundaryPoints.size() == 1) + return boundaryPoints[0]; + // return the central point in the identified positions in to try to be perpendicular to the shore. + return boundaryPoints[boundaryPoints.size() / 2]; +} + +WorldPosition FindFishingHole(PlayerbotAI* botAI) +{ + Player* player = botAI->GetBot(); + GuidVector gos = PAI_VALUE(GuidVector, "nearest game objects no los"); + GameObject* nearestFishingHole = nullptr; + float minDist = std::numeric_limits::max(); + for (auto const& guid : gos) + { + GameObject* go = botAI->GetGameObject(guid); + if (!go) + continue; + if (go->GetGoType() == GAMEOBJECT_TYPE_FISHINGHOLE) + { + float dist = player->GetDistance2d(go); + if (dist < minDist) + { + minDist = dist; + nearestFishingHole = go; + } + } + } + if (nearestFishingHole) + return WorldPosition(nearestFishingHole->GetMapId(), nearestFishingHole->GetPositionX(), nearestFishingHole->GetPositionY(), nearestFishingHole->GetPositionZ()); + + return WorldPosition(); +} + +bool MoveNearWaterAction::Execute(Event event) +{ + WorldPosition landSpot = AI_VALUE(WorldPosition, "fishing spot"); + if (landSpot.IsValid()) + return MoveTo(landSpot.GetMapId(), landSpot.GetPositionX(), landSpot.GetPositionY(), landSpot.GetPositionZ()); + + return false; +} + +bool MoveNearWaterAction::isUseful() +{ + if (!AI_VALUE(bool, "can fish")) + return false; + FishingSpotValue* fishingSpotValueObject = (FishingSpotValue*)context->GetValue("fishing spot"); + WorldPosition pos = fishingSpotValueObject->Get(); + return !pos.IsValid() || fishingSpotValueObject->IsStale(FISHING_LOCATION_TIMEOUT) || pos != bot->GetPosition(); +} + +bool MoveNearWaterAction::isPossible() +{ + Player* master = botAI->GetMaster(); + float fishingSearchWindow; + + if (master) + fishingSearchWindow = sPlayerbotAIConfig->fishingDistanceFromMaster; + else + fishingSearchWindow = sPlayerbotAIConfig->fishingDistance; + + WorldPosition fishingHole = FindFishingHole(botAI); + + if (fishingHole.IsValid()) + { + float distance = bot->GetExactDist2d(&fishingHole); + bool hasLOS = bot->IsWithinLOS(fishingHole.GetPositionX(), fishingHole.GetPositionY(), fishingHole.GetPositionZ()); + // Water spot is in range, and we have LOS to it. Set bot position to fishing spot and do not move + if (distance >= MIN_DISTANCE_TO_WATER && + distance <= MAX_DISTANCE_TO_WATER && hasLOS) + { + SET_AI_VALUE(WorldPosition, "fishing spot", WorldPosition(WorldPosition(bot->GetMapId(), bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ()))); + return false; + } + // Water spot is out of range, lets look for a spot to move to for the fishing hole. + if (distance > MAX_DISTANCE_TO_WATER || distance < MIN_DISTANCE_TO_WATER) + { + float angle = bot->GetAngle(fishingHole.GetPositionX(), fishingHole.GetPositionY()); + WorldPosition landSpot = FindLandRadialFromPosition(botAI, fishingHole, MIN_DISTANCE_TO_WATER, MAX_DISTANCE_TO_WATER, SEARCH_INCREMENT, fishingSearchWindow, 32); + if (landSpot.IsValid()) + { + SET_AI_VALUE(WorldPosition, "fishing spot", landSpot); + return true; + } + } + } + // Lets find some water where we can fish. + WorldPosition water = FindWaterRadial( + bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), + bot->GetMap(), bot->GetPhaseMask(), + MIN_DISTANCE_TO_WATER, + fishingSearchWindow + MAX_DISTANCE_TO_WATER, + SEARCH_INCREMENT, false); + + if (!water.IsValid()) + return false; + + bool hasLOS = bot->IsWithinLOS(water.GetPositionX(), water.GetPositionY(), water.GetPositionZ()); + float angle = bot->GetAngle(water.GetPositionX(), water.GetPositionY()); + WorldPosition landSpot = + FindLandFromPosition(botAI, 0.0f, MAX_DISTANCE_TO_WATER, 1.0f, angle, water, fishingSearchWindow, false); + + if (landSpot.IsValid()) + { + SET_AI_VALUE(WorldPosition, "fishing spot", landSpot); + return true; + } + return false; +} + +bool EquipFishingPoleAction::Execute(Event event) +{ + if (!_pole) + return false; + + WorldPacket eqPacket(CMSG_AUTOEQUIP_ITEM_SLOT, 2); + eqPacket << _pole->GetGUID() << uint8(EQUIPMENT_SLOT_MAINHAND); + WorldPackets::Item::AutoEquipItemSlot nicePacket(std::move(eqPacket)); + nicePacket.Read(); + bot->GetSession()->HandleAutoEquipItemSlotOpcode(nicePacket); + return true; +} + +bool EquipFishingPoleAction::isUseful() +{ + Item* mainHand = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, EQUIPMENT_SLOT_MAINHAND); + if (IsFishingPole(mainHand)) + return false; + + for (uint8 slot = INVENTORY_SLOT_ITEM_START; slot < INVENTORY_SLOT_ITEM_END; ++slot) + { + if (Item* item = bot->GetItemByPos(INVENTORY_SLOT_BAG_0, slot)) + { + if (IsFishingPole(item)) + { + _pole = item; + return true; + } + } + } + + for (uint8 bag = INVENTORY_SLOT_BAG_START; bag < INVENTORY_SLOT_BAG_END; ++bag) + { + if (Bag* pBag = bot->GetBagByPos(bag)) + { + for (uint32 j = 0; j < pBag->GetBagSize(); ++j) + { + if (Item* item = pBag->GetItemByPos(j)) + { + if (IsFishingPole(item)) + { + _pole = item; + return true; + } + } + } + } + } + + if (sRandomPlayerbotMgr->IsRandomBot(bot)) + { + bot->StoreNewItemInBestSlots(FISHING_POLE, 1); // Try to get a fishing pole + return true; + } + + Player* master = botAI->GetMaster(); + if (!master) + return false; + + std::string masterName = master->GetName(); + std::string text = sPlayerbotTextMgr->GetBotTextOrDefault( + "no_fishing_pole_error", "I don't have a Fishing Pole",{}); + botAI->Whisper(text, masterName); + + return false; +} + +bool FishingAction::Execute(Event event) +{ + WorldPosition target = WorldPosition(); + WorldPosition fishingHole = FindFishingHole(botAI); + if (fishingHole.IsValid()) + { + Position pos = fishingHole; + float distance = bot->GetExactDist2d(&pos); + bool hasLOS = bot->IsWithinLOS(fishingHole.GetPositionX(), fishingHole.GetPositionY(), fishingHole.GetPositionZ()); + if (distance < MAX_DISTANCE_TO_WATER && + distance > MIN_DISTANCE_TO_WATER && hasLOS) + target = fishingHole; + } + if (!target.IsValid()) + { + target = FindWaterRadial(bot, bot->GetPositionX(), bot->GetPositionY(), + bot->GetPositionZ(), bot->GetMap(), bot->GetPhaseMask(), + MIN_DISTANCE_TO_WATER, MAX_DISTANCE_TO_WATER, SEARCH_INCREMENT, true, 32); + if (!target.IsValid()) + return false; + } + Position pos = target; + + if (!bot->HasInArc(1.0, &pos, 1.0)) + { + float angle = bot->GetAngle(pos.GetPositionX(), pos.GetPositionY()); + bot->SetOrientation(angle); + bot->SendMovementFlagUpdate(); + } + + EquipFishingPoleAction equipAction(botAI); + if (equipAction.isUseful()) + return equipAction.Execute(event); + + botAI->CastSpell(FISHING_SPELL, bot); + botAI->ChangeStrategy("+use bobber", BOT_STATE_NON_COMBAT); + + return true; +} + +bool FishingAction::isUseful() +{ + if (!AI_VALUE(bool, "can fish")) + return false; + FishingSpotValue* fishingSpotValueObject = (FishingSpotValue*)context->GetValue("fishing spot"); + WorldPosition pos = fishingSpotValueObject->Get(); + + return pos.IsValid() && !fishingSpotValueObject->IsStale(FISHING_LOCATION_TIMEOUT) && pos == bot->GetPosition(); +} + +bool UseBobberAction::isUseful() +{ + return AI_VALUE(bool, "can use fishing bobber"); +} + +bool UseBobberAction::Execute(Event event) +{ + GuidVector gos = AI_VALUE(GuidVector, "nearest game objects no los"); + for (auto const& guid : gos) + { + if (GameObject* go = botAI->GetGameObject(guid)) + { + if (go->GetEntry() != FISHING_BOBBER) + continue; + if (go->GetOwnerGUID() != bot->GetGUID()) + continue; + if (go->getLootState() == GO_READY) + { + go->Use(bot); + botAI->ChangeStrategy("-use bobber", BOT_STATE_NON_COMBAT); + return true; + } + } + } + return false; +} + +bool EndMasterFishingAction::Execute(Event event) +{ + botAI->ChangeStrategy("-master fishing", BOT_STATE_NON_COMBAT); + return true; +} + +bool EndMasterFishingAction::isUseful() +{ + FishingSpotValue* fishingSpotValueObject = (FishingSpotValue*)context->GetValue("fishing spot"); + WorldPosition pos = fishingSpotValueObject->Get(); + if (pos.IsValid() && !fishingSpotValueObject->IsStale(FISHING_LOCATION_TIMEOUT) && pos == bot->GetPosition()) + return false; + + WorldPosition nearWater = FindWaterRadial(bot, bot->GetPositionX(), bot->GetPositionY(), bot->GetPositionZ(), + bot->GetMap(), bot->GetPhaseMask(), MIN_DISTANCE_TO_WATER, sPlayerbotAIConfig->endFishingWithMaster, 10.0f); + return !nearWater.IsValid(); +} + +bool RemoveBobberStrategyAction::Execute(Event event) +{ + botAI->ChangeStrategy("-use bobber", BOT_STATE_NON_COMBAT); + return true; +} diff --git a/src/strategy/actions/FishingAction.h b/src/strategy/actions/FishingAction.h new file mode 100644 index 00000000..407825ed --- /dev/null +++ b/src/strategy/actions/FishingAction.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_FISHINGACTION_H +#define _PLAYERBOT_FISHINGACTION_H + +#include "Action.h" +#include "MovementActions.h" +#include "Event.h" +#include "Playerbots.h" + +extern const uint32 FISHING_SPELL; +extern const uint32 FISHING_POLE; +extern const uint32 FISHING_BOBBER; + +WorldPosition FindWaterRadial(Player* bot, float x, float y, float z, Map* map, uint32 phaseMask, float minDistance, float maxDistance, float increment, bool checkLOS=false, int numDirections = 16); + +class PlayerbotAI; + +class FishingAction : public Action +{ +public: + FishingAction(PlayerbotAI* botAI) : Action(botAI, "go fishing"){} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class EquipFishingPoleAction : public Action +{ +public: + EquipFishingPoleAction(PlayerbotAI* botAI) : Action(botAI, "equip fishing pole") {} + bool Execute(Event event) override; + bool isUseful() override; +private: + Item* _pole = nullptr; +}; + +class MoveNearWaterAction : public MovementAction +{ +public: + MoveNearWaterAction(PlayerbotAI* botAI): MovementAction(botAI, "move near water") {} + bool Execute(Event event) override; + bool isUseful() override; + bool isPossible() override; +}; + +class UseBobberAction : public Action +{ +public: + UseBobberAction(PlayerbotAI* botAI) : Action(botAI, "use fishing bobber") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class EndMasterFishingAction : public Action +{ +public: + EndMasterFishingAction(PlayerbotAI* botAI) : Action(botAI, "end master fishing") {} + bool Execute(Event event) override; + bool isUseful() override; +}; + +class RemoveBobberStrategyAction : public Action +{ +public: + RemoveBobberStrategyAction(PlayerbotAI* botAI) : Action(botAI, "remove bobber strategy") {} + bool Execute(Event event) override; +}; +#endif diff --git a/src/strategy/actions/FollowActions.cpp b/src/strategy/actions/FollowActions.cpp index d168e8cc..2593ea28 100644 --- a/src/strategy/actions/FollowActions.cpp +++ b/src/strategy/actions/FollowActions.cpp @@ -97,6 +97,8 @@ bool FollowAction::isUseful() distance = bot->GetDistance(loc.GetPositionX(), loc.GetPositionY(), loc.GetPositionZ()); } + if (botAI->HasStrategy("master fishing", BOT_STATE_NON_COMBAT)) + return sServerFacade->IsDistanceGreaterThan(distance, sPlayerbotAIConfig->fishingDistanceFromMaster); return sServerFacade->IsDistanceGreaterThan(distance, formation->GetMaxDistance()); } diff --git a/src/strategy/actions/SeeSpellAction.cpp b/src/strategy/actions/SeeSpellAction.cpp index f42dcade..88848ca8 100644 --- a/src/strategy/actions/SeeSpellAction.cpp +++ b/src/strategy/actions/SeeSpellAction.cpp @@ -14,6 +14,8 @@ #include "PositionValue.h" #include "ByteBuffer.h" +std::set const FISHING_SPELLS = {7620, 7731, 7732, 18248, 33095, 51294}; + Creature* SeeSpellAction::CreateWps(Player* wpOwner, float x, float y, float z, float o, uint32 entry, Creature* lastWp, bool important) { @@ -57,6 +59,16 @@ bool SeeSpellAction::Execute(Event event) // if (!botAI->HasStrategy("RTSC", botAI->GetState())) // return false; + if (FISHING_SPELLS.find(spellId) != FISHING_SPELLS.end()) + { + if (AI_VALUE(bool, "can fish") && sPlayerbotAIConfig->enableFishingWithMaster) + { + botAI->ChangeStrategy("+master fishing", BOT_STATE_NON_COMBAT); + return true; + } + return false; + } + if (spellId != RTSC_MOVE_SPELL) return false; diff --git a/src/strategy/generic/LootNonCombatStrategy.cpp b/src/strategy/generic/LootNonCombatStrategy.cpp index 324243a1..caf901b8 100644 --- a/src/strategy/generic/LootNonCombatStrategy.cpp +++ b/src/strategy/generic/LootNonCombatStrategy.cpp @@ -27,3 +27,11 @@ void RevealStrategy::InitTriggers(std::vector& triggers) triggers.push_back( new TriggerNode("often", NextAction::array(0, new NextAction("reveal gathering item", 50.0f), nullptr))); } + +void UseBobberStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back( + new TriggerNode("can use fishing bobber", NextAction::array(0, new NextAction("use fishing bobber", 20.0f), nullptr))); + triggers.push_back( + new TriggerNode("random", NextAction::array(0, new NextAction("remove bobber strategy", 20.0f), nullptr))); +} diff --git a/src/strategy/generic/LootNonCombatStrategy.h b/src/strategy/generic/LootNonCombatStrategy.h index 2de1d776..fef564f8 100644 --- a/src/strategy/generic/LootNonCombatStrategy.h +++ b/src/strategy/generic/LootNonCombatStrategy.h @@ -37,4 +37,13 @@ public: std::string const getName() override { return "reveal"; } }; +class UseBobberStrategy : public Strategy +{ +public: + UseBobberStrategy(PlayerbotAI* botAI) : Strategy(botAI){} + uint32 GetType() const override { return STRATEGY_TYPE_NONCOMBAT; } + void InitTriggers(std::vector& triggers) override; + std::string const getName() override {return "use bobber";} +}; + #endif diff --git a/src/strategy/generic/NonCombatStrategy.cpp b/src/strategy/generic/NonCombatStrategy.cpp index 7363f8c6..09a24c67 100644 --- a/src/strategy/generic/NonCombatStrategy.cpp +++ b/src/strategy/generic/NonCombatStrategy.cpp @@ -35,5 +35,15 @@ void MountStrategy::InitTriggers(std::vector& triggers) void WorldBuffStrategy::InitTriggers(std::vector& triggers) { - triggers.push_back(new TriggerNode("need world buff", NextAction::array(0, new NextAction("world buff", 1.0f), NULL))); + triggers.push_back(new TriggerNode("need world buff", NextAction::array(0, new NextAction("world buff", 1.0f), nullptr))); +} + +void MasterFishingStrategy::InitTriggers(std::vector& triggers) +{ + triggers.push_back(new TriggerNode("very often", NextAction::array(0, new NextAction("move near water" , 10.0f), nullptr))); + + triggers.push_back(new TriggerNode("very often", NextAction::array(0, new NextAction("go fishing" , 10.0f), nullptr))); + + triggers.push_back(new TriggerNode("random", NextAction::array(0, new NextAction("end master fishing", 12.0f), + new NextAction("equip upgrades", 6.0f), nullptr))); } diff --git a/src/strategy/generic/NonCombatStrategy.h b/src/strategy/generic/NonCombatStrategy.h index ca4d3fd2..18549ac1 100644 --- a/src/strategy/generic/NonCombatStrategy.h +++ b/src/strategy/generic/NonCombatStrategy.h @@ -58,4 +58,12 @@ public: std::string const getName() override { return "worldbuff"; } }; +class MasterFishingStrategy : public Strategy +{ +public: + MasterFishingStrategy(PlayerbotAI* botAI) : Strategy(botAI){} + uint32 GetType() const override { return STRATEGY_TYPE_NONCOMBAT; } + void InitTriggers(std::vector& triggers) override; + std::string const getName() override {return "master fishing";} +}; #endif diff --git a/src/strategy/generic/WorldPacketHandlerStrategy.cpp b/src/strategy/generic/WorldPacketHandlerStrategy.cpp index 97b1ba1b..e09408c2 100644 --- a/src/strategy/generic/WorldPacketHandlerStrategy.cpp +++ b/src/strategy/generic/WorldPacketHandlerStrategy.cpp @@ -58,6 +58,7 @@ void WorldPacketHandlerStrategy::InitTriggers(std::vector& trigger // relevance), nullptr))); triggers.push_back(new TriggerNode("group list", NextAction::array(0, new NextAction("reset botAI", relevance), nullptr))); triggers.push_back(new TriggerNode("see spell", NextAction::array(0, new NextAction("see spell", relevance), nullptr))); + triggers.push_back(new TriggerNode("release spirit", NextAction::array(0, new NextAction("release", relevance), nullptr))); triggers.push_back(new TriggerNode("revive from corpse", NextAction::array(0, new NextAction("revive from corpse", relevance), nullptr))); triggers.push_back(new TriggerNode("master loot roll", NextAction::array(0, new NextAction("master loot roll", relevance), nullptr))); diff --git a/src/strategy/triggers/FishingTriggers.cpp b/src/strategy/triggers/FishingTriggers.cpp new file mode 100644 index 00000000..dae87757 --- /dev/null +++ b/src/strategy/triggers/FishingTriggers.cpp @@ -0,0 +1,11 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "FishingTriggers.h" +#include "Playerbots.h" + +bool CanFishTrigger::IsActive() { return AI_VALUE(bool, "can fish"); } + +bool CanUseFishingBobberTrigger::IsActive() { return AI_VALUE(bool, "can use fishing bobber");} diff --git a/src/strategy/triggers/FishingTriggers.h b/src/strategy/triggers/FishingTriggers.h new file mode 100644 index 00000000..ed3c6962 --- /dev/null +++ b/src/strategy/triggers/FishingTriggers.h @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_FISHING_TRIGGER_H +#define _PLAYERBOT_FISHING_TRIGGER_H + +#include "GenericTriggers.h" + +class CanFishTrigger : public Trigger +{ +public: + CanFishTrigger(PlayerbotAI* ai) : Trigger(ai, "can fish") {}; + bool IsActive() override; +}; + +class CanUseFishingBobberTrigger : public Trigger +{ +public: + CanUseFishingBobberTrigger(PlayerbotAI* ai) : Trigger(ai, "can use fishing bobber") {}; + bool IsActive() override; +}; + +#endif diff --git a/src/strategy/triggers/TriggerContext.h b/src/strategy/triggers/TriggerContext.h index 1c829ae9..1e8abf94 100644 --- a/src/strategy/triggers/TriggerContext.h +++ b/src/strategy/triggers/TriggerContext.h @@ -7,6 +7,7 @@ #define _PLAYERBOT_TRIGGERCONTEXT_H #include "CureTriggers.h" +#include "FishingTriggers.h" #include "GenericTriggers.h" #include "GuildTriggers.h" #include "LfgTriggers.h" @@ -226,6 +227,8 @@ public: creators["do quest status"] = &TriggerContext::do_quest_status; creators["travel flight status"] = &TriggerContext::travel_flight_status; creators["can self resurrect"] = &TriggerContext::can_self_resurrect; + creators["can fish"] = &TriggerContext::can_fish; + creators["can use fishing bobber"] = &TriggerContext::can_use_fishing_bobber; creators["new pet"] = &TriggerContext::new_pet; } @@ -425,6 +428,8 @@ private: static Trigger* do_quest_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_DO_QUEST); } static Trigger* travel_flight_status(PlayerbotAI* botAI) { return new NewRpgStatusTrigger(botAI, RPG_TRAVEL_FLIGHT); } static Trigger* can_self_resurrect(PlayerbotAI* ai) { return new SelfResurrectTrigger(ai); } + static Trigger* can_fish(PlayerbotAI* ai) { return new CanFishTrigger(ai); } + static Trigger* can_use_fishing_bobber(PlayerbotAI* ai) { return new CanUseFishingBobberTrigger(ai); } static Trigger* new_pet(PlayerbotAI* ai) { return new NewPetTrigger(ai); } }; diff --git a/src/strategy/values/FishValues.cpp b/src/strategy/values/FishValues.cpp new file mode 100644 index 00000000..18b30467 --- /dev/null +++ b/src/strategy/values/FishValues.cpp @@ -0,0 +1,55 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#include "FishValues.h" +#include "PlayerbotAI.h" +#include "RandomPlayerbotMgr.h" +#include "Map.h" +#include "Spell.h" +#include "FishingAction.h" + +bool CanFishValue::Calculate() +{ + int32 SkillFishing = bot->GetSkillValue(SKILL_FISHING); + + if (SkillFishing == 0) + return false; + + if (bot->isSwimming()) + return false; + + if (bot->IsInCombat()) + return false; + + return true; +} + +bool CanUseFishingBobberValue::Calculate() +{ + GuidVector gos = AI_VALUE(GuidVector, "nearest game objects no los"); + for (auto const& guid : gos) + { + if (GameObject* go = botAI->GetGameObject(guid)) + { + if (go->GetEntry() != FISHING_BOBBER) + continue; + if (go->GetOwnerGUID() != bot->GetGUID()) + continue; + + if (go->getLootState() == GO_READY) + return true; + + // Not ready yet → delay next check + time_t bobberActiveTime = go->GetRespawnTime() - FISHING_BOBBER_READY_TIME; + if (bobberActiveTime > time(0)) + botAI->SetNextCheckDelay((bobberActiveTime - time(0)) * IN_MILLISECONDS + 500); + else + botAI->SetNextCheckDelay(1000); + + return false; + } + } + return false; +} diff --git a/src/strategy/values/FishValues.h b/src/strategy/values/FishValues.h new file mode 100644 index 00000000..4304d6f4 --- /dev/null +++ b/src/strategy/values/FishValues.h @@ -0,0 +1,47 @@ +/* + * Copyright (C) 2016+ AzerothCore , released under GNU AGPL v3 license, you may redistribute it + * and/or modify it under version 3 of the License, or (at your option), any later version. + */ + +#ifndef _PLAYERBOT_FISHVALUES_H +#define _PLAYERBOT_FISHVALUES_H + +#include "Value.h" +#include "TravelMgr.h" +#include "NamedObjectContext.h" + +class PlayerbotAI; + +class CanFishValue : public BoolCalculatedValue +{ +public: + CanFishValue(PlayerbotAI* botAI) : BoolCalculatedValue(botAI, "can fish") {}; + bool Calculate() override; +}; + +class CanUseFishingBobberValue : public BoolCalculatedValue +{ +public: + CanUseFishingBobberValue(PlayerbotAI* botAI) : BoolCalculatedValue(botAI, "can use fishing bobber") {}; + bool Calculate() override; +}; + +class FishingSpotValue : public ManualSetValue +{ +public: + FishingSpotValue(PlayerbotAI* botAI, WorldPosition const& pos = WorldPosition(), std::string const& name = "fishing spot") + : ManualSetValue(botAI, pos, name) {} + + void Set(WorldPosition val) override + { + value = val; + _setTime = getMSTime(); + } + uint32 lastUpdated() const {return _setTime;} + bool IsStale(uint32 maxDuration) const { return getMSTime() - _setTime > maxDuration; } + +private: + uint32 _setTime = 0; +}; + +#endif diff --git a/src/strategy/values/ValueContext.h b/src/strategy/values/ValueContext.h index adf03be8..7349e9d6 100644 --- a/src/strategy/values/ValueContext.h +++ b/src/strategy/values/ValueContext.h @@ -27,6 +27,7 @@ #include "EnemyHealerTargetValue.h" #include "EnemyPlayerValue.h" #include "EstimatedLifetimeValue.h" +#include "FishValues.h" #include "Formations.h" #include "GrindTargetValue.h" #include "GroupValues.h" @@ -312,6 +313,10 @@ public: creators["last flee angle"] = &ValueContext::last_flee_angle; creators["last flee timestamp"] = &ValueContext::last_flee_timestamp; creators["recently flee info"] = &ValueContext::recently_flee_info; + + creators["can fish"] = &ValueContext::can_fish; + creators["can use fishing bobber"] = &ValueContext::can_use_fishing_bobber; + creators["fishing spot"] = &ValueContext::fishing_spot; } private: @@ -555,6 +560,9 @@ private: static UntypedValue* last_flee_angle(PlayerbotAI* ai) { return new LastFleeAngleValue(ai); } static UntypedValue* last_flee_timestamp(PlayerbotAI* ai) { return new LastFleeTimestampValue(ai); } static UntypedValue* recently_flee_info(PlayerbotAI* ai) { return new RecentlyFleeInfo(ai); } + static UntypedValue* can_fish(PlayerbotAI* ai) { return new CanFishValue(ai); } + static UntypedValue* can_use_fishing_bobber(PlayerbotAI* ai) { return new CanUseFishingBobberValue(ai); } + static UntypedValue* fishing_spot(PlayerbotAI* ai) { return new FishingSpotValue(ai); } // ------------------------------------------------------- // Flag for cutom glyphs : true when /w bot glyph equip // -------------------------------------------------------