mirror of
https://github.com/vcmi/vcmi.git
synced 2024-12-28 23:06:24 +02:00
95503d0623
1. Now there is only one bonus: MOVEMENT, with 2 subtypes: 0 is sea, 1 is land For movement value on land depends on creature speed we use a new ARMY_MOVEMENT updater with global bonus. If we does not like such dependency, we can just remove this updater from json. 2. All specialities and secondary skills for movement moved to new system AFAIK
1432 lines
40 KiB
C++
1432 lines
40 KiB
C++
/*
|
|
* CPathfinder.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 "CPathfinder.h"
|
|
|
|
#include "CHeroHandler.h"
|
|
#include "mapping/CMap.h"
|
|
#include "CGameState.h"
|
|
#include "mapObjects/CGHeroInstance.h"
|
|
#include "GameConstants.h"
|
|
#include "CStopWatch.h"
|
|
#include "CConfigHandler.h"
|
|
#include "CPlayerState.h"
|
|
#include "PathfinderUtil.h"
|
|
|
|
VCMI_LIB_NAMESPACE_BEGIN
|
|
|
|
bool canSeeObj(const CGObjectInstance * obj)
|
|
{
|
|
/// Pathfinder should ignore placed events
|
|
return obj != nullptr && obj->ID != Obj::EVENT;
|
|
}
|
|
|
|
void NodeStorage::initialize(const PathfinderOptions & options, const CGameState * gs)
|
|
{
|
|
//TODO: fix this code duplication with AINodeStorage::initialize, problem is to keep `resetTile` inline
|
|
|
|
int3 pos;
|
|
const PlayerColor player = out.hero->tempOwner;
|
|
const int3 sizes = gs->getMapSize();
|
|
const auto fow = static_cast<const CGameInfoCallback *>(gs)->getPlayerTeam(player)->fogOfWarMap;
|
|
|
|
//make 200% sure that these are loop invariants (also a bit shorter code), let compiler do the rest(loop unswitching)
|
|
const bool useFlying = options.useFlying;
|
|
const bool useWaterWalking = options.useWaterWalking;
|
|
|
|
for(pos.z=0; pos.z < sizes.z; ++pos.z)
|
|
{
|
|
for(pos.x=0; pos.x < sizes.x; ++pos.x)
|
|
{
|
|
for(pos.y=0; pos.y < sizes.y; ++pos.y)
|
|
{
|
|
const TerrainTile tile = gs->map->getTile(pos);
|
|
if(tile.terType->isWater())
|
|
{
|
|
resetTile(pos, ELayer::SAIL, PathfinderUtil::evaluateAccessibility<ELayer::SAIL>(pos, tile, fow, player, gs));
|
|
if(useFlying)
|
|
resetTile(pos, ELayer::AIR, PathfinderUtil::evaluateAccessibility<ELayer::AIR>(pos, tile, fow, player, gs));
|
|
if(useWaterWalking)
|
|
resetTile(pos, ELayer::WATER, PathfinderUtil::evaluateAccessibility<ELayer::WATER>(pos, tile, fow, player, gs));
|
|
}
|
|
if(tile.terType->isLand())
|
|
{
|
|
resetTile(pos, ELayer::LAND, PathfinderUtil::evaluateAccessibility<ELayer::LAND>(pos, tile, fow, player, gs));
|
|
if(useFlying)
|
|
resetTile(pos, ELayer::AIR, PathfinderUtil::evaluateAccessibility<ELayer::AIR>(pos, tile, fow, player, gs));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<CGPathNode *> NodeStorage::calculateNeighbours(
|
|
const PathNodeInfo & source,
|
|
const PathfinderConfig * pathfinderConfig,
|
|
const CPathfinderHelper * pathfinderHelper)
|
|
{
|
|
std::vector<CGPathNode *> neighbours;
|
|
neighbours.reserve(16);
|
|
auto accessibleNeighbourTiles = pathfinderHelper->getNeighbourTiles(source);
|
|
|
|
for(auto & neighbour : accessibleNeighbourTiles)
|
|
{
|
|
for(EPathfindingLayer i = EPathfindingLayer::LAND; i <= EPathfindingLayer::AIR; i.advance(1))
|
|
{
|
|
auto node = getNode(neighbour, i);
|
|
|
|
if(node->accessible == CGPathNode::NOT_SET)
|
|
continue;
|
|
|
|
neighbours.push_back(node);
|
|
}
|
|
}
|
|
|
|
return neighbours;
|
|
}
|
|
|
|
std::vector<CGPathNode *> NodeStorage::calculateTeleportations(
|
|
const PathNodeInfo & source,
|
|
const PathfinderConfig * pathfinderConfig,
|
|
const CPathfinderHelper * pathfinderHelper)
|
|
{
|
|
std::vector<CGPathNode *> neighbours;
|
|
|
|
if(!source.isNodeObjectVisitable())
|
|
return neighbours;
|
|
|
|
auto accessibleExits = pathfinderHelper->getTeleportExits(source);
|
|
|
|
for(auto & neighbour : accessibleExits)
|
|
{
|
|
auto node = getNode(neighbour, source.node->layer);
|
|
|
|
neighbours.push_back(node);
|
|
}
|
|
|
|
return neighbours;
|
|
}
|
|
|
|
std::vector<int3> CPathfinderHelper::getNeighbourTiles(const PathNodeInfo & source) const
|
|
{
|
|
std::vector<int3> neighbourTiles;
|
|
neighbourTiles.reserve(16);
|
|
|
|
getNeighbours(
|
|
*source.tile,
|
|
source.node->coord,
|
|
neighbourTiles,
|
|
boost::logic::indeterminate,
|
|
source.node->layer == EPathfindingLayer::SAIL);
|
|
|
|
if(source.isNodeObjectVisitable())
|
|
{
|
|
vstd::erase_if(neighbourTiles, [&](int3 tile) -> bool
|
|
{
|
|
return !canMoveBetween(tile, source.nodeObject->visitablePos());
|
|
});
|
|
}
|
|
|
|
return neighbourTiles;
|
|
}
|
|
|
|
NodeStorage::NodeStorage(CPathsInfo & pathsInfo, const CGHeroInstance * hero)
|
|
:out(pathsInfo)
|
|
{
|
|
out.hero = hero;
|
|
out.hpos = hero->visitablePos();
|
|
}
|
|
|
|
void NodeStorage::resetTile(
|
|
const int3 & tile,
|
|
EPathfindingLayer layer,
|
|
CGPathNode::EAccessibility accessibility)
|
|
{
|
|
getNode(tile, layer)->update(tile, layer, accessibility);
|
|
}
|
|
|
|
std::vector<CGPathNode *> NodeStorage::getInitialNodes()
|
|
{
|
|
auto initialNode = getNode(out.hpos, out.hero->boat ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND);
|
|
|
|
initialNode->turns = 0;
|
|
initialNode->moveRemains = out.hero->movement;
|
|
initialNode->setCost(0.0);
|
|
|
|
if(!initialNode->coord.valid())
|
|
{
|
|
initialNode->coord = out.hpos;
|
|
}
|
|
|
|
return std::vector<CGPathNode *> { initialNode };
|
|
}
|
|
|
|
void NodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInfo & source)
|
|
{
|
|
assert(destination.node != source.node->theNodeBefore); //two tiles can't point to each other
|
|
destination.node->setCost(destination.cost);
|
|
destination.node->moveRemains = destination.movementLeft;
|
|
destination.node->turns = destination.turn;
|
|
destination.node->theNodeBefore = source.node;
|
|
destination.node->action = destination.action;
|
|
}
|
|
|
|
PathfinderOptions::PathfinderOptions()
|
|
{
|
|
useFlying = settings["pathfinder"]["layers"]["flying"].Bool();
|
|
useWaterWalking = settings["pathfinder"]["layers"]["waterWalking"].Bool();
|
|
useEmbarkAndDisembark = settings["pathfinder"]["layers"]["sailing"].Bool();
|
|
useTeleportTwoWay = settings["pathfinder"]["teleports"]["twoWay"].Bool();
|
|
useTeleportOneWay = settings["pathfinder"]["teleports"]["oneWay"].Bool();
|
|
useTeleportOneWayRandom = settings["pathfinder"]["teleports"]["oneWayRandom"].Bool();
|
|
useTeleportWhirlpool = settings["pathfinder"]["teleports"]["whirlpool"].Bool();
|
|
|
|
useCastleGate = settings["pathfinder"]["teleports"]["castleGate"].Bool();
|
|
|
|
lightweightFlyingMode = settings["pathfinder"]["lightweightFlyingMode"].Bool();
|
|
oneTurnSpecialLayersLimit = settings["pathfinder"]["oneTurnSpecialLayersLimit"].Bool();
|
|
originalMovementRules = settings["pathfinder"]["originalMovementRules"].Bool();
|
|
}
|
|
|
|
void MovementCostRule::process(
|
|
const PathNodeInfo & source,
|
|
CDestinationNodeInfo & destination,
|
|
const PathfinderConfig * pathfinderConfig,
|
|
CPathfinderHelper * pathfinderHelper) const
|
|
{
|
|
float costAtNextTile = destination.cost;
|
|
int turnAtNextTile = destination.turn;
|
|
int moveAtNextTile = destination.movementLeft;
|
|
int cost = pathfinderHelper->getMovementCost(source, destination, moveAtNextTile);
|
|
int remains = moveAtNextTile - cost;
|
|
int sourceLayerMaxMovePoints = pathfinderHelper->getMaxMovePoints(source.node->layer);
|
|
|
|
if(remains < 0)
|
|
{
|
|
//occurs rarely, when hero with low movepoints tries to leave the road
|
|
costAtNextTile += static_cast<float>(moveAtNextTile) / sourceLayerMaxMovePoints;//we spent all points of current turn
|
|
pathfinderHelper->updateTurnInfo(++turnAtNextTile);
|
|
|
|
int destinationLayerMaxMovePoints = pathfinderHelper->getMaxMovePoints(destination.node->layer);
|
|
|
|
moveAtNextTile = destinationLayerMaxMovePoints;
|
|
|
|
cost = pathfinderHelper->getMovementCost(source, destination, moveAtNextTile); //cost must be updated, movement points changed :(
|
|
remains = moveAtNextTile - cost;
|
|
}
|
|
|
|
if(destination.action == CGPathNode::EMBARK || destination.action == CGPathNode::DISEMBARK)
|
|
{
|
|
/// FREE_SHIP_BOARDING bonus only remove additional penalty
|
|
/// land <-> sail transition still cost movement points as normal movement
|
|
remains = pathfinderHelper->movementPointsAfterEmbark(moveAtNextTile, cost, (destination.action == CGPathNode::DISEMBARK));
|
|
cost = moveAtNextTile - remains;
|
|
}
|
|
|
|
costAtNextTile += static_cast<float>(cost) / sourceLayerMaxMovePoints;
|
|
|
|
destination.cost = costAtNextTile;
|
|
destination.turn = turnAtNextTile;
|
|
destination.movementLeft = remains;
|
|
|
|
if(destination.isBetterWay() &&
|
|
((source.node->turns == turnAtNextTile && remains) || pathfinderHelper->passOneTurnLimitCheck(source)))
|
|
{
|
|
pathfinderConfig->nodeStorage->commit(destination, source);
|
|
|
|
return;
|
|
}
|
|
|
|
destination.blocked = true;
|
|
}
|
|
|
|
PathfinderConfig::PathfinderConfig(
|
|
std::shared_ptr<INodeStorage> nodeStorage,
|
|
std::vector<std::shared_ptr<IPathfindingRule>> rules)
|
|
: nodeStorage(nodeStorage), rules(rules), options()
|
|
{
|
|
}
|
|
|
|
std::vector<std::shared_ptr<IPathfindingRule>> SingleHeroPathfinderConfig::buildRuleSet()
|
|
{
|
|
return std::vector<std::shared_ptr<IPathfindingRule>>{
|
|
std::make_shared<LayerTransitionRule>(),
|
|
std::make_shared<DestinationActionRule>(),
|
|
std::make_shared<MovementToDestinationRule>(),
|
|
std::make_shared<MovementCostRule>(),
|
|
std::make_shared<MovementAfterDestinationRule>()
|
|
};
|
|
}
|
|
|
|
SingleHeroPathfinderConfig::SingleHeroPathfinderConfig(CPathsInfo & out, CGameState * gs, const CGHeroInstance * hero)
|
|
: PathfinderConfig(std::make_shared<NodeStorage>(out, hero), buildRuleSet())
|
|
{
|
|
pathfinderHelper.reset(new CPathfinderHelper(gs, hero, options));
|
|
}
|
|
|
|
CPathfinderHelper * SingleHeroPathfinderConfig::getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs)
|
|
{
|
|
return pathfinderHelper.get();
|
|
}
|
|
|
|
CPathfinder::CPathfinder(
|
|
CGameState * _gs,
|
|
std::shared_ptr<PathfinderConfig> config)
|
|
: gamestate(_gs)
|
|
, config(config)
|
|
, source()
|
|
, destination()
|
|
{
|
|
initializeGraph();
|
|
}
|
|
|
|
|
|
void CPathfinder::push(CGPathNode * node)
|
|
{
|
|
if(node && !node->inPQ)
|
|
{
|
|
node->inPQ = true;
|
|
node->pq = &this->pq;
|
|
auto handle = pq.push(node);
|
|
node->pqHandle = handle;
|
|
}
|
|
}
|
|
|
|
CGPathNode * CPathfinder::topAndPop()
|
|
{
|
|
auto node = pq.top();
|
|
|
|
pq.pop();
|
|
node->inPQ = false;
|
|
node->pq = nullptr;
|
|
return node;
|
|
}
|
|
|
|
void CPathfinder::calculatePaths()
|
|
{
|
|
//logGlobal->info("Calculating paths for hero %s (adress %d) of player %d", hero->name, hero , hero->tempOwner);
|
|
|
|
//initial tile - set cost on 0 and add to the queue
|
|
std::vector<CGPathNode *> initialNodes = config->nodeStorage->getInitialNodes();
|
|
int counter = 0;
|
|
|
|
for(auto initialNode : initialNodes)
|
|
{
|
|
if(!gamestate->isInTheMap(initialNode->coord)/* || !gs->map->isInTheMap(dest)*/) //check input
|
|
{
|
|
logGlobal->error("CGameState::calculatePaths: Hero outside the gs->map? How dare you...");
|
|
throw std::runtime_error("Wrong checksum");
|
|
}
|
|
|
|
source.setNode(gamestate, initialNode);
|
|
auto hlp = config->getOrCreatePathfinderHelper(source, gamestate);
|
|
|
|
if(hlp->isHeroPatrolLocked())
|
|
continue;
|
|
|
|
pq.push(initialNode);
|
|
}
|
|
|
|
while(!pq.empty())
|
|
{
|
|
counter++;
|
|
auto node = topAndPop();
|
|
|
|
source.setNode(gamestate, node);
|
|
source.node->locked = true;
|
|
|
|
int movement = source.node->moveRemains;
|
|
uint8_t turn = source.node->turns;
|
|
float cost = source.node->getCost();
|
|
|
|
auto hlp = config->getOrCreatePathfinderHelper(source, gamestate);
|
|
|
|
hlp->updateTurnInfo(turn);
|
|
if(!movement)
|
|
{
|
|
hlp->updateTurnInfo(++turn);
|
|
movement = hlp->getMaxMovePoints(source.node->layer);
|
|
if(!hlp->passOneTurnLimitCheck(source))
|
|
continue;
|
|
}
|
|
|
|
source.isInitialPosition = source.nodeHero == hlp->hero;
|
|
source.updateInfo(hlp, gamestate);
|
|
|
|
//add accessible neighbouring nodes to the queue
|
|
auto neighbourNodes = config->nodeStorage->calculateNeighbours(source, config.get(), hlp);
|
|
for(CGPathNode * neighbour : neighbourNodes)
|
|
{
|
|
if(neighbour->locked)
|
|
continue;
|
|
|
|
if(!hlp->isLayerAvailable(neighbour->layer))
|
|
continue;
|
|
|
|
destination.setNode(gamestate, neighbour);
|
|
hlp = config->getOrCreatePathfinderHelper(destination, gamestate);
|
|
|
|
if(!hlp->isPatrolMovementAllowed(neighbour->coord))
|
|
continue;
|
|
|
|
/// Check transition without tile accessability rules
|
|
if(source.node->layer != neighbour->layer && !isLayerTransitionPossible())
|
|
continue;
|
|
|
|
destination.turn = turn;
|
|
destination.movementLeft = movement;
|
|
destination.cost = cost;
|
|
destination.updateInfo(hlp, gamestate);
|
|
destination.isGuardianTile = destination.guarded && isDestinationGuardian();
|
|
|
|
for(auto rule : config->rules)
|
|
{
|
|
rule->process(source, destination, config.get(), hlp);
|
|
|
|
if(destination.blocked)
|
|
break;
|
|
}
|
|
|
|
if(!destination.blocked)
|
|
push(destination.node);
|
|
|
|
} //neighbours loop
|
|
|
|
//just add all passable teleport exits
|
|
hlp = config->getOrCreatePathfinderHelper(source, gamestate);
|
|
|
|
/// For now we disable teleports usage for patrol movement
|
|
/// VCAI not aware about patrol and may stuck while attempt to use teleport
|
|
if(hlp->patrolState == CPathfinderHelper::PATROL_RADIUS)
|
|
continue;
|
|
|
|
auto teleportationNodes = config->nodeStorage->calculateTeleportations(source, config.get(), hlp);
|
|
for(CGPathNode * teleportNode : teleportationNodes)
|
|
{
|
|
if(teleportNode->locked)
|
|
continue;
|
|
/// TODO: We may consider use invisible exits on FoW border in future
|
|
/// Useful for AI when at least one tile around exit is visible and passable
|
|
/// Objects are usually visible on FoW border anyway so it's not cheating.
|
|
///
|
|
/// For now it's disabled as it's will cause crashes in movement code.
|
|
if(teleportNode->accessible == CGPathNode::BLOCKED)
|
|
continue;
|
|
|
|
destination.setNode(gamestate, teleportNode);
|
|
destination.turn = turn;
|
|
destination.movementLeft = movement;
|
|
destination.cost = cost;
|
|
|
|
if(destination.isBetterWay())
|
|
{
|
|
destination.action = getTeleportDestAction();
|
|
config->nodeStorage->commit(destination, source);
|
|
|
|
if(destination.node->action == CGPathNode::TELEPORT_NORMAL)
|
|
push(destination.node);
|
|
}
|
|
}
|
|
} //queue loop
|
|
|
|
logAi->trace("CPathfinder finished with %s iterations", std::to_string(counter));
|
|
}
|
|
|
|
std::vector<int3> CPathfinderHelper::getAllowedTeleportChannelExits(TeleportChannelID channelID) const
|
|
{
|
|
std::vector<int3> allowedExits;
|
|
|
|
for(auto objId : getTeleportChannelExits(channelID, hero->tempOwner))
|
|
{
|
|
auto obj = getObj(objId);
|
|
if(dynamic_cast<const CGWhirlpool *>(obj))
|
|
{
|
|
auto pos = obj->getBlockedPos();
|
|
for(auto p : pos)
|
|
{
|
|
if(gs->map->getTile(p).topVisitableId() == obj->ID)
|
|
allowedExits.push_back(p);
|
|
}
|
|
}
|
|
else if(obj && CGTeleport::isExitPassable(gs, hero, obj))
|
|
allowedExits.push_back(obj->visitablePos());
|
|
}
|
|
|
|
return allowedExits;
|
|
}
|
|
|
|
std::vector<int3> CPathfinderHelper::getCastleGates(const PathNodeInfo & source) const
|
|
{
|
|
std::vector<int3> allowedExits;
|
|
|
|
auto towns = getPlayerState(hero->tempOwner)->towns;
|
|
for(const auto & town : towns)
|
|
{
|
|
if(town->id != source.nodeObject->id && town->visitingHero == nullptr
|
|
&& town->hasBuilt(BuildingID::CASTLE_GATE, ETownType::INFERNO))
|
|
{
|
|
allowedExits.push_back(town->visitablePos());
|
|
}
|
|
}
|
|
|
|
return allowedExits;
|
|
}
|
|
|
|
std::vector<int3> CPathfinderHelper::getTeleportExits(const PathNodeInfo & source) const
|
|
{
|
|
std::vector<int3> teleportationExits;
|
|
|
|
const CGTeleport * objTeleport = dynamic_cast<const CGTeleport *>(source.nodeObject);
|
|
if(isAllowedTeleportEntrance(objTeleport))
|
|
{
|
|
for(auto exit : getAllowedTeleportChannelExits(objTeleport->channel))
|
|
{
|
|
teleportationExits.push_back(exit);
|
|
}
|
|
}
|
|
else if(options.useCastleGate
|
|
&& (source.nodeObject->ID == Obj::TOWN && source.nodeObject->subID == ETownType::INFERNO
|
|
&& source.objectRelations != PlayerRelations::ENEMIES))
|
|
{
|
|
/// TODO: Find way to reuse CPlayerSpecificInfoCallback::getTownsInfo
|
|
/// This may be handy if we allow to use teleportation to friendly towns
|
|
for(auto exit : getCastleGates(source))
|
|
{
|
|
teleportationExits.push_back(exit);
|
|
}
|
|
}
|
|
|
|
return teleportationExits;
|
|
}
|
|
|
|
bool CPathfinderHelper::isHeroPatrolLocked() const
|
|
{
|
|
return patrolState == PATROL_LOCKED;
|
|
}
|
|
|
|
bool CPathfinderHelper::isPatrolMovementAllowed(const int3 & dst) const
|
|
{
|
|
if(patrolState == PATROL_RADIUS)
|
|
{
|
|
if(!vstd::contains(patrolTiles, dst))
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool CPathfinder::isLayerTransitionPossible() const
|
|
{
|
|
ELayer destLayer = destination.node->layer;
|
|
|
|
/// No layer transition allowed when previous node action is BATTLE
|
|
if(source.node->action == CGPathNode::BATTLE)
|
|
return false;
|
|
|
|
switch(source.node->layer)
|
|
{
|
|
case ELayer::LAND:
|
|
if(destLayer == ELayer::AIR)
|
|
{
|
|
if(!config->options.lightweightFlyingMode || source.isInitialPosition)
|
|
return true;
|
|
}
|
|
else if(destLayer == ELayer::SAIL)
|
|
{
|
|
if(destination.tile->isWater())
|
|
return true;
|
|
}
|
|
else
|
|
return true;
|
|
|
|
break;
|
|
|
|
case ELayer::SAIL:
|
|
if(destLayer == ELayer::LAND && !destination.tile->isWater())
|
|
return true;
|
|
|
|
break;
|
|
|
|
case ELayer::AIR:
|
|
if(destLayer == ELayer::LAND)
|
|
return true;
|
|
|
|
break;
|
|
|
|
case ELayer::WATER:
|
|
if(destLayer == ELayer::LAND)
|
|
return true;
|
|
|
|
break;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
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
|
|
if(destination.node->accessible == CGPathNode::ACCESSIBLE)
|
|
destination.blocked = true;
|
|
}
|
|
|
|
break;
|
|
|
|
case EPathfindingLayer::SAIL:
|
|
//tile must be accessible -> exception: unblocked blockvis tiles -> clear but guarded by nearby monster coast
|
|
if((destination.node->accessible != CGPathNode::ACCESSIBLE && (destination.node->accessible != CGPathNode::BLOCKVIS || destination.tile->blocked))
|
|
|| 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)
|
|
{
|
|
if((source.node->accessible != CGPathNode::ACCESSIBLE &&
|
|
source.node->accessible != CGPathNode::VISITABLE) &&
|
|
(destination.node->accessible != CGPathNode::VISITABLE &&
|
|
destination.node->accessible != CGPathNode::ACCESSIBLE))
|
|
{
|
|
destination.blocked = true;
|
|
}
|
|
}
|
|
else if(destination.node->accessible != CGPathNode::ACCESSIBLE)
|
|
{
|
|
/// Hero that fly can only land on accessible tiles
|
|
if(destination.nodeObject)
|
|
destination.blocked = true;
|
|
}
|
|
|
|
break;
|
|
|
|
case EPathfindingLayer::WATER:
|
|
if(destination.node->accessible != CGPathNode::ACCESSIBLE && destination.node->accessible != CGPathNode::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;
|
|
}
|
|
}
|
|
|
|
PathfinderBlockingRule::BlockingReason MovementToDestinationRule::getBlockingReason(
|
|
const PathNodeInfo & source,
|
|
const CDestinationNodeInfo & destination,
|
|
const PathfinderConfig * pathfinderConfig,
|
|
const CPathfinderHelper * pathfinderHelper) const
|
|
{
|
|
|
|
if(destination.node->accessible == CGPathNode::BLOCKED)
|
|
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
|
|
if(source.node->action != CGPathNode::EMBARK && !destination.isGuardianTile)
|
|
return BlockingReason::SOURCE_GUARDED;
|
|
}
|
|
|
|
if(source.node->layer == EPathfindingLayer::LAND)
|
|
{
|
|
if(!destination.isNodeObjectVisitable())
|
|
return BlockingReason::DESTINATION_BLOCKED;
|
|
|
|
if(destination.nodeObject->ID != Obj::BOAT && !destination.nodeHero)
|
|
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)
|
|
|| destination.node->accessible != CGPathNode::ACCESSIBLE)
|
|
{
|
|
return BlockingReason::DESTINATION_BLOCKED;
|
|
}
|
|
|
|
if(destination.guarded)
|
|
return BlockingReason::DESTINATION_BLOCKED;
|
|
|
|
break;
|
|
}
|
|
|
|
return BlockingReason::NONE;
|
|
}
|
|
|
|
|
|
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 == CGPathNode::ENodeAction::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 don't fly
|
|
case CGPathNode::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 CGTeleport * 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 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 CGPathNode::BLOCKING_VISIT:
|
|
return destination.guarded
|
|
? BlockingReason::DESTINATION_GUARDED
|
|
: BlockingReason::DESTINATION_BLOCKVIS;
|
|
|
|
case CGPathNode::NORMAL:
|
|
return BlockingReason::NONE;
|
|
|
|
case CGPathNode::EMBARK:
|
|
if(pathfinderHelper->options.useEmbarkAndDisembark)
|
|
return BlockingReason::NONE;
|
|
|
|
return BlockingReason::DESTINATION_BLOCKED;
|
|
|
|
case CGPathNode::DISEMBARK:
|
|
if(pathfinderHelper->options.useEmbarkAndDisembark)
|
|
return destination.guarded ? BlockingReason::DESTINATION_GUARDED : BlockingReason::NONE;
|
|
|
|
return BlockingReason::DESTINATION_BLOCKED;
|
|
|
|
case CGPathNode::BATTLE:
|
|
/// Movement after BATTLE action only possible from guarded tile to guardian tile
|
|
if(destination.guarded)
|
|
return BlockingReason::DESTINATION_GUARDED;
|
|
|
|
break;
|
|
}
|
|
|
|
return BlockingReason::DESTINATION_BLOCKED;
|
|
}
|
|
|
|
void DestinationActionRule::process(
|
|
const PathNodeInfo & source,
|
|
CDestinationNodeInfo & destination,
|
|
const PathfinderConfig * pathfinderConfig,
|
|
CPathfinderHelper * pathfinderHelper) const
|
|
{
|
|
if(destination.action != CGPathNode::ENodeAction::UNKNOWN)
|
|
{
|
|
#ifdef VCMI_TRACE_PATHFINDER
|
|
logAi->trace("Accepted precalculated action at %s", destination.coord.toString());
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
CGPathNode::ENodeAction action = CGPathNode::NORMAL;
|
|
auto hero = pathfinderHelper->hero;
|
|
|
|
switch(destination.node->layer)
|
|
{
|
|
case EPathfindingLayer::LAND:
|
|
if(source.node->layer == EPathfindingLayer::SAIL)
|
|
{
|
|
// TODO: Handle dismebark into guarded areaa
|
|
action = CGPathNode::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 = CGPathNode::EMBARK;
|
|
else if(destination.nodeHero)
|
|
{
|
|
if(destination.heroRelations == PlayerRelations::ENEMIES)
|
|
action = CGPathNode::BATTLE;
|
|
else
|
|
action = CGPathNode::BLOCKING_VISIT;
|
|
}
|
|
else if(destination.nodeObject->ID == Obj::TOWN)
|
|
{
|
|
if(destination.nodeObject->passableFor(hero->tempOwner))
|
|
action = CGPathNode::VISIT;
|
|
else if(objRel == PlayerRelations::ENEMIES)
|
|
action = CGPathNode::BATTLE;
|
|
}
|
|
else if(destination.nodeObject->ID == Obj::GARRISON || destination.nodeObject->ID == Obj::GARRISON2)
|
|
{
|
|
if(destination.nodeObject->passableFor(hero->tempOwner))
|
|
{
|
|
if(destination.guarded)
|
|
action = CGPathNode::BATTLE;
|
|
}
|
|
else if(objRel == PlayerRelations::ENEMIES)
|
|
action = CGPathNode::BATTLE;
|
|
}
|
|
else if(destination.nodeObject->ID == Obj::BORDER_GATE)
|
|
{
|
|
if(destination.nodeObject->passableFor(hero->tempOwner))
|
|
{
|
|
if(destination.guarded)
|
|
action = CGPathNode::BATTLE;
|
|
}
|
|
else
|
|
action = CGPathNode::BLOCKING_VISIT;
|
|
}
|
|
else if(destination.isGuardianTile)
|
|
action = CGPathNode::BATTLE;
|
|
else if(destination.nodeObject->blockVisit && !(pathfinderConfig->options.useCastleGate && destination.nodeObject->ID == Obj::TOWN))
|
|
action = CGPathNode::BLOCKING_VISIT;
|
|
|
|
if(action == CGPathNode::NORMAL)
|
|
{
|
|
if(destination.guarded)
|
|
action = CGPathNode::BATTLE;
|
|
else
|
|
action = CGPathNode::VISIT;
|
|
}
|
|
}
|
|
else if(destination.guarded)
|
|
action = CGPathNode::BATTLE;
|
|
|
|
break;
|
|
}
|
|
|
|
destination.action = action;
|
|
}
|
|
|
|
CGPathNode::ENodeAction CPathfinder::getTeleportDestAction() const
|
|
{
|
|
CGPathNode::ENodeAction action = CGPathNode::TELEPORT_NORMAL;
|
|
|
|
if(destination.isNodeObjectVisitable() && destination.nodeHero)
|
|
{
|
|
if(destination.heroRelations == PlayerRelations::ENEMIES)
|
|
action = CGPathNode::TELEPORT_BATTLE;
|
|
else
|
|
action = CGPathNode::TELEPORT_BLOCKING_VISIT;
|
|
}
|
|
|
|
return action;
|
|
}
|
|
|
|
bool CPathfinder::isDestinationGuardian() const
|
|
{
|
|
return gamestate->guardingCreaturePosition(destination.node->coord) == destination.node->coord;
|
|
}
|
|
|
|
void CPathfinderHelper::initializePatrol()
|
|
{
|
|
auto state = PATROL_NONE;
|
|
|
|
if(hero->patrol.patrolling && !getPlayerState(hero->tempOwner)->human)
|
|
{
|
|
if(hero->patrol.patrolRadius)
|
|
{
|
|
state = PATROL_RADIUS;
|
|
gs->getTilesInRange(patrolTiles, hero->patrol.initialPos, hero->patrol.patrolRadius, boost::optional<PlayerColor>(), 0, int3::DIST_MANHATTAN);
|
|
}
|
|
else
|
|
state = PATROL_LOCKED;
|
|
}
|
|
|
|
patrolState = state;
|
|
}
|
|
|
|
void CPathfinder::initializeGraph()
|
|
{
|
|
INodeStorage * nodeStorage = config->nodeStorage.get();
|
|
nodeStorage->initialize(config->options, gamestate);
|
|
}
|
|
|
|
bool CPathfinderHelper::canMoveBetween(const int3 & a, const int3 & b) const
|
|
{
|
|
return gs->checkForVisitableDir(a, b);
|
|
}
|
|
|
|
bool CPathfinderHelper::isAllowedTeleportEntrance(const CGTeleport * obj) const
|
|
{
|
|
if(!obj || !isTeleportEntrancePassable(obj, hero->tempOwner))
|
|
return false;
|
|
|
|
auto whirlpool = dynamic_cast<const CGWhirlpool *>(obj);
|
|
if(whirlpool)
|
|
{
|
|
if(addTeleportWhirlpool(whirlpool))
|
|
return true;
|
|
}
|
|
else if(addTeleportTwoWay(obj) || addTeleportOneWay(obj) || addTeleportOneWayRandom(obj))
|
|
return true;
|
|
|
|
return false;
|
|
}
|
|
|
|
bool CPathfinderHelper::addTeleportTwoWay(const CGTeleport * obj) const
|
|
{
|
|
return options.useTeleportTwoWay && isTeleportChannelBidirectional(obj->channel, hero->tempOwner);
|
|
}
|
|
|
|
bool CPathfinderHelper::addTeleportOneWay(const CGTeleport * obj) const
|
|
{
|
|
if(options.useTeleportOneWay && isTeleportChannelUnidirectional(obj->channel, hero->tempOwner))
|
|
{
|
|
auto passableExits = CGTeleport::getPassableExits(gs, hero, getTeleportChannelExits(obj->channel, hero->tempOwner));
|
|
if(passableExits.size() == 1)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool CPathfinderHelper::addTeleportOneWayRandom(const CGTeleport * obj) const
|
|
{
|
|
if(options.useTeleportOneWayRandom && isTeleportChannelUnidirectional(obj->channel, hero->tempOwner))
|
|
{
|
|
auto passableExits = CGTeleport::getPassableExits(gs, hero, getTeleportChannelExits(obj->channel, hero->tempOwner));
|
|
if(passableExits.size() > 1)
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool CPathfinderHelper::addTeleportWhirlpool(const CGWhirlpool * obj) const
|
|
{
|
|
return options.useTeleportWhirlpool && hasBonusOfType(Bonus::WHIRLPOOL_PROTECTION) && obj;
|
|
}
|
|
|
|
int CPathfinderHelper::movementPointsAfterEmbark(int movement, int basicCost, bool disembark) const
|
|
{
|
|
return hero->movementPointsAfterEmbark(movement, basicCost, disembark, getTurnInfo());
|
|
}
|
|
|
|
bool CPathfinderHelper::passOneTurnLimitCheck(const PathNodeInfo & source) const
|
|
{
|
|
|
|
if(!options.oneTurnSpecialLayersLimit)
|
|
return true;
|
|
|
|
if(source.node->layer == EPathfindingLayer::WATER)
|
|
return false;
|
|
if(source.node->layer == EPathfindingLayer::AIR)
|
|
{
|
|
if(options.originalMovementRules && source.node->accessible == CGPathNode::ACCESSIBLE)
|
|
return true;
|
|
else
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
TurnInfo::BonusCache::BonusCache(TConstBonusListPtr bl)
|
|
{
|
|
for(const auto & terrain : VLC->terrainTypeHandler->objects)
|
|
{
|
|
noTerrainPenalty.push_back(static_cast<bool>(
|
|
bl->getFirst(Selector::type()(Bonus::NO_TERRAIN_PENALTY).And(Selector::subtype()(terrain->getIndex())))));
|
|
}
|
|
|
|
freeShipBoarding = static_cast<bool>(bl->getFirst(Selector::type()(Bonus::FREE_SHIP_BOARDING)));
|
|
flyingMovement = static_cast<bool>(bl->getFirst(Selector::type()(Bonus::FLYING_MOVEMENT)));
|
|
flyingMovementVal = bl->valOfBonuses(Selector::type()(Bonus::FLYING_MOVEMENT));
|
|
waterWalking = static_cast<bool>(bl->getFirst(Selector::type()(Bonus::WATER_WALKING)));
|
|
waterWalkingVal = bl->valOfBonuses(Selector::type()(Bonus::WATER_WALKING));
|
|
pathfindingVal = bl->valOfBonuses(Selector::type()(Bonus::ROUGH_TERRAIN_DISCOUNT));
|
|
}
|
|
|
|
TurnInfo::TurnInfo(const CGHeroInstance * Hero, const int turn)
|
|
: hero(Hero), maxMovePointsLand(-1), maxMovePointsWater(-1)
|
|
{
|
|
bonuses = hero->getAllBonuses(Selector::days(turn), Selector::all, nullptr, "");
|
|
bonusCache = std::make_unique<BonusCache>(bonuses);
|
|
nativeTerrain = hero->getNativeTerrain();
|
|
}
|
|
|
|
bool TurnInfo::isLayerAvailable(const EPathfindingLayer layer) const
|
|
{
|
|
switch(layer)
|
|
{
|
|
case EPathfindingLayer::AIR:
|
|
if(!hasBonusOfType(Bonus::FLYING_MOVEMENT))
|
|
return false;
|
|
|
|
break;
|
|
|
|
case EPathfindingLayer::WATER:
|
|
if(!hasBonusOfType(Bonus::WATER_WALKING))
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool TurnInfo::hasBonusOfType(Bonus::BonusType type, int subtype) const
|
|
{
|
|
switch(type)
|
|
{
|
|
case Bonus::FREE_SHIP_BOARDING:
|
|
return bonusCache->freeShipBoarding;
|
|
case Bonus::FLYING_MOVEMENT:
|
|
return bonusCache->flyingMovement;
|
|
case Bonus::WATER_WALKING:
|
|
return bonusCache->waterWalking;
|
|
case Bonus::NO_TERRAIN_PENALTY:
|
|
return bonusCache->noTerrainPenalty[subtype];
|
|
}
|
|
|
|
return static_cast<bool>(
|
|
bonuses->getFirst(Selector::type()(type).And(Selector::subtype()(subtype))));
|
|
}
|
|
|
|
int TurnInfo::valOfBonuses(Bonus::BonusType type, int subtype) const
|
|
{
|
|
switch(type)
|
|
{
|
|
case Bonus::FLYING_MOVEMENT:
|
|
return bonusCache->flyingMovementVal;
|
|
case Bonus::WATER_WALKING:
|
|
return bonusCache->waterWalkingVal;
|
|
case Bonus::ROUGH_TERRAIN_DISCOUNT:
|
|
return bonusCache->pathfindingVal;
|
|
}
|
|
|
|
return bonuses->valOfBonuses(Selector::type()(type).And(Selector::subtype()(subtype)));
|
|
}
|
|
|
|
int TurnInfo::getMaxMovePoints(const EPathfindingLayer layer) const
|
|
{
|
|
if(maxMovePointsLand == -1)
|
|
maxMovePointsLand = hero->maxMovePointsCached(true, this);
|
|
if(maxMovePointsWater == -1)
|
|
maxMovePointsWater = hero->maxMovePointsCached(false, this);
|
|
|
|
return layer == EPathfindingLayer::SAIL ? maxMovePointsWater : maxMovePointsLand;
|
|
}
|
|
|
|
void TurnInfo::updateHeroBonuses(Bonus::BonusType type, const CSelector& sel) const
|
|
{
|
|
switch(type)
|
|
{
|
|
case Bonus::FREE_SHIP_BOARDING:
|
|
bonusCache->freeShipBoarding = static_cast<bool>(bonuses->getFirst(Selector::type()(Bonus::FREE_SHIP_BOARDING)));
|
|
break;
|
|
case Bonus::FLYING_MOVEMENT:
|
|
bonusCache->flyingMovement = static_cast<bool>(bonuses->getFirst(Selector::type()(Bonus::FLYING_MOVEMENT)));
|
|
bonusCache->flyingMovementVal = bonuses->valOfBonuses(Selector::type()(Bonus::FLYING_MOVEMENT));
|
|
break;
|
|
case Bonus::WATER_WALKING:
|
|
bonusCache->waterWalking = static_cast<bool>(bonuses->getFirst(Selector::type()(Bonus::WATER_WALKING)));
|
|
bonusCache->waterWalkingVal = bonuses->valOfBonuses(Selector::type()(Bonus::WATER_WALKING));
|
|
break;
|
|
case Bonus::ROUGH_TERRAIN_DISCOUNT:
|
|
bonusCache->pathfindingVal = bonuses->valOfBonuses(Selector::type()(Bonus::ROUGH_TERRAIN_DISCOUNT));
|
|
break;
|
|
default:
|
|
bonuses = hero->getUpdatedBonusList(*bonuses, Selector::type()(type).And(sel));
|
|
}
|
|
}
|
|
|
|
CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Hero, const PathfinderOptions & Options)
|
|
: CGameInfoCallback(gs, boost::optional<PlayerColor>()), turn(-1), hero(Hero), options(Options), owner(Hero->tempOwner)
|
|
{
|
|
turnsInfo.reserve(16);
|
|
updateTurnInfo();
|
|
initializePatrol();
|
|
}
|
|
|
|
CPathfinderHelper::~CPathfinderHelper()
|
|
{
|
|
for(auto ti : turnsInfo)
|
|
delete ti;
|
|
}
|
|
|
|
void CPathfinderHelper::updateTurnInfo(const int Turn)
|
|
{
|
|
if(turn != Turn)
|
|
{
|
|
turn = Turn;
|
|
if(turn >= turnsInfo.size())
|
|
{
|
|
auto ti = new TurnInfo(hero, turn);
|
|
turnsInfo.push_back(ti);
|
|
}
|
|
}
|
|
}
|
|
|
|
bool CPathfinderHelper::isLayerAvailable(const EPathfindingLayer layer) const
|
|
{
|
|
switch(layer)
|
|
{
|
|
case EPathfindingLayer::AIR:
|
|
if(!options.useFlying)
|
|
return false;
|
|
|
|
break;
|
|
|
|
case EPathfindingLayer::WATER:
|
|
if(!options.useWaterWalking)
|
|
return false;
|
|
|
|
break;
|
|
}
|
|
|
|
return turnsInfo[turn]->isLayerAvailable(layer);
|
|
}
|
|
|
|
const TurnInfo * CPathfinderHelper::getTurnInfo() const
|
|
{
|
|
return turnsInfo[turn];
|
|
}
|
|
|
|
bool CPathfinderHelper::hasBonusOfType(const Bonus::BonusType type, const int subtype) const
|
|
{
|
|
return turnsInfo[turn]->hasBonusOfType(type, subtype);
|
|
}
|
|
|
|
int CPathfinderHelper::getMaxMovePoints(const EPathfindingLayer layer) const
|
|
{
|
|
return turnsInfo[turn]->getMaxMovePoints(layer);
|
|
}
|
|
|
|
void CPathfinderHelper::getNeighbours(
|
|
const TerrainTile & srct,
|
|
const int3 & tile,
|
|
std::vector<int3> & vec,
|
|
const boost::logic::tribool & onLand,
|
|
const bool limitCoastSailing) const
|
|
{
|
|
CMap * map = gs->map;
|
|
|
|
static const int3 dirs[] = {
|
|
int3(-1, +1, +0), int3(0, +1, +0), int3(+1, +1, +0),
|
|
int3(-1, +0, +0), /* source pos */ int3(+1, +0, +0),
|
|
int3(-1, -1, +0), int3(0, -1, +0), int3(+1, -1, +0)
|
|
};
|
|
|
|
for(auto & dir : dirs)
|
|
{
|
|
const int3 hlp = tile + dir;
|
|
if(!map->isInTheMap(hlp))
|
|
continue;
|
|
|
|
const TerrainTile & hlpt = map->getTile(hlp);
|
|
if(!hlpt.terType->isPassable())
|
|
continue;
|
|
|
|
// //we cannot visit things from blocked tiles
|
|
// if(srct.blocked && !srct.visitable && hlpt.visitable && srct.blockingObjects.front()->ID != HEROI_TYPE)
|
|
// {
|
|
// continue;
|
|
// }
|
|
|
|
/// Following condition let us avoid diagonal movement over coast when sailing
|
|
if(srct.terType->isWater() && limitCoastSailing && hlpt.terType->isWater() && dir.x && dir.y) //diagonal move through water
|
|
{
|
|
int3 hlp1 = tile,
|
|
hlp2 = tile;
|
|
hlp1.x += dir.x;
|
|
hlp2.y += dir.y;
|
|
|
|
if(map->getTile(hlp1).terType->isLand() || map->getTile(hlp2).terType->isLand())
|
|
continue;
|
|
}
|
|
|
|
if(indeterminate(onLand) || onLand == hlpt.terType->isLand())
|
|
{
|
|
vec.push_back(hlp);
|
|
}
|
|
}
|
|
}
|
|
|
|
int CPathfinderHelper::getMovementCost(
|
|
const int3 & src,
|
|
const int3 & dst,
|
|
const TerrainTile * ct,
|
|
const TerrainTile * dt,
|
|
const int remainingMovePoints,
|
|
const bool checkLast) const
|
|
{
|
|
if(src == dst) //same tile
|
|
return 0;
|
|
|
|
auto ti = getTurnInfo();
|
|
|
|
if(ct == nullptr || dt == nullptr)
|
|
{
|
|
ct = hero->cb->getTile(src);
|
|
dt = hero->cb->getTile(dst);
|
|
}
|
|
|
|
/// TODO: by the original game rules hero shouldn't be affected by terrain penalty while flying.
|
|
/// Also flying movement only has penalty when player moving over blocked tiles.
|
|
/// So if you only have base flying with 40% penalty you can still ignore terrain penalty while having zero flying penalty.
|
|
int ret = hero->getTileCost(*dt, *ct, ti);
|
|
/// Unfortunately this can't be implemented yet as server don't know when player flying and when he's not.
|
|
/// Difference in cost calculation on client and server is much worse than incorrect cost.
|
|
/// So this one is waiting till server going to use pathfinder rules for path validation.
|
|
|
|
if(dt->blocked && ti->hasBonusOfType(Bonus::FLYING_MOVEMENT))
|
|
{
|
|
ret = static_cast<int>(ret * (100.0 + ti->valOfBonuses(Bonus::FLYING_MOVEMENT)) / 100.0);
|
|
}
|
|
else if(dt->terType->isWater())
|
|
{
|
|
if(hero->boat && ct->hasFavorableWinds() && dt->hasFavorableWinds())
|
|
ret = static_cast<int>(ret * 0.666);
|
|
else if(!hero->boat && ti->hasBonusOfType(Bonus::WATER_WALKING))
|
|
{
|
|
ret = static_cast<int>(ret * (100.0 + ti->valOfBonuses(Bonus::WATER_WALKING)) / 100.0);
|
|
}
|
|
}
|
|
|
|
if(src.x != dst.x && src.y != dst.y) //it's diagonal move
|
|
{
|
|
int old = ret;
|
|
ret = static_cast<int>(ret * M_SQRT2);
|
|
//diagonal move costs too much but normal move is possible - allow diagonal move for remaining move points
|
|
if(ret > remainingMovePoints && remainingMovePoints >= old)
|
|
{
|
|
return remainingMovePoints;
|
|
}
|
|
}
|
|
|
|
/// TODO: This part need rework in order to work properly with flying and water walking
|
|
/// Currently it's only work properly for normal movement or sailing
|
|
int left = remainingMovePoints-ret;
|
|
if(checkLast && left > 0 && remainingMovePoints-ret < 250) //it might be the last tile - if no further move possible we take all move points
|
|
{
|
|
std::vector<int3> vec;
|
|
vec.reserve(8); //optimization
|
|
getNeighbours(*dt, dst, vec, ct->terType->isLand(), true);
|
|
for(auto & elem : vec)
|
|
{
|
|
int fcost = getMovementCost(dst, elem, nullptr, nullptr, left, false);
|
|
if(fcost <= left)
|
|
{
|
|
return ret;
|
|
}
|
|
}
|
|
ret = remainingMovePoints;
|
|
}
|
|
|
|
return ret;
|
|
}
|
|
|
|
int3 CGPath::startPos() const
|
|
{
|
|
return nodes[nodes.size()-1].coord;
|
|
}
|
|
|
|
int3 CGPath::endPos() const
|
|
{
|
|
return nodes[0].coord;
|
|
}
|
|
|
|
CPathsInfo::CPathsInfo(const int3 & Sizes, const CGHeroInstance * hero_)
|
|
: sizes(Sizes), hero(hero_)
|
|
{
|
|
nodes.resize(boost::extents[ELayer::NUM_LAYERS][sizes.z][sizes.x][sizes.y]);
|
|
}
|
|
|
|
CPathsInfo::~CPathsInfo() = default;
|
|
|
|
const CGPathNode * CPathsInfo::getPathInfo(const int3 & tile) const
|
|
{
|
|
assert(vstd::iswithin(tile.x, 0, sizes.x));
|
|
assert(vstd::iswithin(tile.y, 0, sizes.y));
|
|
assert(vstd::iswithin(tile.z, 0, sizes.z));
|
|
|
|
return getNode(tile);
|
|
}
|
|
|
|
bool CPathsInfo::getPath(CGPath & out, const int3 & dst) const
|
|
{
|
|
out.nodes.clear();
|
|
const CGPathNode * curnode = getNode(dst);
|
|
if(!curnode->theNodeBefore)
|
|
return false;
|
|
|
|
while(curnode)
|
|
{
|
|
const CGPathNode cpn = * curnode;
|
|
curnode = curnode->theNodeBefore;
|
|
out.nodes.push_back(cpn);
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const CGPathNode * CPathsInfo::getNode(const int3 & coord) const
|
|
{
|
|
auto landNode = &nodes[ELayer::LAND][coord.z][coord.x][coord.y];
|
|
if(landNode->reachable())
|
|
return landNode;
|
|
else
|
|
return &nodes[ELayer::SAIL][coord.z][coord.x][coord.y];
|
|
}
|
|
|
|
PathNodeInfo::PathNodeInfo()
|
|
: node(nullptr), nodeObject(nullptr), tile(nullptr), coord(-1, -1, -1), guarded(false), isInitialPosition(false)
|
|
{
|
|
}
|
|
|
|
void PathNodeInfo::setNode(CGameState * gs, CGPathNode * n)
|
|
{
|
|
node = n;
|
|
|
|
if(coord != node->coord)
|
|
{
|
|
assert(node->coord.valid());
|
|
|
|
coord = node->coord;
|
|
tile = gs->getTile(coord);
|
|
nodeObject = tile->topVisitableObj();
|
|
|
|
if(nodeObject && nodeObject->ID == Obj::HERO)
|
|
{
|
|
nodeHero = dynamic_cast<const CGHeroInstance *>(nodeObject);
|
|
nodeObject = tile->topVisitableObj(true);
|
|
|
|
if(!nodeObject)
|
|
nodeObject = nodeHero;
|
|
}
|
|
else
|
|
{
|
|
nodeHero = nullptr;
|
|
}
|
|
}
|
|
|
|
guarded = false;
|
|
}
|
|
|
|
void PathNodeInfo::updateInfo(CPathfinderHelper * hlp, CGameState * gs)
|
|
{
|
|
if(gs->guardingCreaturePosition(node->coord).valid() && !isInitialPosition)
|
|
{
|
|
guarded = true;
|
|
}
|
|
|
|
if(nodeObject)
|
|
{
|
|
objectRelations = gs->getPlayerRelations(hlp->owner, nodeObject->tempOwner);
|
|
}
|
|
|
|
if(nodeHero)
|
|
{
|
|
heroRelations = gs->getPlayerRelations(hlp->owner, nodeHero->tempOwner);
|
|
}
|
|
}
|
|
|
|
CDestinationNodeInfo::CDestinationNodeInfo()
|
|
: PathNodeInfo(),
|
|
blocked(false),
|
|
action(CGPathNode::ENodeAction::UNKNOWN)
|
|
{
|
|
}
|
|
|
|
void CDestinationNodeInfo::setNode(CGameState * gs, CGPathNode * n)
|
|
{
|
|
PathNodeInfo::setNode(gs, n);
|
|
|
|
blocked = false;
|
|
action = CGPathNode::ENodeAction::UNKNOWN;
|
|
}
|
|
|
|
bool CDestinationNodeInfo::isBetterWay() const
|
|
{
|
|
if(node->turns == 0xff) //we haven't been here before
|
|
return true;
|
|
else
|
|
return cost < node->getCost(); //this route is faster
|
|
}
|
|
|
|
bool PathNodeInfo::isNodeObjectVisitable() const
|
|
{
|
|
/// Hero can't visit objects while walking on water or flying
|
|
return (node->layer == EPathfindingLayer::LAND || node->layer == EPathfindingLayer::SAIL)
|
|
&& (canSeeObj(nodeObject) || canSeeObj(nodeHero));
|
|
}
|
|
|
|
VCMI_LIB_NAMESPACE_END
|