/* * PathfindingRules.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 "PathfindingRules.h" #include "CGPathNode.h" #include "CPathfinder.h" #include "INodeStorage.h" #include "PathfinderOptions.h" #include "../mapObjects/CGHeroInstance.h" #include "../mapObjects/MiscObjects.h" #include "../mapping/CMapDefines.h" VCMI_LIB_NAMESPACE_BEGIN void MovementCostRule::process( const PathNodeInfo & source, CDestinationNodeInfo & destination, const PathfinderConfig * pathfinderConfig, CPathfinderHelper * pathfinderHelper) const { const float currentCost = destination.cost; const int currentTurnsUsed = destination.turn; const int currentMovePointsLeft = destination.movementLeft; const int sourceLayerMaxMovePoints = pathfinderHelper->getMaxMovePoints(source.node->layer); int moveCostPoints = pathfinderHelper->getMovementCost(source, destination, currentMovePointsLeft); float destinationCost = currentCost; int destTurnsUsed = currentTurnsUsed; int destMovePointsLeft = currentMovePointsLeft; if(currentMovePointsLeft < moveCostPoints) { // occurs rarely, when hero with low movepoints tries to leave the road // in this case, all remaining movement points from current turn are spent // and actual movement will happen on next turn, spending points from next turn pool destinationCost += static_cast(currentMovePointsLeft) / sourceLayerMaxMovePoints; destTurnsUsed += 1; destMovePointsLeft = sourceLayerMaxMovePoints; // update move cost - it might have changed since hero now makes next turn and replenished his pool moveCostPoints = pathfinderHelper->getMovementCost(source, destination, destMovePointsLeft); pathfinderHelper->updateTurnInfo(destTurnsUsed); } if(destination.action == EPathNodeAction::EMBARK || destination.action == EPathNodeAction::DISEMBARK) { // FREE_SHIP_BOARDING bonus only remove additional penalty // land <-> sail transition still cost movement points as normal movement const int movementPointsAfterEmbark = pathfinderHelper->movementPointsAfterEmbark(destMovePointsLeft, moveCostPoints, (destination.action == EPathNodeAction::DISEMBARK)); const int destinationLayerMaxMovePoints = pathfinderHelper->getMaxMovePoints(destination.node->layer); const float costBeforeConversion = static_cast(destMovePointsLeft) / sourceLayerMaxMovePoints; const float costAfterConversion = static_cast(movementPointsAfterEmbark) / destinationLayerMaxMovePoints; const float costDelta = costBeforeConversion - costAfterConversion; assert(costDelta >= 0); destMovePointsLeft = movementPointsAfterEmbark; destinationCost += costDelta; } else { // Standard movement assert(destMovePointsLeft >= moveCostPoints); destMovePointsLeft -= moveCostPoints; destinationCost += static_cast(moveCostPoints) / sourceLayerMaxMovePoints; } // pathfinder / priority queue does not supports negative costs assert(destinationCost >= currentCost); destination.cost = destinationCost; destination.turn = destTurnsUsed; destination.movementLeft = destMovePointsLeft; if(destination.isBetterWay() && ((source.node->turns == destTurnsUsed && destMovePointsLeft) || pathfinderHelper->passOneTurnLimitCheck(source))) { pathfinderConfig->nodeStorage->commit(destination, source); return; } destination.blocked = true; } void PathfinderBlockingRule::process( const PathNodeInfo & source, CDestinationNodeInfo & destination, const PathfinderConfig * pathfinderConfig, CPathfinderHelper * pathfinderHelper) const { auto blockingReason = getBlockingReason(source, destination, pathfinderConfig, pathfinderHelper); destination.blocked = blockingReason != BlockingReason::NONE; } void DestinationActionRule::process( const PathNodeInfo & source, CDestinationNodeInfo & destination, const PathfinderConfig * pathfinderConfig, CPathfinderHelper * pathfinderHelper) const { if(destination.action != EPathNodeAction::UNKNOWN) { #ifdef VCMI_TRACE_PATHFINDER logAi->trace("Accepted precalculated action at %s", destination.coord.toString()); #endif return; } EPathNodeAction action = EPathNodeAction::NORMAL; const auto * hero = pathfinderHelper->hero; switch(destination.node->layer.toEnum()) { case EPathfindingLayer::LAND: if(source.node->layer == EPathfindingLayer::SAIL) { // TODO: Handle dismebark into guarded areaa action = EPathNodeAction::DISEMBARK; break; } /// don't break - next case shared for both land and sail layers [[fallthrough]]; case EPathfindingLayer::SAIL: if(destination.isNodeObjectVisitable()) { auto objRel = destination.objectRelations; if(destination.nodeObject->ID == Obj::BOAT) action = EPathNodeAction::EMBARK; else if(destination.nodeHero) { if(destination.heroRelations == PlayerRelations::ENEMIES) action = EPathNodeAction::BATTLE; else action = EPathNodeAction::BLOCKING_VISIT; } else if(destination.nodeObject->ID == Obj::TOWN) { if(destination.nodeObject->passableFor(hero->tempOwner)) action = EPathNodeAction::VISIT; else if(objRel == PlayerRelations::ENEMIES) action = EPathNodeAction::BATTLE; } else if(destination.nodeObject->ID == Obj::GARRISON || destination.nodeObject->ID == Obj::GARRISON2) { if(destination.nodeObject->passableFor(hero->tempOwner)) { if(destination.guarded) action = EPathNodeAction::BATTLE; } else if(objRel == PlayerRelations::ENEMIES) action = EPathNodeAction::BATTLE; } else if(destination.nodeObject->ID == Obj::BORDER_GATE) { if(destination.nodeObject->passableFor(hero->tempOwner)) { if(destination.guarded) action = EPathNodeAction::BATTLE; } else action = EPathNodeAction::BLOCKING_VISIT; } else if(destination.isGuardianTile) action = EPathNodeAction::BATTLE; else if(destination.nodeObject->isBlockedVisitable() && !(pathfinderConfig->options.useCastleGate && destination.nodeObject->ID == Obj::TOWN)) action = EPathNodeAction::BLOCKING_VISIT; if(action == EPathNodeAction::NORMAL) { if(destination.guarded) action = EPathNodeAction::BATTLE; else action = EPathNodeAction::VISIT; } } else if(destination.guarded) action = EPathNodeAction::BATTLE; break; } destination.action = action; } void MovementAfterDestinationRule::process( const PathNodeInfo & source, CDestinationNodeInfo & destination, const PathfinderConfig * config, CPathfinderHelper * pathfinderHelper) const { auto blocker = getBlockingReason(source, destination, config, pathfinderHelper); if(blocker == BlockingReason::DESTINATION_GUARDED && destination.action == EPathNodeAction::BATTLE) { return; // allow bypass guarded tile but only in direction of guard, a bit UI related thing } destination.blocked = blocker != BlockingReason::NONE; } PathfinderBlockingRule::BlockingReason MovementAfterDestinationRule::getBlockingReason( const PathNodeInfo & source, const CDestinationNodeInfo & destination, const PathfinderConfig * config, const CPathfinderHelper * pathfinderHelper) const { switch(destination.action) { /// TODO: Investigate what kind of limitation is possible to apply on movement from visitable tiles /// Likely in many cases we don't need to add visitable tile to queue when hero doesn't fly case EPathNodeAction::VISIT: { /// For now we only add visitable tile into queue when it's teleporter that allow transit /// Movement from visitable tile when hero is standing on it is possible into any layer const auto * objTeleport = dynamic_cast(destination.nodeObject); if(pathfinderHelper->isAllowedTeleportEntrance(objTeleport)) { /// For now we'll always allow transit over teleporters /// Transit over whirlpools only allowed when hero is protected return BlockingReason::NONE; } else if(destination.nodeObject->ID == Obj::GARRISON || destination.nodeObject->ID == Obj::GARRISON2 || destination.nodeObject->ID == Obj::BORDER_GATE) { /// Transit via unguarded garrisons is always possible return BlockingReason::NONE; } return BlockingReason::DESTINATION_VISIT; } case EPathNodeAction::BLOCKING_VISIT: return BlockingReason::DESTINATION_BLOCKVIS; case EPathNodeAction::NORMAL: return BlockingReason::NONE; case EPathNodeAction::EMBARK: if(pathfinderHelper->options.useEmbarkAndDisembark) return BlockingReason::NONE; return BlockingReason::DESTINATION_BLOCKED; case EPathNodeAction::DISEMBARK: if(pathfinderHelper->options.useEmbarkAndDisembark) return destination.guarded ? BlockingReason::DESTINATION_GUARDED : BlockingReason::NONE; return BlockingReason::DESTINATION_BLOCKED; case EPathNodeAction::BATTLE: // H3 rule: do not allow direct attack on wandering monsters if hero lands on visitable object if (config->options.originalFlyRules && destination.nodeObject && source.node->layer == EPathfindingLayer::AIR) return BlockingReason::DESTINATION_BLOCKED; // Movement after BATTLE action only possible from guarded tile to guardian tile if(destination.guarded) { if (pathfinderHelper->options.ignoreGuards) return BlockingReason::NONE; else return BlockingReason::DESTINATION_GUARDED; } break; } return BlockingReason::DESTINATION_BLOCKED; } PathfinderBlockingRule::BlockingReason MovementToDestinationRule::getBlockingReason( const PathNodeInfo & source, const CDestinationNodeInfo & destination, const PathfinderConfig * pathfinderConfig, const CPathfinderHelper * pathfinderHelper) const { if(destination.node->accessible == EPathAccessibility::BLOCKED) return BlockingReason::DESTINATION_BLOCKED; switch(destination.node->layer.toEnum()) { case EPathfindingLayer::LAND: if(!pathfinderHelper->canMoveBetween(source.coord, destination.coord)) return BlockingReason::DESTINATION_BLOCKED; if(source.guarded) { if(source.node->layer != EPathfindingLayer::AIR // zone of control is ignored when flying && !pathfinderConfig->options.ignoreGuards && (!destination.isGuardianTile || pathfinderHelper->getGuardiansCount(source.coord) > 1)) // Can step into tile of guard { return BlockingReason::SOURCE_GUARDED; } } break; case EPathfindingLayer::SAIL: if(!pathfinderHelper->canMoveBetween(source.coord, destination.coord)) return BlockingReason::DESTINATION_BLOCKED; if(source.guarded) { // Hero embarked a boat standing on a guarded tile -> we must allow to move away from that tile if(source.node->action != EPathNodeAction::EMBARK && !destination.isGuardianTile) return BlockingReason::SOURCE_GUARDED; } if(source.node->layer == EPathfindingLayer::LAND) { if(!destination.isNodeObjectVisitable()) return BlockingReason::DESTINATION_BLOCKED; if(!destination.nodeHero && !destination.nodeObject->isCoastVisitable()) return BlockingReason::DESTINATION_BLOCKED; } else if(destination.isNodeObjectVisitable() && destination.nodeObject->ID == Obj::BOAT) { /// Hero in boat can't visit empty boats return BlockingReason::DESTINATION_BLOCKED; } break; case EPathfindingLayer::WATER: if(!pathfinderHelper->canMoveBetween(source.coord, destination.coord)) return BlockingReason::DESTINATION_BLOCKED; if (destination.node->accessible != EPathAccessibility::ACCESSIBLE) return BlockingReason::DESTINATION_BLOCKED; if(destination.guarded) return BlockingReason::DESTINATION_BLOCKED; break; } return BlockingReason::NONE; } void LayerTransitionRule::process( const PathNodeInfo & source, CDestinationNodeInfo & destination, const PathfinderConfig * pathfinderConfig, CPathfinderHelper * pathfinderHelper) const { if(source.node->layer == destination.node->layer) return; switch(source.node->layer.toEnum()) { case EPathfindingLayer::LAND: if(destination.node->layer == EPathfindingLayer::SAIL) { /// Cannot enter empty water tile from land -> it has to be visitable if(destination.node->accessible == EPathAccessibility::ACCESSIBLE) destination.blocked = true; } break; case EPathfindingLayer::SAIL: // have to disembark first before visiting objects on land if (destination.tile->visitable()) destination.blocked = true; //can disembark only on accessible tiles or tiles guarded by nearby monster if((destination.node->accessible != EPathAccessibility::ACCESSIBLE && destination.node->accessible != EPathAccessibility::GUARDED)) destination.blocked = true; break; case EPathfindingLayer::AIR: if(pathfinderConfig->options.originalFlyRules) { if(source.node->accessible != EPathAccessibility::ACCESSIBLE && source.node->accessible != EPathAccessibility::VISITABLE) { if (destination.node->accessible == EPathAccessibility::BLOCKVIS) { // Can't visit 'blockvisit' objects on coast if hero will end up on water terrain if (source.tile->blocked() || !destination.tile->entrableTerrain(source.tile)) destination.blocked = true; } } } else { // Hero that fly can only land on accessible tiles if(destination.node->accessible != EPathAccessibility::ACCESSIBLE && destination.nodeObject) destination.blocked = true; } break; case EPathfindingLayer::WATER: if(destination.node->accessible != EPathAccessibility::ACCESSIBLE && destination.node->accessible != EPathAccessibility::VISITABLE) { /// Hero that walking on water can transit to accessible and visitable tiles /// Though hero can't interact with blocking visit objects while standing on water destination.blocked = true; } break; } } VCMI_LIB_NAMESPACE_END