2023-06-21 13:46:09 +03:00
/*
* 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
{
2023-06-22 14:42:24 +03:00
const float currentCost = destination . cost ;
const int currentTurnsUsed = destination . turn ;
const int currentMovePointsLeft = destination . movementLeft ;
const int sourceLayerMaxMovePoints = pathfinderHelper -> getMaxMovePoints ( source . node -> layer );
2023-06-21 13:46:09 +03:00
2023-06-22 14:42:24 +03:00
int moveCostPoints = pathfinderHelper -> getMovementCost ( source , destination , currentMovePointsLeft );
float destinationCost = currentCost ;
int destTurnsUsed = currentTurnsUsed ;
int destMovePointsLeft = currentMovePointsLeft ;
if ( currentMovePointsLeft < moveCostPoints )
2023-06-21 13:46:09 +03:00
{
2023-06-22 14:42:24 +03:00
// 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
2023-06-21 13:46:09 +03:00
2023-06-22 14:42:24 +03:00
destinationCost += static_cast < float > ( currentMovePointsLeft ) / sourceLayerMaxMovePoints ;
destTurnsUsed += 1 ;
destMovePointsLeft = sourceLayerMaxMovePoints ;
2023-06-21 13:46:09 +03:00
2023-06-22 14:42:24 +03:00
// update move cost - it might have changed since hero now makes next turn and replenished his pool
moveCostPoints = pathfinderHelper -> getMovementCost ( source , destination , destMovePointsLeft );
2023-06-21 13:46:09 +03:00
2023-06-22 14:42:24 +03:00
pathfinderHelper -> updateTurnInfo ( destTurnsUsed );
2023-06-21 13:46:09 +03:00
}
2023-06-21 15:38:57 +03:00
if ( destination . action == EPathNodeAction :: EMBARK || destination . action == EPathNodeAction :: DISEMBARK )
2023-06-21 13:46:09 +03:00
{
2023-06-22 14:42:24 +03:00
// 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 < float > ( destMovePointsLeft ) / sourceLayerMaxMovePoints ;
const float costAfterConversion = static_cast < float > ( 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 < float > ( moveCostPoints ) / sourceLayerMaxMovePoints ;
2023-06-21 13:46:09 +03:00
}
2023-06-22 14:42:24 +03:00
// pathfinder / priority queue does not supports negative costs
assert ( destinationCost >= currentCost );
2023-06-21 13:46:09 +03:00
2023-06-22 14:42:24 +03:00
destination . cost = destinationCost ;
destination . turn = destTurnsUsed ;
destination . movementLeft = destMovePointsLeft ;
2023-06-21 13:46:09 +03:00
if ( destination . isBetterWay () &&
2023-06-22 14:42:24 +03:00
(( source . node -> turns == destTurnsUsed && destMovePointsLeft ) || pathfinderHelper -> passOneTurnLimitCheck ( source )))
2023-06-21 13:46:09 +03:00
{
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
{
2023-06-21 15:38:57 +03:00
if ( destination . action != EPathNodeAction :: UNKNOWN )
2023-06-21 13:46:09 +03:00
{
#ifdef VCMI_TRACE_PATHFINDER
logAi -> trace ( "Accepted precalculated action at %s" , destination . coord . toString ());
#endif
return ;
}
2023-06-21 15:38:57 +03:00
EPathNodeAction action = EPathNodeAction :: NORMAL ;
2023-06-21 13:46:09 +03:00
const auto * hero = pathfinderHelper -> hero ;
switch ( destination . node -> layer )
{
case EPathfindingLayer :: LAND :
if ( source . node -> layer == EPathfindingLayer :: SAIL )
{
// TODO: Handle dismebark into guarded areaa
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: DISEMBARK ;
2023-06-21 13:46:09 +03:00
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 )
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: EMBARK ;
2023-06-21 13:46:09 +03:00
else if ( destination . nodeHero )
{
if ( destination . heroRelations == PlayerRelations :: ENEMIES )
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: BATTLE ;
2023-06-21 13:46:09 +03:00
else
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: BLOCKING_VISIT ;
2023-06-21 13:46:09 +03:00
}
else if ( destination . nodeObject -> ID == Obj :: TOWN )
{
if ( destination . nodeObject -> passableFor ( hero -> tempOwner ))
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: VISIT ;
2023-06-21 13:46:09 +03:00
else if ( objRel == PlayerRelations :: ENEMIES )
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: BATTLE ;
2023-06-21 13:46:09 +03:00
}
else if ( destination . nodeObject -> ID == Obj :: GARRISON || destination . nodeObject -> ID == Obj :: GARRISON2 )
{
if ( destination . nodeObject -> passableFor ( hero -> tempOwner ))
{
if ( destination . guarded )
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: BATTLE ;
2023-06-21 13:46:09 +03:00
}
else if ( objRel == PlayerRelations :: ENEMIES )
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: BATTLE ;
2023-06-21 13:46:09 +03:00
}
else if ( destination . nodeObject -> ID == Obj :: BORDER_GATE )
{
if ( destination . nodeObject -> passableFor ( hero -> tempOwner ))
{
if ( destination . guarded )
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: BATTLE ;
2023-06-21 13:46:09 +03:00
}
else
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: BLOCKING_VISIT ;
2023-06-21 13:46:09 +03:00
}
else if ( destination . isGuardianTile )
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: BATTLE ;
2023-06-21 16:49:44 +03:00
else if ( destination . nodeObject -> isBlockedVisitable () && ! ( pathfinderConfig -> options . useCastleGate && destination . nodeObject -> ID == Obj :: TOWN ))
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: BLOCKING_VISIT ;
2023-06-21 13:46:09 +03:00
2023-06-21 15:38:57 +03:00
if ( action == EPathNodeAction :: NORMAL )
2023-06-21 13:46:09 +03:00
{
if ( destination . guarded )
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: BATTLE ;
2023-06-21 13:46:09 +03:00
else
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: VISIT ;
2023-06-21 13:46:09 +03:00
}
}
else if ( destination . guarded )
2023-06-21 15:38:57 +03:00
action = EPathNodeAction :: BATTLE ;
2023-06-21 13:46:09 +03:00
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 );
2023-06-21 15:38:57 +03:00
if ( blocker == BlockingReason :: DESTINATION_GUARDED && destination . action == EPathNodeAction :: BATTLE )
2023-06-21 13:46:09 +03:00
{
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
2023-06-21 15:38:57 +03:00
case EPathNodeAction :: VISIT :
2023-06-21 13:46:09 +03:00
{
/// 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 < const CGTeleport *> ( 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 ;
}
2023-06-21 15:38:57 +03:00
case EPathNodeAction :: BLOCKING_VISIT :
2023-06-21 13:46:09 +03:00
return destination . guarded
? BlockingReason :: DESTINATION_GUARDED
: BlockingReason :: DESTINATION_BLOCKVIS ;
2023-06-21 15:38:57 +03:00
case EPathNodeAction :: NORMAL :
2023-06-21 13:46:09 +03:00
return BlockingReason :: NONE ;
2023-06-21 15:38:57 +03:00
case EPathNodeAction :: EMBARK :
2023-06-21 13:46:09 +03:00
if ( pathfinderHelper -> options . useEmbarkAndDisembark )
return BlockingReason :: NONE ;
return BlockingReason :: DESTINATION_BLOCKED ;
2023-06-21 15:38:57 +03:00
case EPathNodeAction :: DISEMBARK :
2023-06-21 13:46:09 +03:00
if ( pathfinderHelper -> options . useEmbarkAndDisembark )
return destination . guarded ? BlockingReason :: DESTINATION_GUARDED : BlockingReason :: NONE ;
return BlockingReason :: DESTINATION_BLOCKED ;
2023-06-21 15:38:57 +03:00
case EPathNodeAction :: BATTLE :
2023-06-21 13:46:09 +03:00
/// Movement after BATTLE action only possible from guarded tile to guardian tile
if ( destination . guarded )
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
{
2023-06-21 15:38:57 +03:00
if ( destination . node -> accessible == EPathAccessibility :: BLOCKED )
2023-06-21 13:46:09 +03:00
return BlockingReason :: DESTINATION_BLOCKED ;
switch ( destination . node -> layer )
{
case EPathfindingLayer :: LAND :
if ( ! pathfinderHelper -> canMoveBetween ( source . coord , destination . coord ))
return BlockingReason :: DESTINATION_BLOCKED ;
if ( source . guarded )
{
if ( ! ( pathfinderConfig -> options . originalMovementRules && source . node -> layer == EPathfindingLayer :: AIR ) &&
! destination . isGuardianTile ) // 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
2023-06-21 15:38:57 +03:00
if ( source . node -> action != EPathNodeAction :: EMBARK && ! destination . isGuardianTile )
2023-06-21 13:46:09 +03:00
return BlockingReason :: SOURCE_GUARDED ;
}
if ( source . node -> layer == EPathfindingLayer :: LAND )
{
if ( ! destination . isNodeObjectVisitable ())
return BlockingReason :: DESTINATION_BLOCKED ;
2023-06-21 16:49:44 +03:00
if ( ! destination . nodeHero && ! destination . nodeObject -> isCoastVisitable ())
2023-06-21 13:46:09 +03:00
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 )
2023-06-21 15:38:57 +03:00
|| destination . node -> accessible != EPathAccessibility :: ACCESSIBLE )
2023-06-21 13:46:09 +03:00
{
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 )
{
case EPathfindingLayer :: LAND :
if ( destination . node -> layer == EPathfindingLayer :: SAIL )
{
/// Cannot enter empty water tile from land -> it has to be visitable
2023-06-21 15:38:57 +03:00
if ( destination . node -> accessible == EPathAccessibility :: ACCESSIBLE )
2023-06-21 13:46:09 +03:00
destination . blocked = true ;
}
break ;
case EPathfindingLayer :: SAIL :
//tile must be accessible -> exception: unblocked blockvis tiles -> clear but guarded by nearby monster coast
2023-06-21 15:38:57 +03:00
if (( destination . node -> accessible != EPathAccessibility :: ACCESSIBLE && ( destination . node -> accessible != EPathAccessibility :: BLOCKVIS || destination . tile -> blocked ))
2023-06-21 13:46:09 +03:00
|| destination . tile -> visitable ) //TODO: passableness problem -> town says it's passable (thus accessible) but we obviously can't disembark onto town gate
{
destination . blocked = true ;
}
break ;
case EPathfindingLayer :: AIR :
if ( pathfinderConfig -> options . originalMovementRules )
{
2023-06-21 15:38:57 +03:00
if (( source . node -> accessible != EPathAccessibility :: ACCESSIBLE &&
source . node -> accessible != EPathAccessibility :: VISITABLE ) &&
( destination . node -> accessible != EPathAccessibility :: VISITABLE &&
destination . node -> accessible != EPathAccessibility :: ACCESSIBLE ))
2023-06-21 13:46:09 +03:00
{
destination . blocked = true ;
}
}
2023-06-21 15:38:57 +03:00
else if ( destination . node -> accessible != EPathAccessibility :: ACCESSIBLE )
2023-06-21 13:46:09 +03:00
{
/// Hero that fly can only land on accessible tiles
if ( destination . nodeObject )
destination . blocked = true ;
}
break ;
case EPathfindingLayer :: WATER :
2023-06-21 15:38:57 +03:00
if ( destination . node -> accessible != EPathAccessibility :: ACCESSIBLE && destination . node -> accessible != EPathAccessibility :: VISITABLE )
2023-06-21 13:46:09 +03:00
{
/// 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