diff --git a/AI/Nullkiller/AIGateway.cpp b/AI/Nullkiller/AIGateway.cpp index c648b0129..bee760d7d 100644 --- a/AI/Nullkiller/AIGateway.cpp +++ b/AI/Nullkiller/AIGateway.cpp @@ -1311,6 +1311,11 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h) auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos) { + if(cb->getObj(exitId) && cb->getObj(exitId)->ID == Obj::WHIRLPOOL) + { + nullkiller->armyFormation->rearrangeArmyForWhirlpool(*h); + } + destinationTeleport = exitId; if(exitPos.valid()) destinationTeleportPos = exitPos; @@ -1332,6 +1337,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h) status.setChannelProbing(true); for(auto exit : teleportChannelProbingList) doTeleportMovement(exit, int3(-1)); + teleportChannelProbingList.clear(); status.setChannelProbing(false); diff --git a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp index b088a86a4..0aa077688 100644 --- a/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp +++ b/AI/Nullkiller/Behaviors/ExplorationBehavior.cpp @@ -47,10 +47,13 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const } case Obj::MONOLITH_ONE_WAY_ENTRANCE: case Obj::MONOLITH_TWO_WAY: - case Obj::SUBTERRANEAN_GATE: + case Obj::SUBTERRANEAN_GATE: + case Obj::WHIRLPOOL: { - auto tObj = dynamic_cast(obj); - for (auto exit : cb->getTeleportChannelExits(tObj->channel)) + auto tObj = dynamic_cast(obj); + if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability) + break; + for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits) { if (exit != tObj->id) { diff --git a/AI/Nullkiller/CMakeLists.txt b/AI/Nullkiller/CMakeLists.txt index 8ea2634b4..7d9f272a1 100644 --- a/AI/Nullkiller/CMakeLists.txt +++ b/AI/Nullkiller/CMakeLists.txt @@ -8,6 +8,7 @@ set(Nullkiller_SRCS Pathfinding/Actions/QuestAction.cpp Pathfinding/Actions/BuyArmyAction.cpp Pathfinding/Actions/BoatActions.cpp + Pathfinding/Actions/WhirlpoolAction.cpp Pathfinding/Actions/TownPortalAction.cpp Pathfinding/Actions/AdventureSpellCastMovementActions.cpp Pathfinding/Rules/AILayerTransitionRule.cpp @@ -79,6 +80,7 @@ set(Nullkiller_HEADERS Pathfinding/Actions/QuestAction.h Pathfinding/Actions/BuyArmyAction.h Pathfinding/Actions/BoatActions.h + Pathfinding/Actions/WhirlpoolAction.h Pathfinding/Actions/TownPortalAction.h Pathfinding/Actions/AdventureSpellCastMovementActions.h Pathfinding/Rules/AILayerTransitionRule.h diff --git a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp index 95b5ca725..8fe4851b2 100644 --- a/AI/Nullkiller/Goals/ExecuteHeroChain.cpp +++ b/AI/Nullkiller/Goals/ExecuteHeroChain.cpp @@ -196,6 +196,26 @@ void ExecuteHeroChain::accept(AIGateway * ai) } } + auto findWhirlpool = [&ai](const int3 & pos) -> ObjectInstanceID + { + auto objs = ai->myCb->getVisitableObjs(pos); + auto whirlpool = std::find_if(objs.begin(), objs.end(), [](const CGObjectInstance * o)->bool + { + return o->ID == Obj::WHIRLPOOL; + }); + + return whirlpool != objs.end() ? dynamic_cast(*whirlpool)->id : ObjectInstanceID(-1); + }; + + auto sourceWhirlpool = findWhirlpool(hero->visitablePos()); + auto targetWhirlpool = findWhirlpool(node->coord); + + if(i != chainPath.nodes.size() - 1 && sourceWhirlpool.hasValue() && sourceWhirlpool == targetWhirlpool) + { + logAi->trace("AI exited whirlpool at %s but expected at %s", hero->visitablePos().toString(), node->coord.toString()); + continue; + } + if(hero->movementPointsRemaining()) { try diff --git a/AI/Nullkiller/Helpers/ArmyFormation.cpp b/AI/Nullkiller/Helpers/ArmyFormation.cpp index 464cb10d0..26d8c7d40 100644 --- a/AI/Nullkiller/Helpers/ArmyFormation.cpp +++ b/AI/Nullkiller/Helpers/ArmyFormation.cpp @@ -14,27 +14,37 @@ namespace NKAI { -void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker) +void ArmyFormation::rearrangeArmyForWhirlpool(const CGHeroInstance * hero) { - auto freeSlots = attacker->getFreeSlotsQueue(); + addSingleCreatureStacks(hero); +} + +void ArmyFormation::addSingleCreatureStacks(const CGHeroInstance * hero) +{ + auto freeSlots = hero->getFreeSlotsQueue(); while(!freeSlots.empty()) { - auto weakestCreature = vstd::minElementByFun(attacker->Slots(), [](const std::pair & slot) -> int + auto weakestCreature = vstd::minElementByFun(hero->Slots(), [](const std::pair & slot) -> int { return slot.second->getCount() == 1 ? std::numeric_limits::max() : slot.second->getCreatureID().toCreature()->getAIValue(); }); - if(weakestCreature == attacker->Slots().end() || weakestCreature->second->getCount() == 1) + if(weakestCreature == hero->Slots().end() || weakestCreature->second->getCount() == 1) { break; } - cb->splitStack(attacker, attacker, weakestCreature->first, freeSlots.front(), 1); + cb->splitStack(hero, hero, weakestCreature->first, freeSlots.front(), 1); freeSlots.pop(); } +} + +void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker) +{ + addSingleCreatureStacks(attacker); if(town->fortLevel() > CGTownInstance::FORT) { diff --git a/AI/Nullkiller/Helpers/ArmyFormation.h b/AI/Nullkiller/Helpers/ArmyFormation.h index cb7cf379e..37ef2df47 100644 --- a/AI/Nullkiller/Helpers/ArmyFormation.h +++ b/AI/Nullkiller/Helpers/ArmyFormation.h @@ -31,6 +31,10 @@ public: ArmyFormation(std::shared_ptr CB, const Nullkiller * ai): cb(CB) {} void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker); + + void rearrangeArmyForWhirlpool(const CGHeroInstance * hero); + + void addSingleCreatureStacks(const CGHeroInstance * hero); }; } diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp index f7a0e6c30..3f67d4b15 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.cpp +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.cpp @@ -10,6 +10,7 @@ #include "StdInc.h" #include "AINodeStorage.h" #include "Actions/TownPortalAction.h" +#include "Actions/WhirlpoolAction.h" #include "../Goals/Goals.h" #include "../AIGateway.h" #include "../Engine/Nullkiller.h" @@ -255,10 +256,45 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf { commit(dstNode, srcNode, destination.action, destination.turn, destination.movementLeft, destination.cost); - if(srcNode->specialAction || srcNode->chainOther) + // regular pathfinder can not go directly through whirlpool + bool isWhirlpoolTeleport = destination.nodeObject + && destination.nodeObject->ID == Obj::WHIRLPOOL; + + if(srcNode->specialAction + || srcNode->chainOther + || isWhirlpoolTeleport) { // there is some action on source tile which should be performed before we can bypass it - destination.node->theNodeBefore = source.node; + dstNode->theNodeBefore = source.node; + + if(isWhirlpoolTeleport) + { + if(dstNode->actor->creatureSet->Slots().size() == 1 + && dstNode->actor->creatureSet->Slots().begin()->second->getCount() == 1) + { + return; + } + + auto weakest = vstd::minElementByFun(dstNode->actor->creatureSet->Slots(), [](std::pair pair) -> int + { + return pair.second->getCount() * pair.second->getCreatureID().toCreature()->getAIValue(); + }); + + if(weakest == dstNode->actor->creatureSet->Slots().end()) + { + logAi->debug("Empty army entering whirlpool detected at tile %s", dstNode->coord.toString()); + destination.blocked = true; + + return; + } + + if(dstNode->actor->creatureSet->getFreeSlots().size()) + dstNode->armyLoss += weakest->second->getCreatureID().toCreature()->getAIValue(); + else + dstNode->armyLoss += (weakest->second->getCount() + 1) / 2 * weakest->second->getCreatureID().toCreature()->getAIValue(); + + dstNode->specialAction = AIPathfinding::WhirlpoolAction::instance; + } } if(dstNode->specialAction && dstNode->actor) @@ -1014,8 +1050,8 @@ std::vector AINodeStorage::calculateTeleportations( for(auto & neighbour : accessibleExits) { - auto node = getOrCreateNode(neighbour, source.node->layer, srcNode->actor); - + std::optional node = getOrCreateNode(neighbour, source.node->layer, srcNode->actor); + if(!node) continue; diff --git a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp index d4a3a651a..863d8975a 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp @@ -48,6 +48,8 @@ namespace AIPathfinding { options.canUseCast = true; options.allowLayerTransitioningAfterBattle = true; + options.useTeleportWhirlpool = true; + options.forceUseTeleportWhirlpool = true; } AIPathfinderConfig::~AIPathfinderConfig() = default; diff --git a/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.cpp b/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.cpp new file mode 100644 index 000000000..56625a1b3 --- /dev/null +++ b/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.cpp @@ -0,0 +1,55 @@ +/* +* WhirlpoolAction.cpp, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ + +#include "StdInc.h" +#include "../../Goals/AdventureSpellCast.h" +#include "../../../../lib/mapObjects/MapObjects.h" +#include "WhirlpoolAction.h" +#include "../../AIGateway.h" + +namespace NKAI +{ + +using namespace AIPathfinding; + +std::shared_ptr WhirlpoolAction::instance = std::make_shared(); + +void WhirlpoolAction::execute(AIGateway * ai, const CGHeroInstance * hero) const +{ + ai->nullkiller->armyFormation->rearrangeArmyForWhirlpool(hero); +} + +std::string WhirlpoolAction::toString() const +{ + return "Prepare for whirlpool"; +} +/* +bool TownPortalAction::canAct(const CGHeroInstance * hero, const AIPathNode * source) const +{ +#ifdef VCMI_TRACE_PATHFINDER + logAi->trace( + "Hero %s has %d mana and needed %d and already spent %d", + hero->name, + hero->mana, + getManaCost(hero), + source->manaCost); +#endif + + return hero->mana >= source->manaCost + getManaCost(hero); +} + +uint32_t TownPortalAction::getManaCost(const CGHeroInstance * hero) const +{ + SpellID summonBoat = SpellID::TOWN_PORTAL; + + return hero->getSpellCost(summonBoat.toSpell()); +}*/ + +} diff --git a/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.h b/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.h new file mode 100644 index 000000000..704b94b0f --- /dev/null +++ b/AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.h @@ -0,0 +1,35 @@ +/* +* WhirlpoolAction.h, part of VCMI engine +* +* Authors: listed in file AUTHORS in main folder +* +* License: GNU General Public License v2.0 or later +* Full text of license available in license.txt file, in main folder +* +*/ + +#pragma once + +#include "SpecialAction.h" +#include "../../../../lib/mapObjects/MapObjects.h" +#include "../../Goals/AdventureSpellCast.h" + +namespace NKAI +{ +namespace AIPathfinding +{ + class WhirlpoolAction : public SpecialAction + { + public: + WhirlpoolAction() + { + } + + static std::shared_ptr instance; + + void execute(AIGateway * ai, const CGHeroInstance * hero) const override; + + std::string toString() const override; + }; +} +} diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp index 34f4f2766..88d61372c 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp @@ -11,9 +11,11 @@ #include "AIMovementAfterDestinationRule.h" #include "../Actions/BattleAction.h" #include "../Actions/QuestAction.h" +#include "../Actions/WhirlpoolAction.h" #include "../../Goals/Invalid.h" #include "AIPreviousNodeRule.h" #include "../../../../lib/pathfinder/PathfinderOptions.h" +#include "../../../../lib/pathfinder/CPathfinder.h" namespace NKAI { diff --git a/lib/pathfinder/CPathfinder.cpp b/lib/pathfinder/CPathfinder.cpp index cefb55673..ef92af1df 100644 --- a/lib/pathfinder/CPathfinder.cpp +++ b/lib/pathfinder/CPathfinder.cpp @@ -466,7 +466,7 @@ bool CPathfinderHelper::addTeleportOneWayRandom(const CGTeleport * obj) const bool CPathfinderHelper::addTeleportWhirlpool(const CGWhirlpool * obj) const { - return options.useTeleportWhirlpool && hasBonusOfType(BonusType::WHIRLPOOL_PROTECTION) && obj; + return options.useTeleportWhirlpool && (whirlpoolProtection || options.forceUseTeleportWhirlpool) && obj; } int CPathfinderHelper::movementPointsAfterEmbark(int movement, int basicCost, bool disembark) const @@ -506,6 +506,8 @@ CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Her updateTurnInfo(); initializePatrol(); + whirlpoolProtection = Hero->hasBonusOfType(BonusType::WHIRLPOOL_PROTECTION); + SpellID flySpell = SpellID::FLY; canCastFly = Hero->canCastThisSpell(flySpell.toSpell()); diff --git a/lib/pathfinder/CPathfinder.h b/lib/pathfinder/CPathfinder.h index 518244b6d..4791a450f 100644 --- a/lib/pathfinder/CPathfinder.h +++ b/lib/pathfinder/CPathfinder.h @@ -85,6 +85,7 @@ public: const PathfinderOptions & options; bool canCastFly; bool canCastWaterWalk; + bool whirlpoolProtection; CPathfinderHelper(CGameState * gs, const CGHeroInstance * Hero, const PathfinderOptions & Options); virtual ~CPathfinderHelper(); diff --git a/lib/pathfinder/PathfinderOptions.cpp b/lib/pathfinder/PathfinderOptions.cpp index 675731c8c..bdc749c1a 100644 --- a/lib/pathfinder/PathfinderOptions.cpp +++ b/lib/pathfinder/PathfinderOptions.cpp @@ -34,6 +34,7 @@ PathfinderOptions::PathfinderOptions() , turnLimit(std::numeric_limits::max()) , canUseCast(false) , allowLayerTransitioningAfterBattle(false) + , forceUseTeleportWhirlpool(false) { } diff --git a/lib/pathfinder/PathfinderOptions.h b/lib/pathfinder/PathfinderOptions.h index 044511910..f2469c9d2 100644 --- a/lib/pathfinder/PathfinderOptions.h +++ b/lib/pathfinder/PathfinderOptions.h @@ -30,6 +30,7 @@ struct DLL_LINKAGE PathfinderOptions bool useTeleportOneWay; // One-way monoliths with one known exit only bool useTeleportOneWayRandom; // One-way monoliths with more than one known exit bool useTeleportWhirlpool; // Force enabled if hero protected or unaffected (have one stack of one creature) + bool forceUseTeleportWhirlpool; // Force enabled if hero protected or unaffected (have one stack of one creature) /// TODO: Find out with client and server code, merge with normal teleporters. /// Likely proper implementation would require some refactoring of CGTeleport.