/* * AIPathfinderConfig.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 "AIPathfinderConfig.h" #include "../Goals/Goals.h" #include "../../../CCallback.h" #include "../../../lib/mapping/CMap.h" #include "../../../lib/mapObjects/MapObjects.h" namespace AIPathfinding { class VirtualBoatAction : public ISpecialAction { private: uint64_t specialChain; public: VirtualBoatAction(uint64_t specialChain) :specialChain(specialChain) { } uint64_t getSpecialChain() const { return specialChain; } }; class BuildBoatAction : public VirtualBoatAction { private: const IShipyard * shipyard; public: BuildBoatAction(const IShipyard * shipyard) :VirtualBoatAction(AINodeStorage::RESOURCE_CHAIN), shipyard(shipyard) { } virtual Goals::TSubgoal whatToDo(HeroPtr hero) const override { return sptr(Goals::BuildBoat(shipyard)); } }; class SummonBoatAction : public VirtualBoatAction { public: SummonBoatAction() :VirtualBoatAction(AINodeStorage::CAST_CHAIN) { } virtual Goals::TSubgoal whatToDo(HeroPtr hero) const override { return sptr(Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT)); } virtual void applyOnDestination( HeroPtr hero, CDestinationNodeInfo & destination, const PathNodeInfo & source, AIPathNode * dstMode, const AIPathNode * srcNode) const override { dstMode->manaCost = srcNode->manaCost + getManaCost(hero); dstMode->theNodeBefore = source.node; } bool isAffordableBy(HeroPtr 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); } private: uint32_t getManaCost(HeroPtr hero) const { SpellID summonBoat = SpellID::SUMMON_BOAT; return hero->getSpellCost(summonBoat.toSpell()); } }; class BattleAction : public ISpecialAction { private: const int3 target; const HeroPtr hero; public: BattleAction(const int3 target) :target(target) { } virtual Goals::TSubgoal whatToDo(HeroPtr hero) const override { return sptr(Goals::VisitTile(target).sethero(hero)); } }; class AILayerTransitionRule : public LayerTransitionRule { private: CPlayerSpecificInfoCallback * cb; VCAI * ai; std::map> virtualBoats; std::shared_ptr nodeStorage; std::shared_ptr summonableVirtualBoat; public: AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, VCAI * ai, std::shared_ptr nodeStorage) :cb(cb), ai(ai), nodeStorage(nodeStorage) { setup(); } virtual void process( const PathNodeInfo & source, CDestinationNodeInfo & destination, const PathfinderConfig * pathfinderConfig, CPathfinderHelper * pathfinderHelper) const override { LayerTransitionRule::process(source, destination, pathfinderConfig, pathfinderHelper); if(!destination.blocked) { return; } if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL) { std::shared_ptr virtualBoat = findVirtualBoat(destination, source); if(virtualBoat && tryEmbarkVirtualBoat(destination, source, virtualBoat)) { #ifdef VCMI_TRACE_PATHFINDER logAi->trace("Embarking to virtual boat while moving %s -> %s!", source.coord.toString(), destination.coord.toString()); #endif } } } private: void setup() { std::vector shipyards; for(const CGTownInstance * t : cb->getTownsInfo()) { if(t->hasBuilt(BuildingID::SHIPYARD)) shipyards.push_back(t); } for(const CGObjectInstance * obj : ai->visitableObjs) { if(obj->ID != Obj::TOWN) //towns were handled in the previous loop { if(const IShipyard * shipyard = IShipyard::castFrom(obj)) shipyards.push_back(shipyard); } } for(const IShipyard * shipyard : shipyards) { if(shipyard->shipyardStatus() == IShipyard::GOOD) { int3 boatLocation = shipyard->bestLocation(); virtualBoats[boatLocation] = std::make_shared(shipyard); logAi->debug("Virtual boat added at %s", boatLocation.toString()); } } auto hero = nodeStorage->getHero(); auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell(); if(hero->canCastThisSpell(summonBoatSpell) && hero->getSpellSchoolLevel(summonBoatSpell) >= SecSkillLevel::ADVANCED) { // TODO: For lower school level we might need to check the existance of some boat summonableVirtualBoat.reset(new SummonBoatAction()); } } std::shared_ptr findVirtualBoat( CDestinationNodeInfo &destination, const PathNodeInfo &source) const { std::shared_ptr virtualBoat; if(vstd::contains(virtualBoats, destination.coord)) { virtualBoat = virtualBoats.at(destination.coord); } else if( summonableVirtualBoat && summonableVirtualBoat->isAffordableBy(nodeStorage->getHero(), nodeStorage->getAINode(source.node))) { virtualBoat = summonableVirtualBoat; } return virtualBoat; } bool tryEmbarkVirtualBoat( CDestinationNodeInfo &destination, const PathNodeInfo &source, std::shared_ptr virtualBoat) const { bool result = false; nodeStorage->updateAINode(destination.node, [&](AIPathNode * node) { auto boatNodeOptional = nodeStorage->getOrCreateNode( node->coord, node->layer, node->chainMask | virtualBoat->getSpecialChain()); if(boatNodeOptional) { AIPathNode * boatNode = boatNodeOptional.get(); if(boatNode->action == CGPathNode::UNKNOWN) { boatNode->specialAction = virtualBoat; destination.blocked = false; destination.action = CGPathNode::ENodeAction::EMBARK; destination.node = boatNode; result = true; } else { #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Special transition node already allocated. Blocked moving %s -> %s", source.coord.toString(), destination.coord.toString()); #endif } } else { logAi->debug( "Can not allocate special transition node while moving %s -> %s", source.coord.toString(), destination.coord.toString()); } }); return result; } }; class AIMovementAfterDestinationRule : public MovementAfterDestinationRule { private: CPlayerSpecificInfoCallback * cb; std::shared_ptr nodeStorage; public: AIMovementAfterDestinationRule(CPlayerSpecificInfoCallback * cb, std::shared_ptr nodeStorage) :cb(cb), nodeStorage(nodeStorage) { } virtual void process( const PathNodeInfo & source, CDestinationNodeInfo & destination, const PathfinderConfig * pathfinderConfig, CPathfinderHelper * pathfinderHelper) const override { if(nodeStorage->hasBetterChain(source, destination)) { destination.blocked = true; return; } auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper); if(blocker == BlockingReason::NONE) return; if(blocker == BlockingReason::DESTINATION_BLOCKVIS && destination.nodeObject) { auto objID = destination.nodeObject->ID; if((objID == Obj::HERO && destination.objectRelations != PlayerRelations::ENEMIES) || objID == Obj::SUBTERRANEAN_GATE || objID == Obj::MONOLITH_TWO_WAY || objID == Obj::MONOLITH_ONE_WAY_ENTRANCE || objID == Obj::MONOLITH_ONE_WAY_EXIT || objID == Obj::WHIRLPOOL) { destination.blocked = true; } return; } if(blocker == BlockingReason::DESTINATION_VISIT) { return; } if(blocker == BlockingReason::DESTINATION_GUARDED) { auto srcGuardians = cb->getGuardingCreatures(source.coord); auto destGuardians = cb->getGuardingCreatures(destination.coord); if(destGuardians.empty()) { destination.blocked = true; return; } vstd::erase_if(destGuardians, [&](const CGObjectInstance * destGuard) -> bool { return vstd::contains(srcGuardians, destGuard); }); auto guardsAlreadyBypassed = destGuardians.empty() && srcGuardians.size(); if(guardsAlreadyBypassed && nodeStorage->isBattleNode(source.node)) { #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Bypass guard at destination while moving %s -> %s", source.coord.toString(), destination.coord.toString()); #endif return; } const AIPathNode * destNode = nodeStorage->getAINode(destination.node); auto battleNodeOptional = nodeStorage->getOrCreateNode( destination.coord, destination.node->layer, destNode->chainMask | AINodeStorage::BATTLE_CHAIN); if(!battleNodeOptional) { #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Can not allocate battle node while moving %s -> %s", source.coord.toString(), destination.coord.toString()); #endif destination.blocked = true; return; } AIPathNode * battleNode = battleNodeOptional.get(); if(battleNode->locked) { #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Block bypass guard at destination while moving %s -> %s", source.coord.toString(), destination.coord.toString()); #endif destination.blocked = true; return; } auto hero = nodeStorage->getHero(); auto danger = evaluateDanger(destination.coord, hero); destination.node = battleNode; nodeStorage->commit(destination, source); if(battleNode->danger < danger) { battleNode->danger = danger; } battleNode->specialAction = std::make_shared(destination.coord); #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Begin bypass guard at destination with danger %s while moving %s -> %s", std::to_string(danger), source.coord.toString(), destination.coord.toString()); #endif return; } destination.blocked = true; } }; class AIMovementToDestinationRule : public MovementToDestinationRule { private: CPlayerSpecificInfoCallback * cb; std::shared_ptr nodeStorage; public: AIMovementToDestinationRule(CPlayerSpecificInfoCallback * cb, std::shared_ptr nodeStorage) :cb(cb), nodeStorage(nodeStorage) { } virtual void process( const PathNodeInfo & source, CDestinationNodeInfo & destination, const PathfinderConfig * pathfinderConfig, CPathfinderHelper * pathfinderHelper) const override { auto blocker = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper); if(blocker == BlockingReason::NONE) return; if(blocker == BlockingReason::DESTINATION_BLOCKED && destination.action == CGPathNode::EMBARK && nodeStorage->getAINode(destination.node)->specialAction) { return; } if(blocker == BlockingReason::SOURCE_GUARDED && nodeStorage->isBattleNode(source.node)) { #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Bypass src guard while moving from %s to %s", source.coord.toString(), destination.coord.toString()); #endif return; } destination.blocked = true; } }; class AIPreviousNodeRule : public MovementToDestinationRule { private: CPlayerSpecificInfoCallback * cb; std::shared_ptr nodeStorage; public: AIPreviousNodeRule(CPlayerSpecificInfoCallback * cb, std::shared_ptr nodeStorage) :cb(cb), nodeStorage(nodeStorage) { } virtual void process( const PathNodeInfo & source, CDestinationNodeInfo & destination, const PathfinderConfig * pathfinderConfig, CPathfinderHelper * pathfinderHelper) const override { if(source.node->action == CGPathNode::ENodeAction::BLOCKING_VISIT || source.node->action == CGPathNode::ENodeAction::VISIT) { // we can not directly bypass objects, we need to interact with them first destination.node->theNodeBefore = source.node; #ifdef VCMI_TRACE_PATHFINDER logAi->trace( "Link src node %s to destination node %s while bypassing visitable obj", source.coord.toString(), destination.coord.toString()); #endif return; } auto aiSourceNode = nodeStorage->getAINode(source.node); if(aiSourceNode->specialAction) { // there is some action on source tile which should be performed before we can bypass it destination.node->theNodeBefore = source.node; } } }; std::vector> makeRuleset( CPlayerSpecificInfoCallback * cb, VCAI * ai, std::shared_ptr nodeStorage) { std::vector> rules = { std::make_shared(cb, ai, nodeStorage), std::make_shared(), std::make_shared(cb, nodeStorage), std::make_shared(), std::make_shared(cb, nodeStorage), std::make_shared(cb, nodeStorage) }; return rules; } AIPathfinderConfig::AIPathfinderConfig( CPlayerSpecificInfoCallback * cb, VCAI * ai, std::shared_ptr nodeStorage) :PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)) { } }