1
0
mirror of https://github.com/vcmi/vcmi.git synced 2024-12-14 10:12:59 +02:00
vcmi/lib/pathfinder/PathfindingRules.cpp

427 lines
14 KiB
C++
Raw Normal View History

/*
* 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<float>(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<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;
}
// 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<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;
}
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:
2024-05-28 15:37:42 +02:00
// 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;
2024-05-28 15:37:42 +02:00
// 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)
{
2024-05-28 15:37:42 +02:00
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:
2024-05-23 20:47:13 +02:00
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:
2024-05-28 15:37:42 +02:00
// have to disembark first before visiting objects on land
if (destination.tile->visitable())
2024-05-28 15:37:42 +02:00
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:
2024-02-07 20:27:02 +02:00
if(pathfinderConfig->options.originalFlyRules)
{
2024-05-28 15:37:42 +02:00
if(source.node->accessible != EPathAccessibility::ACCESSIBLE && source.node->accessible != EPathAccessibility::VISITABLE)
{
if (destination.node->accessible == EPathAccessibility::BLOCKVIS)
{
2024-05-28 15:37:42 +02:00
// 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;
}
}
}
2024-05-28 15:37:42 +02:00
else
{
2024-05-28 15:37:42 +02:00
// 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