diff --git a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp index 0096a466d..84160954a 100644 --- a/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp +++ b/AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp @@ -157,12 +157,13 @@ void DangerHitMapAnalyzer::calculateTileOwners() if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z) hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]); - std::map townHeroes; + std::vector> temporaryHeroes; std::map heroTownMap; + std::map townHeroes; auto addTownHero = [&](const CGTownInstance * town) { - auto townHero = new CGHeroInstance(town->cb); + auto townHero = temporaryHeroes.emplace_back(std::make_unique(town->cb)).get(); CRandomGenerator rng; auto visitablePos = town->visitablePos(); @@ -238,11 +239,6 @@ void DangerHitMapAnalyzer::calculateTileOwners() hitMap[pos.x][pos.y][pos.z].closestTown = enemyTown; } }); - - for(auto h : townHeroes) - { - delete h.first; - } } const std::vector & DangerHitMapAnalyzer::getTownThreats(const CGTownInstance * town) const diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp index 908f6c6cd..b294b228e 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.cpp @@ -189,15 +189,7 @@ bool ObjectClusterizer::shouldVisitObject(const CGObjectInstance * obj) const return true; //all of the following is met } -void ObjectClusterizer::clusterize() -{ - auto start = std::chrono::high_resolution_clock::now(); - - nearObjects.reset(); - farObjects.reset(); - blockedObjects.clear(); - - Obj ignoreObjects[] = { +Obj ObjectClusterizer::IgnoredObjectTypes[] = { Obj::BOAT, Obj::EYE_OF_MAGI, Obj::MONOLITH_ONE_WAY_ENTRANCE, @@ -216,7 +208,15 @@ void ObjectClusterizer::clusterize() Obj::REDWOOD_OBSERVATORY, Obj::CARTOGRAPHER, Obj::PILLAR_OF_FIRE - }; +}; + +void ObjectClusterizer::clusterize() +{ + auto start = std::chrono::high_resolution_clock::now(); + + nearObjects.reset(); + farObjects.reset(); + blockedObjects.clear(); logAi->debug("Begin object clusterization"); @@ -225,137 +225,18 @@ void ObjectClusterizer::clusterize() ai->memory->visitableObjs.end()); #if NKAI_TRACE_LEVEL == 0 - parallel_for(blocked_range(0, objs.size()), [&](const blocked_range & r) + parallel_for(blocked_range(0, objs.size()), [&](const blocked_range & r) { #else blocked_range r(0, objs.size()); #endif - { auto priorityEvaluator = ai->priorityEvaluators->acquire(); for(int i = r.begin(); i != r.end(); i++) { - auto obj = objs[i]; - - if(!shouldVisitObject(obj)) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Skip object %s%s.", obj->getObjectName(), obj->visitablePos().toString()); -#endif - continue; - } - -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString()); -#endif - - auto paths = ai->pathfinder->getPathInfo(obj->visitablePos(), true); - - if(paths.empty()) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("No paths found."); -#endif - continue; - } - - std::sort(paths.begin(), paths.end(), [](const AIPath & p1, const AIPath & p2) -> bool - { - return p1.movementCost() < p2.movementCost(); - }); - - if(vstd::contains(ignoreObjects, obj->ID)) - { - farObjects.addObject(obj, paths.front(), 0); - -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Object ignored. Moved to far objects with path %s", paths.front().toString()); -#endif - - continue; - } - - std::set heroesProcessed; - - for(auto & path : paths) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Checking path %s", path.toString()); -#endif - - if(!shouldVisit(ai, path.targetHero, obj)) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Hero %s does not need to visit %s", path.targetHero->getObjectName(), obj->getObjectName()); -#endif - continue; - } - - if(path.nodes.size() > 1) - { - auto blocker = getBlocker(path); - - if(blocker) - { - if(vstd::contains(heroesProcessed, path.targetHero)) - { -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Hero %s is already processed.", path.targetHero->getObjectName()); -#endif - continue; - } - - heroesProcessed.insert(path.targetHero); - - float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); - - if(priority < MIN_PRIORITY) - continue; - - ClusterMap::accessor cluster; - blockedObjects.insert( - cluster, - ClusterMap::value_type(blocker, std::make_shared(blocker))); - - cluster->second->addObject(obj, path, priority); - -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Path added to cluster %s%s", blocker->getObjectName(), blocker->visitablePos().toString()); -#endif - continue; - } - } - - heroesProcessed.insert(path.targetHero); - - float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); - - if(priority < MIN_PRIORITY) - continue; - - bool interestingObject = path.turn() <= 2 || priority > 0.5f; - - if(interestingObject) - { - nearObjects.addObject(obj, path, priority); - } - else - { - farObjects.addObject(obj, path, priority); - } - -#if NKAI_TRACE_LEVEL >= 2 - logAi->trace("Path %s added to %s objects. Turn: %d, priority: %f", - path.toString(), - interestingObject ? "near" : "far", - path.turn(), - priority); -#endif - } + clusterizeObject(objs[i], priorityEvaluator.get()); } #if NKAI_TRACE_LEVEL == 0 }); -#else - } #endif logAi->trace("Near objects count: %i", nearObjects.objects.size()); @@ -376,4 +257,123 @@ void ObjectClusterizer::clusterize() logAi->trace("Clusterization complete in %ld", timeElapsed(start)); } +void ObjectClusterizer::clusterizeObject(const CGObjectInstance * obj, PriorityEvaluator * priorityEvaluator) +{ + if(!shouldVisitObject(obj)) + { +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Skip object %s%s.", obj->getObjectName(), obj->visitablePos().toString()); +#endif + return; + } + +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString()); +#endif + + auto paths = ai->pathfinder->getPathInfo(obj->visitablePos(), true); + + if(paths.empty()) + { +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("No paths found."); +#endif + return; + } + + std::sort(paths.begin(), paths.end(), [](const AIPath & p1, const AIPath & p2) -> bool + { + return p1.movementCost() < p2.movementCost(); + }); + + if(vstd::contains(IgnoredObjectTypes, obj->ID)) + { + farObjects.addObject(obj, paths.front(), 0); + +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Object ignored. Moved to far objects with path %s", paths.front().toString()); +#endif + + return; + } + + std::set heroesProcessed; + + for(auto & path : paths) + { +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Checking path %s", path.toString()); +#endif + + if(!shouldVisit(ai, path.targetHero, obj)) + { +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Hero %s does not need to visit %s", path.targetHero->getObjectName(), obj->getObjectName()); +#endif + continue; + } + + if(path.nodes.size() > 1) + { + auto blocker = getBlocker(path); + + if(blocker) + { + if(vstd::contains(heroesProcessed, path.targetHero)) + { +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Hero %s is already processed.", path.targetHero->getObjectName()); +#endif + continue; + } + + heroesProcessed.insert(path.targetHero); + + float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); + + if(priority < MIN_PRIORITY) + continue; + + ClusterMap::accessor cluster; + blockedObjects.insert( + cluster, + ClusterMap::value_type(blocker, std::make_shared(blocker))); + + cluster->second->addObject(obj, path, priority); + +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Path added to cluster %s%s", blocker->getObjectName(), blocker->visitablePos().toString()); +#endif + continue; + } + } + + heroesProcessed.insert(path.targetHero); + + float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj))); + + if(priority < MIN_PRIORITY) + continue; + + bool interestingObject = path.turn() <= 2 || priority > 0.5f; + + if(interestingObject) + { + nearObjects.addObject(obj, path, priority); + } + else + { + farObjects.addObject(obj, path, priority); + } + +#if NKAI_TRACE_LEVEL >= 2 + logAi->trace("Path %s added to %s objects. Turn: %d, priority: %f", + path.toString(), + interestingObject ? "near" : "far", + path.turn(), + priority); +#endif + } +} + } diff --git a/AI/Nullkiller/Analyzers/ObjectClusterizer.h b/AI/Nullkiller/Analyzers/ObjectClusterizer.h index bc4118917..56754a416 100644 --- a/AI/Nullkiller/Analyzers/ObjectClusterizer.h +++ b/AI/Nullkiller/Analyzers/ObjectClusterizer.h @@ -49,9 +49,13 @@ public: using ClusterMap = tbb::concurrent_hash_map>; +class PriorityEvaluator; + class ObjectClusterizer { private: + static Obj IgnoredObjectTypes[]; + ObjectCluster nearObjects; ObjectCluster farObjects; ClusterMap blockedObjects; @@ -68,6 +72,7 @@ public: private: bool shouldVisitObject(const CGObjectInstance * obj) const; + void clusterizeObject(const CGObjectInstance * obj, PriorityEvaluator * priorityEvaluator); }; } diff --git a/AI/Nullkiller/Pathfinding/AINodeStorage.h b/AI/Nullkiller/Pathfinding/AINodeStorage.h index 818ded9c8..893e15633 100644 --- a/AI/Nullkiller/Pathfinding/AINodeStorage.h +++ b/AI/Nullkiller/Pathfinding/AINodeStorage.h @@ -11,8 +11,8 @@ #pragma once #define NKAI_PATHFINDER_TRACE_LEVEL 0 -#define NKAI_GRAPH_TRACE_LEVEL 0 -#define NKAI_TRACE_LEVEL 1 +constexpr int NKAI_GRAPH_TRACE_LEVEL = 0; +#define NKAI_TRACE_LEVEL 0 #include "../../../lib/pathfinder/CGPathNode.h" #include "../../../lib/pathfinder/INodeStorage.h" diff --git a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp index 10d7f9644..50ed296ef 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinder.cpp +++ b/AI/Nullkiller/Pathfinding/AIPathfinder.cpp @@ -58,7 +58,7 @@ std::vector AIPathfinder::getPathInfo(const int3 & tile, bool includeGra return info; } -void AIPathfinder::updatePaths(std::map heroes, PathfinderSettings pathfinderSettings) +void AIPathfinder::updatePaths(const std::map & heroes, PathfinderSettings pathfinderSettings) { if(!storage) { @@ -125,7 +125,7 @@ void AIPathfinder::updatePaths(std::map heroes logAi->trace("Recalculated paths in %ld", timeElapsed(start)); } -void AIPathfinder::updateGraphs(std::map heroes) +void AIPathfinder::updateGraphs(const std::map & heroes) { auto start = std::chrono::high_resolution_clock::now(); std::vector heroesVector; @@ -134,22 +134,23 @@ void AIPathfinder::updateGraphs(std::map heroe for(auto hero : heroes) { - heroGraphs.emplace(hero.first->id, GraphPaths()); - heroesVector.push_back(hero.first); + if(heroGraphs.try_emplace(hero.first->id, GraphPaths()).second) + heroesVector.push_back(hero.first); } - parallel_for(blocked_range(0, heroesVector.size()), [&](const blocked_range & r) + parallel_for(blocked_range(0, heroesVector.size()), [this, &heroesVector](const blocked_range & r) { for(auto i = r.begin(); i != r.end(); i++) heroGraphs.at(heroesVector[i]->id).calculatePaths(heroesVector[i], ai); }); -#if NKAI_GRAPH_TRACE_LEVEL >= 1 - for(auto hero : heroes) + if(NKAI_GRAPH_TRACE_LEVEL >= 1) { - heroGraphs[hero.first->id].dumpToLog(); + for(auto hero : heroes) + { + heroGraphs[hero.first->id].dumpToLog(); + } } -#endif logAi->trace("Graph paths updated in %lld", timeElapsed(start)); } diff --git a/AI/Nullkiller/Pathfinding/AIPathfinder.h b/AI/Nullkiller/Pathfinding/AIPathfinder.h index 07e3c5e6a..ed6c5b5ed 100644 --- a/AI/Nullkiller/Pathfinding/AIPathfinder.h +++ b/AI/Nullkiller/Pathfinding/AIPathfinder.h @@ -46,8 +46,8 @@ public: AIPathfinder(CPlayerSpecificInfoCallback * cb, Nullkiller * ai); std::vector getPathInfo(const int3 & tile, bool includeGraph = false) const; bool isTileAccessible(const HeroPtr & hero, const int3 & tile) const; - void updatePaths(std::map heroes, PathfinderSettings pathfinderSettings); - void updateGraphs(std::map heroes); + void updatePaths(const std::map & heroes, PathfinderSettings pathfinderSettings); + void updateGraphs(const std::map & heroes); void init(); std::shared_ptrgetStorage() diff --git a/AI/Nullkiller/Pathfinding/ObjectGraph.cpp b/AI/Nullkiller/Pathfinding/ObjectGraph.cpp index 6182703f6..5cddf022b 100644 --- a/AI/Nullkiller/Pathfinding/ObjectGraph.cpp +++ b/AI/Nullkiller/Pathfinding/ObjectGraph.cpp @@ -19,17 +19,100 @@ namespace NKAI { -void ObjectGraph::updateGraph(const Nullkiller * ai) +class ObjectGraphCalculator { - auto cb = ai->cb; +private: + ObjectGraph * target; + const Nullkiller * ai; std::map actors; std::map actorObjectMap; - std::vector boats; - auto addObjectActor = [&](const CGObjectInstance * obj) + std::vector> temporaryBoats; + std::vector> temporaryActorHeroes; + +public: + ObjectGraphCalculator(ObjectGraph * target, const Nullkiller * ai) + :ai(ai), target(target) { - auto objectActor = new CGHeroInstance(obj->cb); + for(auto obj : ai->memory->visitableObjs) + { + if(obj && obj->isVisitable() && obj->ID != Obj::HERO) + { + addObjectActor(obj); + } + } + + for(auto town : ai->cb->getTownsInfo()) + { + addObjectActor(town); + } + + PathfinderSettings ps; + + ps.mainTurnDistanceLimit = 5; + ps.scoutTurnDistanceLimit = 1; + ps.allowBypassObjects = false; + + ai->pathfinder->updatePaths(actors, ps); + } + + void calculateConnections(const int3 & pos) + { + auto guarded = ai->cb->getGuardingCreaturePosition(pos).valid(); + + if(guarded) + return; + + auto paths = ai->pathfinder->getPathInfo(pos); + + for(AIPath & path1 : paths) + { + for(AIPath & path2 : paths) + { + if(path1.targetHero == path2.targetHero) + continue; + + auto obj1 = actorObjectMap[path1.targetHero]; + auto obj2 = actorObjectMap[path2.targetHero]; + + auto tile1 = cb->getTile(obj1->visitablePos()); + auto tile2 = cb->getTile(obj2->visitablePos()); + + if(tile2->isWater() && !tile1->isWater()) + { + auto linkTile = cb->getTile(pos); + + if(!linkTile->isWater() || obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD) + continue; + } + + auto danger = ai->pathfinder->getStorage()->evaluateDanger(obj2->visitablePos(), path1.targetHero, true); + + auto updated = target->tryAddConnection( + obj1->visitablePos(), + obj2->visitablePos(), + path1.movementCost() + path2.movementCost(), + danger); + + if(NKAI_GRAPH_TRACE_LEVEL >= 2 && updated) + { + logAi->trace( + "Connected %s[%s] -> %s[%s] through [%s], cost %2f", + obj1->getObjectName(), obj1->visitablePos().toString(), + obj2->getObjectName(), obj2->visitablePos().toString(), + pos.toString(), + path1.movementCost() + path2.movementCost()); + } + } + } + } + +private: + void addObjectActor(const CGObjectInstance * obj) + { + auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique(obj->cb)).get(); + CRandomGenerator rng; auto visitablePos = obj->visitablePos(); @@ -40,105 +123,42 @@ void ObjectGraph::updateGraph(const Nullkiller * ai) if(cb->getTile(visitablePos)->isWater()) { - boats.push_back(new CGBoat(objectActor->cb)); - objectActor->boat = boats.back(); + objectActor->boat = temporaryBoats.emplace_back(std::make_unique(objectActor->cb)).get(); } actorObjectMap[objectActor] = obj; - actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::SHIPYARD ? HeroRole::MAIN : HeroRole::SCOUT; - addObject(obj); + actors[objectActor] = obj->ID == Obj::TOWN || obj->ID == Obj::SHIPYARD ? HeroRole::MAIN : HeroRole::SCOUT; + + target->addObject(obj); }; +}; - for(auto obj : ai->memory->visitableObjs) - { - if(obj && obj->isVisitable() && obj->ID != Obj::HERO) - { - addObjectActor(obj); - } - } +bool ObjectGraph::tryAddConnection( + const int3 & from, + const int3 & to, + float cost, + uint64_t danger) +{ + return nodes[from].connections[to].update(cost, danger); +} - for(auto town : cb->getTownsInfo()) - { - addObjectActor(town); - } +void ObjectGraph::updateGraph(const Nullkiller * ai) +{ + auto cb = ai->cb; - PathfinderSettings ps; - - ps.mainTurnDistanceLimit = 5; - ps.scoutTurnDistanceLimit = 1; - ps.allowBypassObjects = false; + ObjectGraphCalculator calculator(this, ai); - ai->pathfinder->updatePaths(actors, ps); - - foreach_tile_pos(cb.get(), [&](const CPlayerSpecificInfoCallback * cb, const int3 & pos) + foreach_tile_pos(cb.get(), [this, &calculator](const CPlayerSpecificInfoCallback * cb, const int3 & pos) { if(nodes.find(pos) != nodes.end()) return; - auto guarded = ai->cb->getGuardingCreaturePosition(pos).valid(); - - if(guarded) - return; - - auto paths = ai->pathfinder->getPathInfo(pos); - - for(AIPath & path1 : paths) - { - for(AIPath & path2 : paths) - { - if(path1.targetHero == path2.targetHero) - continue; - - auto obj1 = actorObjectMap[path1.targetHero]; - auto obj2 = actorObjectMap[path2.targetHero]; - - auto tile1 = cb->getTile(obj1->visitablePos()); - auto tile2 = cb->getTile(obj2->visitablePos()); - - if(tile2->isWater() && !tile1->isWater()) - { - auto linkTile = cb->getTile(pos); - - if(!linkTile->isWater() || obj1->ID != Obj::BOAT || obj1->ID != Obj::SHIPYARD) - continue; - } - - auto danger = ai->pathfinder->getStorage()->evaluateDanger(obj2->visitablePos(), path1.targetHero, true); - - auto updated = nodes[obj1->visitablePos()].connections[obj2->visitablePos()].update( - path1.movementCost() + path2.movementCost(), - danger); - -#if NKAI_GRAPH_TRACE_LEVEL >= 2 - if(updated) - { - logAi->trace( - "Connected %s[%s] -> %s[%s] through [%s], cost %2f", - obj1->getObjectName(), obj1->visitablePos().toString(), - obj2->getObjectName(), obj2->visitablePos().toString(), - pos.toString(), - path1.movementCost() + path2.movementCost()); - } -#else - (void)updated; -#endif - } - } + calculator.calculateConnections(pos); }); - for(auto h : actorObjectMap) - { - delete h.first; - } + if(NKAI_GRAPH_TRACE_LEVEL >= 1) + dumpToLog("graph"); - for(auto boat : boats) - { - delete boat; - } - -#if NKAI_GRAPH_TRACE_LEVEL >= 1 - dumpToLog("graph"); -#endif } void ObjectGraph::addObject(const CGObjectInstance * obj) @@ -199,14 +219,15 @@ void ObjectGraph::dumpToLog(std::string visualKey) const { for(auto & node : tile.second.connections) { -#if NKAI_GRAPH_TRACE_LEVEL >= 2 - logAi->trace( - "%s -> %s: %f !%d", - node.first.toString(), - tile.first.toString(), - node.second.cost, - node.second.danger); -#endif + if(NKAI_GRAPH_TRACE_LEVEL >= 2) + { + logAi->trace( + "%s -> %s: %f !%d", + node.first.toString(), + tile.first.toString(), + node.second.cost, + node.second.danger); + } logBuilder.addLine(tile.first, node.first); } @@ -278,14 +299,15 @@ void GraphPaths::dumpToLog() const if(!node.previous.valid()) continue; -#if NKAI_GRAPH_TRACE_LEVEL >= 2 - logAi->trace( - "%s -> %s: %f !%d", - node.previous.coord.toString(), - tile.first.toString(), - node.cost, - node.danger); -#endif + if(NKAI_GRAPH_TRACE_LEVEL >= 2) + { + logAi->trace( + "%s -> %s: %f !%d", + node.previous.coord.toString(), + tile.first.toString(), + node.cost, + node.danger); + } logBuilder.addLine(node.previous.coord, tile.first); } @@ -343,7 +365,7 @@ void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHe auto currentNode = currentTile->second[current.nodeType]; - if(currentNode.cost == 0) + if(!currentNode.previous.valid()) break; allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE; @@ -352,7 +374,7 @@ void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHe tilesToPass.push_back(current.coord); - if(currentNode.cost < 2) + if(currentNode.cost < 2.0f) break; current = currentNode.previous; @@ -377,6 +399,7 @@ void GraphPaths::addChainInfo(std::vector & paths, int3 tile, const CGHe n.turns = static_cast(cost) + 1; // just in case lets select worst scenario n.danger = danger; n.targetHero = hero; + n.parentIndex = 0; for(auto & node : path.nodes) { diff --git a/AI/Nullkiller/Pathfinding/ObjectGraph.h b/AI/Nullkiller/Pathfinding/ObjectGraph.h index faf6fb499..4407dfc93 100644 --- a/AI/Nullkiller/Pathfinding/ObjectGraph.h +++ b/AI/Nullkiller/Pathfinding/ObjectGraph.h @@ -76,6 +76,8 @@ public: { return nodes.at(tile); } + + bool tryAddConnection(const int3 & from, const int3 & to, float cost, uint64_t danger); }; struct GraphPathNode; diff --git a/AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp b/AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp index 3f0acd1b2..59368e9c4 100644 --- a/AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp +++ b/AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp @@ -45,7 +45,7 @@ namespace AIPathfinding if(!allowBypassObjects) { - if (source.node->getCost() == 0) + if (source.node->getCost() < 0.0001f) return; // when actor represents moster graph node, we need to let him escape monster