mirror of
https://github.com/vcmi/vcmi.git
synced 2025-09-16 09:26:28 +02:00
Merge pull request #1534 from kambala-decapitator/pathfinder-fly
fix movement cost with Fly
This commit is contained in:
@@ -285,7 +285,7 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
|
|||||||
|
|
||||||
for(auto & neighbour : accessibleNeighbourTiles)
|
for(auto & neighbour : accessibleNeighbourTiles)
|
||||||
{
|
{
|
||||||
for(EPathfindingLayer i = EPathfindingLayer::LAND; i <= EPathfindingLayer::AIR; i.advance(1))
|
for(EPathfindingLayer i = EPathfindingLayer::LAND; i < EPathfindingLayer::NUM_LAYERS; i.advance(1))
|
||||||
{
|
{
|
||||||
auto nextNode = getOrCreateNode(neighbour, i, srcNode->actor);
|
auto nextNode = getOrCreateNode(neighbour, i, srcNode->actor);
|
||||||
|
|
||||||
|
@@ -167,7 +167,7 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
|
|||||||
|
|
||||||
for(auto & neighbour : accessibleNeighbourTiles)
|
for(auto & neighbour : accessibleNeighbourTiles)
|
||||||
{
|
{
|
||||||
for(EPathfindingLayer i = EPathfindingLayer::LAND; i <= EPathfindingLayer::AIR; i.advance(1))
|
for(EPathfindingLayer i = EPathfindingLayer::LAND; i < EPathfindingLayer::NUM_LAYERS; i.advance(1))
|
||||||
{
|
{
|
||||||
auto nextNode = getOrCreateNode(neighbour, i, srcNode->chainMask);
|
auto nextNode = getOrCreateNode(neighbour, i, srcNode->chainMask);
|
||||||
|
|
||||||
|
@@ -331,7 +331,7 @@ TerrainId CCreature::getNativeTerrain() const
|
|||||||
static const auto selectorNoTerrainPenalty = Selector::type()(Bonus::NO_TERRAIN_PENALTY);
|
static const auto selectorNoTerrainPenalty = Selector::type()(Bonus::NO_TERRAIN_PENALTY);
|
||||||
|
|
||||||
//this code is used in the CreatureTerrainLimiter::limit to setup battle bonuses
|
//this code is used in the CreatureTerrainLimiter::limit to setup battle bonuses
|
||||||
//and in the CGHeroInstance::getNativeTerrain() to setup mevement bonuses or/and penalties.
|
//and in the CGHeroInstance::getNativeTerrain() to setup movement bonuses or/and penalties.
|
||||||
return hasBonus(selectorNoTerrainPenalty, selectorNoTerrainPenalty)
|
return hasBonus(selectorNoTerrainPenalty, selectorNoTerrainPenalty)
|
||||||
? TerrainId(ETerrainId::ANY_TERRAIN)
|
? TerrainId(ETerrainId::ANY_TERRAIN)
|
||||||
: (*VLC->townh)[faction]->nativeTerrain;
|
: (*VLC->townh)[faction]->nativeTerrain;
|
||||||
|
@@ -161,9 +161,6 @@ public:
|
|||||||
bool isItNativeTerrain(TerrainId terrain) const;
|
bool isItNativeTerrain(TerrainId terrain) const;
|
||||||
/**
|
/**
|
||||||
Returns creature native terrain considering some terrain bonuses.
|
Returns creature native terrain considering some terrain bonuses.
|
||||||
@param considerBonus is used to avoid Dead Lock when this method is called inside getAllBonuses
|
|
||||||
considerBonus = true is called from Pathfinder and fills actual nativeTerrain considering bonus(es).
|
|
||||||
considerBonus = false is called on Battle init and returns already prepared nativeTerrain without Bonus system calling.
|
|
||||||
*/
|
*/
|
||||||
TerrainId getNativeTerrain() const;
|
TerrainId getNativeTerrain() const;
|
||||||
int32_t getIndex() const override;
|
int32_t getIndex() const override;
|
||||||
|
@@ -78,7 +78,7 @@ std::vector<CGPathNode *> NodeStorage::calculateNeighbours(
|
|||||||
|
|
||||||
for(auto & neighbour : accessibleNeighbourTiles)
|
for(auto & neighbour : accessibleNeighbourTiles)
|
||||||
{
|
{
|
||||||
for(EPathfindingLayer i = EPathfindingLayer::LAND; i <= EPathfindingLayer::AIR; i.advance(1))
|
for(EPathfindingLayer i = EPathfindingLayer::LAND; i < EPathfindingLayer::NUM_LAYERS; i.advance(1))
|
||||||
{
|
{
|
||||||
auto * node = getNode(neighbour, i);
|
auto * node = getNode(neighbour, i);
|
||||||
|
|
||||||
@@ -117,7 +117,7 @@ std::vector<CGPathNode *> NodeStorage::calculateTeleportations(
|
|||||||
std::vector<int3> CPathfinderHelper::getNeighbourTiles(const PathNodeInfo & source) const
|
std::vector<int3> CPathfinderHelper::getNeighbourTiles(const PathNodeInfo & source) const
|
||||||
{
|
{
|
||||||
std::vector<int3> neighbourTiles;
|
std::vector<int3> neighbourTiles;
|
||||||
neighbourTiles.reserve(16);
|
neighbourTiles.reserve(8);
|
||||||
|
|
||||||
getNeighbours(
|
getNeighbours(
|
||||||
*source.tile,
|
*source.tile,
|
||||||
@@ -721,7 +721,7 @@ PathfinderBlockingRule::BlockingReason MovementAfterDestinationRule::getBlocking
|
|||||||
switch(destination.action)
|
switch(destination.action)
|
||||||
{
|
{
|
||||||
/// TODO: Investigate what kind of limitation is possible to apply on movement from visitable tiles
|
/// 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
|
/// Likely in many cases we don't need to add visitable tile to queue when hero doesn't fly
|
||||||
case CGPathNode::VISIT:
|
case CGPathNode::VISIT:
|
||||||
{
|
{
|
||||||
/// For now we only add visitable tile into queue when it's teleporter that allow transit
|
/// For now we only add visitable tile into queue when it's teleporter that allow transit
|
||||||
@@ -730,7 +730,7 @@ PathfinderBlockingRule::BlockingReason MovementAfterDestinationRule::getBlocking
|
|||||||
if(pathfinderHelper->isAllowedTeleportEntrance(objTeleport))
|
if(pathfinderHelper->isAllowedTeleportEntrance(objTeleport))
|
||||||
{
|
{
|
||||||
/// For now we'll always allow transit over teleporters
|
/// For now we'll always allow transit over teleporters
|
||||||
/// Transit over whirlpools only allowed when hero protected
|
/// Transit over whirlpools only allowed when hero is protected
|
||||||
return BlockingReason::NONE;
|
return BlockingReason::NONE;
|
||||||
}
|
}
|
||||||
else if(destination.nodeObject->ID == Obj::GARRISON
|
else if(destination.nodeObject->ID == Obj::GARRISON
|
||||||
@@ -1163,8 +1163,8 @@ int CPathfinderHelper::getMaxMovePoints(const EPathfindingLayer & layer) const
|
|||||||
}
|
}
|
||||||
|
|
||||||
void CPathfinderHelper::getNeighbours(
|
void CPathfinderHelper::getNeighbours(
|
||||||
const TerrainTile & srct,
|
const TerrainTile & srcTile,
|
||||||
const int3 & tile,
|
const int3 & srcCoord,
|
||||||
std::vector<int3> & vec,
|
std::vector<int3> & vec,
|
||||||
const boost::logic::tribool & onLand,
|
const boost::logic::tribool & onLand,
|
||||||
const bool limitCoastSailing) const
|
const bool limitCoastSailing) const
|
||||||
@@ -1179,35 +1179,32 @@ void CPathfinderHelper::getNeighbours(
|
|||||||
|
|
||||||
for(const auto & dir : dirs)
|
for(const auto & dir : dirs)
|
||||||
{
|
{
|
||||||
const int3 hlp = tile + dir;
|
const int3 destCoord = srcCoord + dir;
|
||||||
if(!map->isInTheMap(hlp))
|
if(!map->isInTheMap(destCoord))
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
const TerrainTile & hlpt = map->getTile(hlp);
|
const TerrainTile & destTile = map->getTile(destCoord);
|
||||||
if(!hlpt.terType->isPassable())
|
if(!destTile.terType->isPassable())
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
// //we cannot visit things from blocked tiles
|
// //we cannot visit things from blocked tiles
|
||||||
// if(srct.blocked && !srct.visitable && hlpt.visitable && srct.blockingObjects.front()->ID != HEROI_TYPE)
|
// if(srcTile.blocked && !srcTile.visitable && destTile.visitable && srcTile.blockingObjects.front()->ID != HEROI_TYPE)
|
||||||
// {
|
// {
|
||||||
// continue;
|
// continue;
|
||||||
// }
|
// }
|
||||||
|
|
||||||
/// Following condition let us avoid diagonal movement over coast when sailing
|
/// 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
|
if(srcTile.terType->isWater() && limitCoastSailing && destTile.terType->isWater() && dir.x && dir.y) //diagonal move through water
|
||||||
{
|
{
|
||||||
int3 hlp1 = tile;
|
const int3 horizontalNeighbour = srcCoord + int3{dir.x, 0, 0};
|
||||||
int3 hlp2 = tile;
|
const int3 verticalNeighbour = srcCoord + int3{0, dir.y, 0};
|
||||||
hlp1.x += dir.x;
|
if(map->getTile(horizontalNeighbour).terType->isLand() || map->getTile(verticalNeighbour).terType->isLand())
|
||||||
hlp2.y += dir.y;
|
|
||||||
|
|
||||||
if(map->getTile(hlp1).terType->isLand() || map->getTile(hlp2).terType->isLand())
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(indeterminate(onLand) || onLand == hlpt.terType->isLand())
|
if(indeterminate(onLand) || onLand == destTile.terType->isLand())
|
||||||
{
|
{
|
||||||
vec.push_back(hlp);
|
vec.push_back(destCoord);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1218,7 +1215,9 @@ int CPathfinderHelper::getMovementCost(
|
|||||||
const TerrainTile * ct,
|
const TerrainTile * ct,
|
||||||
const TerrainTile * dt,
|
const TerrainTile * dt,
|
||||||
const int remainingMovePoints,
|
const int remainingMovePoints,
|
||||||
const bool checkLast) const
|
const bool checkLast,
|
||||||
|
boost::logic::tribool isDstSailLayer,
|
||||||
|
boost::logic::tribool isDstWaterLayer) const
|
||||||
{
|
{
|
||||||
if(src == dst) //same tile
|
if(src == dst) //same tile
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1231,48 +1230,49 @@ int CPathfinderHelper::getMovementCost(
|
|||||||
dt = hero->cb->getTile(dst);
|
dt = hero->cb->getTile(dst);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TODO: by the original game rules hero shouldn't be affected by terrain penalty while flying.
|
bool isSailLayer;
|
||||||
/// Also flying movement only has penalty when player moving over blocked tiles.
|
if(indeterminate(isDstSailLayer))
|
||||||
/// So if you only have base flying with 40% penalty you can still ignore terrain penalty while having zero flying penalty.
|
isSailLayer = hero->boat != nullptr && dt->terType->isWater();
|
||||||
int ret = hero->getTileCost(*dt, *ct, ti);
|
else
|
||||||
/// Unfortunately this can't be implemented yet as server don't know when player flying and when he's not.
|
isSailLayer = static_cast<bool>(isDstSailLayer);
|
||||||
/// 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))
|
bool isWaterLayer;
|
||||||
|
if(indeterminate(isDstWaterLayer))
|
||||||
|
isWaterLayer = dt->terType->isWater();
|
||||||
|
else
|
||||||
|
isWaterLayer = static_cast<bool>(isDstWaterLayer);
|
||||||
|
|
||||||
|
int ret = hero->getTileCost(*dt, *ct, ti);
|
||||||
|
if(isSailLayer)
|
||||||
{
|
{
|
||||||
ret = static_cast<int>(ret * (100.0 + ti->valOfBonuses(Bonus::FLYING_MOVEMENT)) / 100.0);
|
if(ct->hasFavorableWinds())
|
||||||
}
|
ret = static_cast<int>(ret * 2.0 / 3);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
else if(ti->hasBonusOfType(Bonus::FLYING_MOVEMENT))
|
||||||
|
vstd::amin(ret, GameConstants::BASE_MOVEMENT_COST + ti->valOfBonuses(Bonus::FLYING_MOVEMENT));
|
||||||
|
else if(isWaterLayer && 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
|
if(src.x != dst.x && src.y != dst.y) //it's diagonal move
|
||||||
{
|
{
|
||||||
int old = ret;
|
int old = ret;
|
||||||
ret = static_cast<int>(ret * M_SQRT2);
|
ret = static_cast<int>(ret * M_SQRT2);
|
||||||
//diagonal move costs too much but normal move is possible - allow diagonal move for remaining move points
|
//diagonal move costs too much but normal move is possible - allow diagonal move for remaining move points
|
||||||
|
// https://heroes.thelazy.net/index.php/Movement#Diagonal_move_exception
|
||||||
if(ret > remainingMovePoints && remainingMovePoints >= old)
|
if(ret > remainingMovePoints && remainingMovePoints >= old)
|
||||||
{
|
{
|
||||||
return remainingMovePoints;
|
return remainingMovePoints;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// TODO: This part need rework in order to work properly with flying and water walking
|
const int left = remainingMovePoints - ret;
|
||||||
/// Currently it's only work properly for normal movement or sailing
|
constexpr auto maxCostOfOneStep = static_cast<int>(175 * M_SQRT2); // diagonal move on Swamp - 247 MP
|
||||||
int left = remainingMovePoints-ret;
|
if(checkLast && left > 0 && left <= maxCostOfOneStep) //it might be the last tile - if no further move possible we take all move points
|
||||||
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;
|
std::vector<int3> vec;
|
||||||
vec.reserve(8); //optimization
|
vec.reserve(8); //optimization
|
||||||
getNeighbours(*dt, dst, vec, ct->terType->isLand(), true);
|
getNeighbours(*dt, dst, vec, ct->terType->isLand(), true);
|
||||||
for(auto & elem : vec)
|
for(const auto & elem : vec)
|
||||||
{
|
{
|
||||||
int fcost = getMovementCost(dst, elem, nullptr, nullptr, left, false);
|
int fcost = getMovementCost(dst, elem, nullptr, nullptr, left, false);
|
||||||
if(fcost <= left)
|
if(fcost <= left)
|
||||||
|
@@ -65,7 +65,7 @@ struct DLL_LINKAGE CGPathNode
|
|||||||
VISITABLE, //tile can be entered as the last tile in path
|
VISITABLE, //tile can be entered as the last tile in path
|
||||||
BLOCKVIS, //visitable from neighboring tile but not passable
|
BLOCKVIS, //visitable from neighboring tile but not passable
|
||||||
FLYABLE, //can only be accessed in air layer
|
FLYABLE, //can only be accessed in air layer
|
||||||
BLOCKED //tile can't be entered nor visited
|
BLOCKED //tile can be neither entered nor visited
|
||||||
};
|
};
|
||||||
|
|
||||||
CGPathNode * theNodeBefore;
|
CGPathNode * theNodeBefore;
|
||||||
@@ -580,8 +580,8 @@ public:
|
|||||||
std::vector<int3> getTeleportExits(const PathNodeInfo & source) const;
|
std::vector<int3> getTeleportExits(const PathNodeInfo & source) const;
|
||||||
|
|
||||||
void getNeighbours(
|
void getNeighbours(
|
||||||
const TerrainTile & srct,
|
const TerrainTile & srcTile,
|
||||||
const int3 & tile,
|
const int3 & srcCoord,
|
||||||
std::vector<int3> & vec,
|
std::vector<int3> & vec,
|
||||||
const boost::logic::tribool & onLand,
|
const boost::logic::tribool & onLand,
|
||||||
const bool limitCoastSailing) const;
|
const bool limitCoastSailing) const;
|
||||||
@@ -591,8 +591,10 @@ public:
|
|||||||
const int3 & dst,
|
const int3 & dst,
|
||||||
const TerrainTile * ct,
|
const TerrainTile * ct,
|
||||||
const TerrainTile * dt,
|
const TerrainTile * dt,
|
||||||
const int remainingMovePoints =- 1,
|
const int remainingMovePoints = -1,
|
||||||
const bool checkLast = true) const;
|
const bool checkLast = true,
|
||||||
|
boost::logic::tribool isDstSailLayer = boost::logic::indeterminate,
|
||||||
|
boost::logic::tribool isDstWaterLayer = boost::logic::indeterminate) const;
|
||||||
|
|
||||||
int getMovementCost(
|
int getMovementCost(
|
||||||
const PathNodeInfo & src,
|
const PathNodeInfo & src,
|
||||||
@@ -606,7 +608,9 @@ public:
|
|||||||
src.tile,
|
src.tile,
|
||||||
dst.tile,
|
dst.tile,
|
||||||
remainingMovePoints,
|
remainingMovePoints,
|
||||||
checkLast
|
checkLast,
|
||||||
|
dst.node->layer == EPathfindingLayer::SAIL,
|
||||||
|
dst.node->layer == EPathfindingLayer::WATER
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -66,10 +66,10 @@ ui32 CGHeroInstance::getTileCost(const TerrainTile & dest, const TerrainTile & f
|
|||||||
{
|
{
|
||||||
int64_t ret = GameConstants::BASE_MOVEMENT_COST;
|
int64_t ret = GameConstants::BASE_MOVEMENT_COST;
|
||||||
|
|
||||||
//if there is road both on dest and src tiles - use road movement cost
|
//if there is road both on dest and src tiles - use src road movement cost
|
||||||
if(dest.roadType->getId() != Road::NO_ROAD && from.roadType->getId() != Road::NO_ROAD)
|
if(dest.roadType->getId() != Road::NO_ROAD && from.roadType->getId() != Road::NO_ROAD)
|
||||||
{
|
{
|
||||||
ret = std::max(dest.roadType->movementCost, from.roadType->movementCost);
|
ret = from.roadType->movementCost;
|
||||||
}
|
}
|
||||||
else if(ti->nativeTerrain != from.terType->getId() &&//the terrain is not native
|
else if(ti->nativeTerrain != from.terType->getId() &&//the terrain is not native
|
||||||
ti->nativeTerrain != ETerrainId::ANY_TERRAIN && //no special creature bonus
|
ti->nativeTerrain != ETerrainId::ANY_TERRAIN && //no special creature bonus
|
||||||
@@ -1106,22 +1106,19 @@ CBonusSystemNode & CGHeroInstance::whereShouldBeAttached(CGameState * gs)
|
|||||||
|
|
||||||
int CGHeroInstance::movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark, const TurnInfo * ti) const
|
int CGHeroInstance::movementPointsAfterEmbark(int MPsBefore, int basicCost, bool disembark, const TurnInfo * ti) const
|
||||||
{
|
{
|
||||||
int ret = 0; //take all MPs by default
|
std::unique_ptr<TurnInfo> turnInfoLocal;
|
||||||
bool localTi = false;
|
|
||||||
if(!ti)
|
if(!ti)
|
||||||
{
|
{
|
||||||
localTi = true;
|
turnInfoLocal = std::make_unique<TurnInfo>(this);
|
||||||
ti = new TurnInfo(this);
|
ti = turnInfoLocal.get();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(!ti->hasBonusOfType(Bonus::FREE_SHIP_BOARDING))
|
||||||
|
return 0; // take all MPs by default
|
||||||
|
|
||||||
int mp1 = ti->getMaxMovePoints(disembark ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL);
|
int mp1 = ti->getMaxMovePoints(disembark ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL);
|
||||||
int mp2 = ti->getMaxMovePoints(disembark ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND);
|
int mp2 = ti->getMaxMovePoints(disembark ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND);
|
||||||
if(ti->hasBonusOfType(Bonus::FREE_SHIP_BOARDING))
|
int ret = static_cast<int>((MPsBefore - basicCost) * static_cast<double>(mp1) / mp2);
|
||||||
ret = static_cast<int>((MPsBefore - basicCost) * static_cast<double>(mp1) / mp2);
|
|
||||||
|
|
||||||
if(localTi)
|
|
||||||
delete ti;
|
|
||||||
|
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -59,7 +59,10 @@ PlayerParams::PlayerParams(MapController & ctrl, int playerId, QWidget *parent)
|
|||||||
{
|
{
|
||||||
auto * ctown = town->town;
|
auto * ctown = town->town;
|
||||||
if(!ctown)
|
if(!ctown)
|
||||||
|
{
|
||||||
ctown = VLC->townh->randomTown;
|
ctown = VLC->townh->randomTown;
|
||||||
|
town->town = ctown;
|
||||||
|
}
|
||||||
if(ctown && town->getOwner().getNum() == playerColor)
|
if(ctown && town->getOwner().getNum() == playerColor)
|
||||||
{
|
{
|
||||||
if(playerInfo.hasMainTown && playerInfo.posOfMainTown == town->pos)
|
if(playerInfo.hasMainTown && playerInfo.posOfMainTown == town->pos)
|
||||||
|
@@ -2342,8 +2342,6 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
|
|||||||
|
|
||||||
//check if destination tile is available
|
//check if destination tile is available
|
||||||
auto pathfinderHelper = std::make_unique<CPathfinderHelper>(gs, h, PathfinderOptions());
|
auto pathfinderHelper = std::make_unique<CPathfinderHelper>(gs, h, PathfinderOptions());
|
||||||
|
|
||||||
pathfinderHelper->updateTurnInfo(0);
|
|
||||||
auto ti = pathfinderHelper->getTurnInfo();
|
auto ti = pathfinderHelper->getTurnInfo();
|
||||||
|
|
||||||
const bool canFly = pathfinderHelper->hasBonusOfType(Bonus::FLYING_MOVEMENT);
|
const bool canFly = pathfinderHelper->hasBonusOfType(Bonus::FLYING_MOVEMENT);
|
||||||
|
Reference in New Issue
Block a user