1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-02-05 13:04:54 +02:00

Merge pull request #2247 from IvanSavenko/pathfinding_fixes

Fix accumulated issues with pathfinding
This commit is contained in:
Ivan Savenko 2023-06-25 21:13:52 +03:00 committed by GitHub
commit 5c3cacd290
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
49 changed files with 296 additions and 231 deletions

View File

@ -802,8 +802,8 @@ void AIGateway::makeTurn()
//for debug purpose
for (auto h : cb->getHeroesInfo())
{
if (h->movement)
logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movement);
if (h->movementPointsRemaining())
logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
}
#if NKAI_TRACE_LEVEL == 0
}

View File

@ -266,7 +266,7 @@ bool isBlockVisitObj(const int3 & pos)
{
if(auto obj = cb->getTopObj(pos))
{
if(obj->blockVisit) //we can't stand on that object
if(obj->isBlockedVisitable()) //we can't stand on that object
return true;
}

View File

@ -187,7 +187,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
if(ai->nullkiller->isHeroLocked(existingHero)
|| existingHero->getArmyStrength() > hero->getArmyStrength()
|| ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN
|| existingHero->movement
|| existingHero->movementPointsRemaining()
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
continue;

View File

@ -206,7 +206,7 @@ Goals::TGoalVec StartupBehavior::decompose() const
for(const CGTownInstance * town : towns)
{
if(town->garrisonHero
&& town->garrisonHero->movement
&& town->garrisonHero->movementPointsRemaining()
&& !town->visitingHero
&& ai->nullkiller->getHeroLockedReason(town->garrisonHero) != HeroLockedReason::DEFENCE)
{

View File

@ -790,7 +790,7 @@ public:
if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
{
auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
auto mpLeft = garrisonHero->movement / (float)garrisonHero->maxMovePoints(true);
auto mpLeft = garrisonHero->movementPointsRemaining() / (float)garrisonHero->movementPointsLimit(true);
evaluationContext.movementCost += mpLeft;
evaluationContext.movementCostByRole[defenderRole] += mpLeft;

View File

@ -78,7 +78,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
try
{
if(hero->movement)
if(hero->movementPointsRemaining() > 0)
{
ai->nullkiller->setActive(hero, node.coord);
@ -117,7 +117,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
}
}
if(hero->movement)
if(hero->movementPointsRemaining())
{
try
{
@ -135,14 +135,14 @@ void ExecuteHeroChain::accept(AIGateway * ai)
return;
}
if(hero->movement > 0)
if(hero->movementPointsRemaining() > 0)
{
CGPath path;
bool isOk = cb->getPathsInfo(hero)->getPath(path, node.coord);
if(isOk && path.nodes.back().turns > 0)
{
logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->getNameTranslated(), hero->movement, node.coord.toString());
logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->getNameTranslated(), hero->movementPointsRemaining(), node.coord.toString());
ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
return;

View File

@ -888,7 +888,7 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
if(actor->hero->tempOwner != ai->playerID)
{
bool onLand = !actor->hero->boat || actor->hero->boat->layer != EPathfindingLayer::SAIL;
actor->initialMovement = actor->hero->maxMovePoints(onLand);
actor->initialMovement = actor->hero->movementPointsLimit(onLand);
}
playerID = actor->hero->tempOwner;
@ -1053,7 +1053,7 @@ struct TowmPortalFinder
return std::nullopt;
AIPathNode * node = nodeOptional.value();
float movementCost = (float)movementNeeded / (float)hero->maxMovePoints(EPathfindingLayer::LAND);
float movementCost = (float)movementNeeded / (float)hero->movementPointsLimit(EPathfindingLayer::LAND);
movementCost += bestNode->getCost();

View File

@ -43,7 +43,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t
{
initialPosition = hero->visitablePos();
layer = hero->boat ? hero->boat->layer : EPathfindingLayer::LAND;
initialMovement = hero->movement;
initialMovement = hero->movementPointsRemaining();
initialTurn = 0;
armyValue = hero->getArmyStrength();
heroFightingStrength = hero->getFightingStrength();
@ -75,7 +75,7 @@ int ChainActor::maxMovePoints(CGPathNode::ELayer layer)
throw std::logic_error("Asking movement points for static actor");
#endif
return hero->maxMovePointsCached(layer, tiCache.get());
return hero->movementPointsLimitCached(layer, tiCache.get());
}
std::string ChainActor::toString() const

View File

@ -216,7 +216,7 @@ bool isBlockVisitObj(const int3 & pos)
{
if(auto obj = cb->getTopObj(pos))
{
if(obj->blockVisit) //we can't stand on that object
if(obj->isBlockedVisitable()) //we can't stand on that object
return true;
}

View File

@ -155,7 +155,7 @@ namespace Goals
// picking up resources does not yield any exploration at all.
// if it blocks the way to some explorable tile AIPathfinder will take care of it
if(obj && obj->blockVisit)
if(obj && obj->isBlockedVisitable())
{
continue;
}
@ -267,7 +267,7 @@ TGoalVec Explore::getAllPossibleSubgoals()
if(!ai->isAbleToExplore(h))
return true;
return !h->movement; //saves time, immobile heroes are useless anyway
return !h->movementPointsRemaining(); //saves time, immobile heroes are useless anyway
});
}

View File

@ -113,7 +113,7 @@ std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
auto initialNode = getOrCreateNode(hpos, hero->boat ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND, NORMAL_CHAIN).value();
initialNode->turns = 0;
initialNode->moveRemains = hero->movement;
initialNode->moveRemains = hero->movementPointsRemaining();
initialNode->danger = 0;
initialNode->setCost(0.0);
@ -245,7 +245,7 @@ void AINodeStorage::calculateTownPortalTeleportations(
auto skillLevel = hero->getSpellSchoolLevel(townPortal);
auto movementCost = GameConstants::BASE_MOVEMENT_COST * (skillLevel >= 3 ? 2 : 3);
if(hero->movement < movementCost)
if(hero->movementPointsRemaining() < movementCost)
{
return;
}

View File

@ -41,6 +41,11 @@ namespace AIPathfinding
std::shared_ptr<AINodeStorage> nodeStorage)
:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), hero(nodeStorage->getHero())
{
options.useEmbarkAndDisembark = true;
options.useTeleportTwoWay = true;
options.useTeleportOneWay = true;
options.useTeleportOneWayRandom = true;
options.useTeleportWhirlpool = true;
}
AIPathfinderConfig::~AIPathfinderConfig() = default;

View File

@ -813,8 +813,8 @@ void VCAI::makeTurn()
//for debug purpose
for (auto h : cb->getHeroesInfo())
{
if (h->movement)
logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movement);
if (h->movementPointsRemaining())
logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
}
}
catch (boost::thread_interrupted & e)
@ -949,7 +949,7 @@ void VCAI::mainLoop()
if (bestGoal->hero) //lock this hero to fulfill goal
{
setGoal(bestGoal->hero, bestGoal);
if (!bestGoal->hero->movement || vstd::contains(invalidPathHeroes, bestGoal->hero))
if (!bestGoal->hero->movementPointsRemaining() || vstd::contains(invalidPathHeroes, bestGoal->hero))
{
if (!vstd::erase_if_present(possibleGoals, bestGoal))
{
@ -1354,7 +1354,7 @@ void VCAI::wander(HeroPtr h)
TimeCheck tc("looking for wander destination");
while(h->movement)
while(h->movementPointsRemaining())
{
validateVisitableObjs();
ah->updatePaths(getMyHeroes());
@ -2031,7 +2031,7 @@ void VCAI::tryRealize(Goals::RecruitHero & g)
void VCAI::tryRealize(Goals::VisitTile & g)
{
if(!g.hero->movement)
if(!g.hero->movementPointsRemaining())
throw cannotFulfillGoalException("Cannot visit tile: hero is out of MPs!");
if(g.tile == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2)
{
@ -2047,7 +2047,7 @@ void VCAI::tryRealize(Goals::VisitTile & g)
void VCAI::tryRealize(Goals::VisitObj & g)
{
auto position = g.tile;
if(!g.hero->movement)
if(!g.hero->movementPointsRemaining())
throw cannotFulfillGoalException("Cannot visit object: hero is out of MPs!");
if(position == g.hero->visitablePos() && cb->getVisitableObjs(g.hero->visitablePos()).size() < 2)
{
@ -2062,7 +2062,7 @@ void VCAI::tryRealize(Goals::VisitObj & g)
void VCAI::tryRealize(Goals::VisitHero & g)
{
if(!g.hero->movement)
if(!g.hero->movementPointsRemaining())
throw cannotFulfillGoalException("Cannot visit target hero: hero is out of MPs!");
const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid));
@ -2263,7 +2263,7 @@ bool VCAI::canAct(HeroPtr h) const
return false;
}
return h->movement;
return h->movementPointsRemaining();
}
HeroPtr VCAI::primaryHero() const
@ -2412,7 +2412,7 @@ void VCAI::performTypicalActions()
if(!h) //hero might be lost. getUnblockedHeroes() called once on start of turn
continue;
logAi->debug("Hero %s started wandering, MP=%d", h->getNameTranslated(), h->movement);
logAi->debug("Hero %s started wandering, MP=%d", h->getNameTranslated(), h->movementPointsRemaining());
makePossibleUpgrades(*h);
pickBestArtifacts(*h);
try

View File

@ -1922,10 +1922,27 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
for (auto & elem : path.nodes)
elem.coord = h->convertFromVisitablePos(elem.coord);
TerrainId currentTerrain = ETerrainId::NONE;
TerrainId newTerrain;
bool wasOnRoad = true;
int sh = -1;
int soundChannel = -1;
std::string soundName;
auto getMovementSoundFor = [&](const CGHeroInstance * hero, int3 posPrev, int3 posNext) -> std::string
{
// flying movement sound
if (hero->hasBonusOfType(BonusType::FLYING_MOVEMENT))
return "HORSE10.wav";
auto prevTile = cb->getTile(h->convertToVisitablePos(posPrev));
auto nextTile = cb->getTile(h->convertToVisitablePos(posNext));
auto prevRoad = prevTile->roadType;
auto nextRoad = nextTile->roadType;
bool movingOnRoad = prevRoad->getId() != Road::NO_ROAD && nextRoad->getId() != Road::NO_ROAD;
if (movingOnRoad)
return nextTile->terType->horseSound;
else
return nextTile->terType->horseSoundPenalty;
};
auto canStop = [&](CGPathNode * node) -> bool
{
@ -1943,18 +1960,13 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
int3 prevCoord = path.nodes[i].coord;
int3 nextCoord = path.nodes[i-1].coord;
auto prevRoad = cb->getTile(h->convertToVisitablePos(prevCoord))->roadType;
auto nextRoad = cb->getTile(h->convertToVisitablePos(nextCoord))->roadType;
bool movingOnRoad = prevRoad->getId() != Road::NO_ROAD && nextRoad->getId() != Road::NO_ROAD;
auto prevObject = getObj(prevCoord, prevCoord == h->pos);
auto nextObjectTop = getObj(nextCoord, false);
auto nextObject = getObj(nextCoord, true);
auto destTeleportObj = getDestTeleportObj(prevObject, nextObjectTop, nextObject);
if (isTeleportAction(path.nodes[i-1].action) && destTeleportObj != nullptr)
{
CCS->soundh->stopSound(sh);
CCS->soundh->stopSound(soundChannel);
destinationTeleport = destTeleportObj->id;
destinationTeleportPos = nextCoord;
doMovement(h->pos, false);
@ -1966,10 +1978,8 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
}
if(i != path.nodes.size() - 1)
{
if (movingOnRoad)
sh = CCS->soundh->playSound(VLC->terrainTypeHandler->getById(currentTerrain)->horseSound, -1);
else
sh = CCS->soundh->playSound(VLC->terrainTypeHandler->getById(currentTerrain)->horseSoundPenalty, -1);
soundName = getMovementSoundFor(h, prevCoord, nextCoord);
soundChannel = CCS->soundh->playSound(soundName, -1);
}
continue;
}
@ -1980,23 +1990,16 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
break;
}
// Start a new sound for the hero movement or let the existing one carry on.
#if 0
// TODO
if (hero is flying && sh == -1)
sh = CCS->soundh->playSound(soundBase::horseFlying, -1);
#endif
{
newTerrain = cb->getTile(h->convertToVisitablePos(prevCoord))->terType->getId();
if(newTerrain != currentTerrain || wasOnRoad != movingOnRoad)
// Start a new sound for the hero movement or let the existing one carry on.
std::string newSoundName = getMovementSoundFor(h, prevCoord, nextCoord);
if(newSoundName != soundName)
{
CCS->soundh->stopSound(sh);
if (movingOnRoad)
sh = CCS->soundh->playSound(VLC->terrainTypeHandler->getById(newTerrain)->horseSound, -1);
else
sh = CCS->soundh->playSound(VLC->terrainTypeHandler->getById(newTerrain)->horseSoundPenalty, -1);
currentTerrain = newTerrain;
wasOnRoad = movingOnRoad;
soundName = newSoundName;
CCS->soundh->stopSound(soundChannel);
soundChannel = CCS->soundh->playSound(soundName, -1);
}
}
@ -2022,7 +2025,7 @@ void CPlayerInterface::doMoveHero(const CGHeroInstance * h, CGPath path)
break;
}
CCS->soundh->stopSound(sh);
CCS->soundh->stopSound(soundChannel);
}
//Update cursor so icon can change if needed when it reappears; doesn;'t apply if a dialog box pops up at the end of the movement

View File

@ -388,7 +388,7 @@ void ClientCommandManager::handleTellCommand(std::istringstream& singleWordBuffe
void ClientCommandManager::handleMpCommand()
{
if(const CGHeroInstance* h = LOCPLINT->localState->getCurrentHero())
printCommandMessage(std::to_string(h->movement) + "; max: " + std::to_string(h->maxMovePoints(true)) + "/" + std::to_string(h->maxMovePoints(false)) + "\n");
printCommandMessage(std::to_string(h->movementPointsRemaining()) + "; max: " + std::to_string(h->movementPointsLimit(true)) + "/" + std::to_string(h->movementPointsLimit(false)) + "\n");
}
void ClientCommandManager::handleSetCommand(std::istringstream& singleWordBuffer)

View File

@ -277,6 +277,8 @@ void ApplyClientNetPackVisitor::visitMoveArtifact(MoveArtifact & pack)
moveArtifact(pack.src.owningPlayer());
if(pack.src.owningPlayer() != pack.dst.owningPlayer())
moveArtifact(pack.dst.owningPlayer());
cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
}
void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
@ -303,11 +305,15 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack)
{
callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactAssembled, pack.al);
cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
}
void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack)
{
callInterfaceIfPresent(cl, pack.al.owningPlayer(), &IGameEventsReceiver::artifactDisassembled, pack.al);
cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
}
void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack)

View File

@ -124,7 +124,7 @@ const CGHeroInstance * PlayerLocalState::getNextWanderingHero(const CGHeroInstan
if (isHeroSleeping(hero))
continue;
if (hero->movement == 0)
if (hero->movementPointsRemaining() == 0)
continue;
if (!firstSuitable)

View File

@ -125,6 +125,12 @@ void AdventureMapInterface::activate()
}
GH.fakeMouseMove(); //to restore the cursor
// workaround for an edge case:
// if player unequips Angel Wings / Boots of Levitation of currently active hero
// game will correctly invalidate paths but current route will not be updated since verifyPath() is not called for current hero
if (LOCPLINT->makingTurn && LOCPLINT->localState->getCurrentHero())
LOCPLINT->localState->verifyPath(LOCPLINT->localState->getCurrentHero());
}
void AdventureMapInterface::deactivate()
@ -676,7 +682,7 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos)
void AdventureMapInterface::showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode)
{
const int maxMovementPointsAtStartOfLastTurn = pathNode.turns > 0 ? hero.maxMovePoints(pathNode.layer == EPathfindingLayer::LAND) : hero.movement;
const int maxMovementPointsAtStartOfLastTurn = pathNode.turns > 0 ? hero.movementPointsLimit(pathNode.layer == EPathfindingLayer::LAND) : hero.movementPointsRemaining();
const int movementPointsLastTurnCost = maxMovementPointsAtStartOfLastTurn - pathNode.moveRemains;
const int remainingPointsAfterMove = pathNode.turns == 0 ? pathNode.moveRemains : 0;

View File

@ -224,7 +224,7 @@ void AdventureMapShortcuts::endTurn()
{
for(auto hero : LOCPLINT->localState->getWanderingHeroes())
{
if(!LOCPLINT->localState->isHeroSleeping(hero) && hero->movement > 0)
if(!LOCPLINT->localState->isHeroSleeping(hero) && hero->movementPointsRemaining() > 0)
{
// Only show hero reminder if conditions met:
// - There still movement points
@ -418,7 +418,7 @@ bool AdventureMapShortcuts::optionHeroSelected()
bool AdventureMapShortcuts::optionHeroCanMove()
{
const auto * hero = LOCPLINT->localState->getCurrentHero();
return optionInMapView() && hero && hero->movement != 0 && LOCPLINT->localState->hasPath(hero);
return optionInMapView() && hero && hero->movementPointsRemaining() != 0 && LOCPLINT->localState->hasPath(hero);
}
bool AdventureMapShortcuts::optionHasNextHero()

View File

@ -234,7 +234,7 @@ CHeroList::CHeroItem::CHeroItem(CHeroList *parent, const CGHeroInstance * Hero)
void CHeroList::CHeroItem::update()
{
movement->setFrame(std::min<size_t>(movement->size()-1, hero->movement / 100));
movement->setFrame(std::min<size_t>(movement->size()-1, hero->movementPointsRemaining() / 100));
mana->setFrame(std::min<size_t>(mana->size()-1, hero->mana / 5));
redraw();
}

View File

@ -329,6 +329,20 @@
"commanders": false
},
"pathfinder" :
{
// if enabled, pathfinder will take use of any available boats
"useBoat" : true,
// if enabled, pathfinder will take use of any bidirectional monoliths
"useMonolithTwoWay" : true,
// if enabled, pathfinder will take use of one-way monolith that only have one known exit
"useMonolithOneWayUnique" : false,
// if enabled, pathfinder will take use of one-way monoliths with multiple exits.
"useMonolithOneWayRandom" : false,
// if enabled and hero has whirlpool protection effect, pathfinder will take use of whirpools
"useWhirlpool" : true
},
"bonuses" :
{
"global" :

View File

@ -640,6 +640,8 @@
"shipwreck" : {
"index" : 0,
"resetDuration" : 0,
"blockedVisitable" : true,
"coastVisitable" : true,
"name" : "Shipwreck",
"aiValue" : 2000,
"rmg" : {
@ -732,6 +734,7 @@
"derelictShip" : {
"index" : 0,
"resetDuration" : 0,
"blockedVisitable" : true,
"name" : "Derelict Ship",
"aiValue" : 4000,
"rmg" : {

View File

@ -3,7 +3,7 @@
{
"type" : "object",
"$schema" : "http://json-schema.org/draft-04/schema",
"required" : [ "general", "video", "adventure", "pathfinder", "battle", "server", "logging", "launcher", "gameTweaks" ],
"required" : [ "general", "video", "adventure", "battle", "server", "logging", "launcher", "gameTweaks" ],
"definitions" : {
"logLevelEnum" : {
"type" : "string",
@ -221,74 +221,6 @@
}
}
},
"pathfinder" : {
"type" : "object",
"additionalProperties" : false,
"default" : {},
"required" : [ "teleports", "layers", "oneTurnSpecialLayersLimit", "originalMovementRules", "lightweightFlyingMode" ],
"properties" : {
"layers" : {
"type" : "object",
"additionalProperties" : false,
"default" : {},
"required" : [ "sailing", "waterWalking", "flying" ],
"properties" : {
"sailing" : {
"type" : "boolean",
"default" : true
},
"waterWalking" : {
"type" : "boolean",
"default" : true
},
"flying" : {
"type" : "boolean",
"default" : true
}
}
},
"teleports" : {
"type" : "object",
"additionalProperties" : false,
"default" : {},
"required" : [ "twoWay", "oneWay", "oneWayRandom", "whirlpool", "castleGate" ],
"properties" : {
"twoWay" : {
"type" : "boolean",
"default" : true
},
"oneWay" : {
"type" : "boolean",
"default" : true
},
"oneWayRandom" : {
"type" : "boolean",
"default" : false
},
"whirlpool" : {
"type" : "boolean",
"default" : true
},
"castleGate" : {
"type" : "boolean",
"default" : false
}
}
},
"oneTurnSpecialLayersLimit" : {
"type" : "boolean",
"default" : true
},
"originalMovementRules" : {
"type" : "boolean",
"default" : false
},
"lightweightFlyingMode" : {
"type" : "boolean",
"default" : false
}
}
},
"battle" : {
"type" : "object",
"additionalProperties" : false,

View File

@ -1900,7 +1900,7 @@ std::vector<CGObjectInstance*> CGameState::guardingCreatures (int3 pos) const
{
for (CGObjectInstance* obj : posTile.visitableObjects)
{
if(obj->blockVisit)
if(obj->isBlockedVisitable())
{
if (obj->ID == Obj::MONSTER) // Monster
guards.push_back(obj);

View File

@ -33,6 +33,8 @@ std::vector<int> IGameSettings::getVector(EGameSettings option) const
return getValue(option).convertTo<std::vector<int>>();
}
GameSettings::~GameSettings() = default;
GameSettings::GameSettings()
: gameSettings(static_cast<size_t>(EGameSettings::OPTIONS_COUNT))
{
@ -91,6 +93,11 @@ void GameSettings::load(const JsonNode & input)
{EGameSettings::TEXTS_ROAD, "textData", "road" },
{EGameSettings::TEXTS_SPELL, "textData", "spell" },
{EGameSettings::TEXTS_TERRAIN, "textData", "terrain" },
{EGameSettings::PATHFINDER_USE_BOAT, "pathfinder", "useBoat" },
{EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY, "pathfinder", "useMonolithTwoWay" },
{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE, "pathfinder", "useMonolithOneWayUnique" },
{EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM, "pathfinder", "useMonolithOneWayRandom" },
{EGameSettings::PATHFINDER_USE_WHIRLPOOL, "pathfinder", "useWhirlpool" },
{EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP, "towns", "buildingsPerTurnCap" },
{EGameSettings::TOWNS_STARTING_DWELLING_CHANCES, "towns", "startingDwellingChances" },
};

View File

@ -57,6 +57,11 @@ enum class EGameSettings
MAP_FORMAT_HORN_OF_THE_ABYSS,
MAP_FORMAT_JSON_VCMI,
MAP_FORMAT_IN_THE_WAKE_OF_GODS,
PATHFINDER_USE_BOAT,
PATHFINDER_USE_MONOLITH_TWO_WAY,
PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE,
PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM,
PATHFINDER_USE_WHIRLPOOL,
TOWNS_BUILDINGS_PER_TURN_CAP,
TOWNS_STARTING_DWELLING_CHANCES,
COMBAT_ONE_HEX_TRIGGERS_OBSTACLES,
@ -68,6 +73,7 @@ class DLL_LINKAGE IGameSettings
{
public:
virtual const JsonNode & getValue(EGameSettings option) const = 0;
virtual ~IGameSettings() = default;
bool getBoolean(EGameSettings option) const;
int64_t getInteger(EGameSettings option) const;
@ -75,12 +81,14 @@ public:
std::vector<int> getVector(EGameSettings option) const;
};
class DLL_LINKAGE GameSettings final : public IGameSettings
class DLL_LINKAGE GameSettings final : public IGameSettings, boost::noncopyable
{
std::vector<JsonNode> gameSettings;
public:
GameSettings();
~GameSettings();
void load(const JsonNode & input);
const JsonNode & getValue(EGameSettings option) const override;

View File

@ -903,11 +903,9 @@ void SetMovePoints::applyGs(CGameState * gs) const
assert(hero);
if(absolute)
hero->movement = val;
hero->setMovementPoints(val);
else
hero->movement += val;
vstd::amax(hero->movement, 0); //not less than 0
hero->setMovementPoints(hero->movementPointsRemaining() + val);
}
void FoWChange::applyGs(CGameState *gs)
@ -1276,7 +1274,7 @@ void TryMoveHero::applyGs(CGameState *gs)
return;
}
h->movement = movePoints;
h->setMovementPoints(movePoints);
if((result == SUCCESS || result == BLOCKING_VISIT || result == EMBARK || result == DISEMBARK) && start != end)
{
@ -1422,11 +1420,11 @@ void HeroRecruited::applyGs(CGameState * gs) const
{ // this is a fresh hero who hasn't appeared yet
if (boatId >= 0) //Hero spawns on water
{
h->movement = h->maxMovePoints(false);
h->setMovementPoints(h->movementPointsLimit(false));
}
else
{
h->movement = h->maxMovePoints(true);
h->setMovementPoints(h->movementPointsLimit(true));
}
}
@ -1479,7 +1477,7 @@ void GiveHero::applyGs(CGameState * gs) const
h->appearance = VLC->objtypeh->getHandlerFor(Obj::HERO, h->type->heroClass->getIndex())->getTemplates().front();
h->setOwner(player);
h->movement = h->maxMovePoints(true);
h->setMovementPoints(h->movementPointsLimit(true));
h->pos = h->convertFromVisitablePos(oldVisitablePos);
gs->map->heroesOnMap.emplace_back(h);
gs->getPlayerState(h->getOwner())->heroes.emplace_back(h);
@ -2052,7 +2050,7 @@ void NewTurn::applyGs(CGameState *gs)
logGlobal->error("Hero %d not found in NewTurn::applyGs", h.id.getNum());
continue;
}
hero->movement = h.move;
hero->setMovementPoints(h.move);
hero->mana = h.mana;
}

View File

@ -30,6 +30,8 @@ void CBankInstanceConstructor::initTypeData(const JsonNode & input)
levels = input["levels"].Vector();
bankResetDuration = static_cast<si32>(input["resetDuration"].Float());
blockVisit = input["blockedVisitable"].Bool();
coastVisitable = input["coastVisitable"].Bool();
}
BankConfig CBankInstanceConstructor::generateConfig(const JsonNode & level, CRandomGenerator & rng) const
@ -58,6 +60,8 @@ BankConfig CBankInstanceConstructor::generateConfig(const JsonNode & level, CRan
void CBankInstanceConstructor::randomizeObject(CBank * bank, CRandomGenerator & rng) const
{
bank->resetDuration = bankResetDuration;
bank->blockVisit = blockVisit;
bank->coastVisitable = coastVisitable;
si32 totalChance = 0;
for(const auto & node : levels)

View File

@ -80,12 +80,18 @@ class CBankInstanceConstructor : public CDefaultObjectTypeHandler<CBank>
BankConfig generateConfig(const JsonNode & conf, CRandomGenerator & rng) const;
JsonVector levels;
// all banks of this type will be reset N days after clearing,
si32 bankResetDuration = 0;
// bank is only visitable from adjacent tile
bool blockVisit;
// bank is visitable from land even when bank is on water tile
bool coastVisitable;
protected:
void initTypeData(const JsonNode & input) override;
public:
// all banks of this type will be reset N days after clearing,
si32 bankResetDuration = 0;
void randomizeObject(CBank * object, CRandomGenerator & rng) const override;
@ -97,6 +103,8 @@ public:
{
h & levels;
h & bankResetDuration;
h & blockVisit;
h & coastVisitable;
h & static_cast<CDefaultObjectTypeHandler<CBank>&>(*this);
}
};

View File

@ -43,6 +43,11 @@ void CBank::initObj(CRandomGenerator & rand)
VLC->objtypeh->getHandlerFor(ID, subID)->configureObject(this, rand);
}
bool CBank::isCoastVisitable() const
{
return coastVisitable;
}
std::string CBank::getHoverText(PlayerColor player) const
{
// TODO: record visited players

View File

@ -21,6 +21,7 @@ class DLL_LINKAGE CBank : public CArmedInstance
std::unique_ptr<BankConfig> bc;
ui32 daycounter;
ui32 resetDuration;
bool coastVisitable;
void setPropertyDer(ui8 what, ui32 val) override;
void doVisit(const CGHeroInstance * hero) const;
@ -35,6 +36,7 @@ public:
std::string getHoverText(PlayerColor player) const override;
void newTurn(CRandomGenerator & rand) const override;
bool wasVisited (PlayerColor player) const override;
bool isCoastVisitable() const override;
void onHeroVisit(const CGHeroInstance * h) const override;
void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
void blockingDialogAnswered(const CGHeroInstance *hero, ui32 answer) const override;
@ -45,6 +47,7 @@ public:
h & daycounter;
h & bc;
h & resetDuration;
h & coastVisitable;
}
friend class CBankInstanceConstructor;

View File

@ -65,7 +65,7 @@ static int lowestSpeed(const CGHeroInstance * chi)
return ret;
}
ui32 CGHeroInstance::getTileCost(const TerrainTile & dest, const TerrainTile & from, const TurnInfo * ti) const
ui32 CGHeroInstance::getTileMovementCost(const TerrainTile & dest, const TerrainTile & from, const TurnInfo * ti) const
{
int64_t ret = GameConstants::BASE_MOVEMENT_COST;
@ -122,6 +122,11 @@ TerrainId CGHeroInstance::getNativeTerrain() const
return nativeTerrain;
}
bool CGHeroInstance::isCoastVisitable() const
{
return true;
}
BattleField CGHeroInstance::getBattlefield() const
{
return BattleField::NONE;
@ -196,10 +201,20 @@ bool CGHeroInstance::canLearnSkill(const SecondarySkill & which) const
return true;
}
int CGHeroInstance::maxMovePoints(bool onLand) const
int CGHeroInstance::movementPointsRemaining() const
{
return movement;
}
void CGHeroInstance::setMovementPoints(int points)
{
movement = std::max(0, points);
}
int CGHeroInstance::movementPointsLimit(bool onLand) const
{
TurnInfo ti(this);
return maxMovePointsCached(onLand, &ti);
return movementPointsLimitCached(onLand, &ti);
}
int CGHeroInstance::getLowestCreatureSpeed() const
@ -219,7 +234,7 @@ void CGHeroInstance::updateArmyMovementBonus(bool onLand, const TurnInfo * ti) c
}
}
int CGHeroInstance::maxMovePointsCached(bool onLand, const TurnInfo * ti) const
int CGHeroInstance::movementPointsLimitCached(bool onLand, const TurnInfo * ti) const
{
updateArmyMovementBonus(onLand, ti);
return ti->valOfBonuses(BonusType::MOVEMENT, !!onLand);
@ -449,14 +464,14 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const
const auto boatPos = visitablePos();
if (cb->gameState()->map->getTile(boatPos).isWater())
{
smp.val = maxMovePoints(false);
smp.val = movementPointsLimit(false);
//Create a new boat for hero
cb->createObject(boatPos, Obj::BOAT, getBoatType().getNum());
boatId = cb->getTopObj(boatPos)->id;
}
else
{
smp.val = maxMovePoints(true);
smp.val = movementPointsLimit(true);
}
cb->giveHero(id, h->tempOwner, boatId); //recreates def and adds hero to player
cb->setObjProperty(id, ObjProperty::ID, Obj::HERO); //set ID to 34 AFTER hero gets correct flag color
@ -1165,7 +1180,7 @@ int CGHeroInstance::movementPointsAfterEmbark(int MPsBefore, int basicCost, bool
EDiggingStatus CGHeroInstance::diggingStatus() const
{
if(static_cast<int>(movement) < maxMovePoints(true))
if(static_cast<int>(movement) < movementPointsLimit(true))
return EDiggingStatus::LACK_OF_MOVEMENT;
if(!VLC->arth->objects[ArtifactID::GRAIL]->canBePutAt(this))
return EDiggingStatus::BACKPACK_IS_FULL;

View File

@ -49,6 +49,7 @@ class DLL_LINKAGE CGHeroInstance : public CArmedInstance, public IBoatGenerator,
private:
std::set<SpellID> spells; //known spells (spell IDs)
mutable int lowestCreatureSpeed;
ui32 movement; //remaining movement points
public:
@ -67,7 +68,6 @@ public:
si32 portrait; //may be custom
si32 mana; // remaining spell points
std::vector<std::pair<SecondarySkill,ui8> > secSkills; //first - ID of skill, second - level of skill (1 - basic, 2 - adv., 3 - expert); if hero has ability (-1, -1) it meansthat it should have default secondary abilities
ui32 movement; //remaining movement points
EHeroGender gender;
std::string nameCustom;
@ -155,7 +155,6 @@ public:
EAlignment getAlignment() const;
bool needsLastStack()const override;
ui32 getTileCost(const TerrainTile & dest, const TerrainTile & from, const TurnInfo * ti) const; //move cost - applying pathfinding skill, road and terrain modifiers. NOT includes diagonal move penalty, last move levelling
//INativeTerrainProvider
FactionID getFaction() const override;
TerrainId getNativeTerrain() const override;
@ -196,9 +195,14 @@ public:
void setSecSkillLevel(const SecondarySkill & which, int val, bool abs); // abs == 0 - changes by value; 1 - sets to value
void levelUp(const std::vector<SecondarySkill> & skills);
int maxMovePoints(bool onLand) const;
/// returns base movement cost for movement between specific tiles. Does not accounts for diagonal movement or last tile exception
ui32 getTileMovementCost(const TerrainTile & dest, const TerrainTile & from, const TurnInfo * ti) const;
void setMovementPoints(int points);
int movementPointsRemaining() const;
int movementPointsLimit(bool onLand) const;
//cached version is much faster, TurnInfo construction is costly
int maxMovePointsCached(bool onLand, const TurnInfo * ti) const;
int movementPointsLimitCached(bool onLand, const TurnInfo * ti) const;
//update army movement bonus
void updateArmyMovementBonus(bool onLand, const TurnInfo * ti) const;
@ -286,6 +290,7 @@ public:
void updateFrom(const JsonNode & data) override;
bool isCoastVisitable() const override;
BattleField getBattlefield() const override;
protected:
void setPropertyDer(ui8 what, ui32 val) override;//synchr

View File

@ -282,6 +282,16 @@ bool CGObjectInstance::isVisitable() const
return appearance->isVisitable();
}
bool CGObjectInstance::isBlockedVisitable() const
{
return blockVisit;
}
bool CGObjectInstance::isCoastVisitable() const
{
return false;
}
bool CGObjectInstance::passableFor(PlayerColor color) const
{
return false;

View File

@ -34,8 +34,6 @@ public:
ObjectInstanceID id;
/// Defines appearance of object on map (animation, blocked tiles, blit order, etc)
std::shared_ptr<const ObjectTemplate> appearance;
/// If true hero can visit this object only from neighbouring tiles and can't stand on this object
bool blockVisit;
std::string instanceName;
std::string typeName;
@ -49,6 +47,8 @@ public:
/// "center" tile from which the sight distance is calculated
int3 getSightCenter() const;
/// If true hero can visit this object only from neighbouring tiles and can't stand on this object
bool blockVisit;
PlayerColor getOwner() const override
{
@ -68,7 +68,15 @@ public:
bool coveringAt(int x, int y) const; //returns true if object covers with picture location (x, y) (h3m pos)
std::set<int3> getBlockedPos() const; //returns set of positions blocked by this object
std::set<int3> getBlockedOffsets() const; //returns set of relative positions blocked by this object
bool isVisitable() const; //returns true if object is visitable
/// returns true if object is visitable
bool isVisitable() const;
/// If true hero can visit this object only from neighbouring tiles and can't stand on this object
virtual bool isBlockedVisitable() const;
/// If true this object can be visited by hero standing on the coast
virtual bool isCoastVisitable() const;
virtual BattleField getBattlefield() const;

View File

@ -246,7 +246,6 @@ public:
{
h & static_cast<IQuestObject&>(*this);
h & static_cast<CGObjectInstance&>(*this);
h & blockVisit;
}
};

View File

@ -1294,6 +1294,11 @@ CGBoat::CGBoat()
layer = EPathfindingLayer::EEPathfindingLayer::SAIL;
}
bool CGBoat::isCoastVisitable() const
{
return true;
}
void CGSirens::initObj(CRandomGenerator & rand)
{
blockVisit = true;

View File

@ -359,6 +359,7 @@ public:
std::array<std::string, PlayerColor::PLAYER_LIMIT_I> flagAnimations;
CGBoat();
bool isCoastVisitable() const override;
template <typename Handler> void serialize(Handler &h, const int version)
{

View File

@ -313,7 +313,7 @@ int3 CMap::guardingCreaturePosition (int3 pos) const
{
for (CGObjectInstance* obj : posTile.visitableObjects)
{
if(obj->blockVisit)
if(obj->isBlockedVisitable())
{
if (obj->ID == Obj::MONSTER) // Monster
return pos;

View File

@ -63,7 +63,7 @@ struct DLL_LINKAGE CGPathNode
CGPathNode * theNodeBefore;
int3 coord; //coordinates
ELayer layer;
ui32 moveRemains; //remaining movement points after hero reaches the tile
int moveRemains; //remaining movement points after hero reaches the tile
ui8 turns; //how many turns we have to wait before reaching the tile - 0 means current turn
EPathAccessibility accessible;

View File

@ -621,7 +621,7 @@ int CPathfinderHelper::getMovementCost(
bool isAirLayer = (hero->boat && hero->boat->layer == EPathfindingLayer::AIR) || ti->hasBonusOfType(BonusType::FLYING_MOVEMENT);
int ret = hero->getTileCost(*dt, *ct, ti);
int ret = hero->getTileMovementCost(*dt, *ct, ti);
if(isSailLayer)
{
if(ct->hasFavorableWinds())

View File

@ -123,7 +123,7 @@ std::vector<CGPathNode *> NodeStorage::getInitialNodes()
auto * initialNode = getNode(out.hpos, out.hero->boat ? out.hero->boat->layer : EPathfindingLayer::LAND);
initialNode->turns = 0;
initialNode->moveRemains = out.hero->movement;
initialNode->moveRemains = out.hero->movementPointsRemaining();
initialNode->setCost(0.0);
if(!initialNode->coord.valid())

View File

@ -10,7 +10,8 @@
#include "StdInc.h"
#include "PathfinderOptions.h"
#include "../CConfigHandler.h"
#include "../GameSettings.h"
#include "../VCMI_Lib.h"
#include "NodeStorage.h"
#include "PathfindingRules.h"
#include "CPathfinder.h"
@ -18,20 +19,18 @@
VCMI_LIB_NAMESPACE_BEGIN
PathfinderOptions::PathfinderOptions()
: useFlying(true)
, useWaterWalking(true)
, useEmbarkAndDisembark(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_BOAT))
, useTeleportTwoWay(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_TWO_WAY))
, useTeleportOneWay(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_UNIQUE))
, useTeleportOneWayRandom(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_MONOLITH_ONE_WAY_RANDOM))
, useTeleportWhirlpool(VLC->settings()->getBoolean(EGameSettings::PATHFINDER_USE_WHIRLPOOL))
, useCastleGate(false)
, lightweightFlyingMode(false)
, oneTurnSpecialLayersLimit(true)
, originalMovementRules(false)
{
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();
}
PathfinderConfig::PathfinderConfig(std::shared_ptr<INodeStorage> nodeStorage, std::vector<std::shared_ptr<IPathfindingRule>> rules):

View File

@ -42,7 +42,7 @@ namespace PathfinderUtil
{
for(const CGObjectInstance * obj : tinfo.visitableObjects)
{
if(obj->blockVisit)
if(obj->isBlockedVisitable())
return EPathAccessibility::BLOCKVIS;
else if(obj->passableFor(player))
return EPathAccessibility::ACCESSIBLE;
@ -70,8 +70,7 @@ namespace PathfinderUtil
break;
case ELayer::AIR:
if(tinfo.blocked || tinfo.terType->isLand())
return EPathAccessibility::FLYABLE;
return EPathAccessibility::FLYABLE;
break;
}

View File

@ -27,43 +27,65 @@ void MovementCostRule::process(
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);
const float currentCost = destination.cost;
const int currentTurnsUsed = destination.turn;
const int currentMovePointsLeft = destination.movementLeft;
const int sourceLayerMaxMovePoints = pathfinderHelper->getMaxMovePoints(source.node->layer);
if(remains < 0)
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
costAtNextTile += static_cast<float>(moveAtNextTile) / sourceLayerMaxMovePoints;//we spent all points of current turn
pathfinderHelper->updateTurnInfo(++turnAtNextTile);
// 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
int destinationLayerMaxMovePoints = pathfinderHelper->getMaxMovePoints(destination.node->layer);
destinationCost += static_cast<float>(currentMovePointsLeft) / sourceLayerMaxMovePoints;
destTurnsUsed += 1;
destMovePointsLeft = sourceLayerMaxMovePoints;
moveAtNextTile = destinationLayerMaxMovePoints;
// update move cost - it might have changed since hero now makes next turn and replenished his pool
moveCostPoints = pathfinderHelper->getMovementCost(source, destination, destMovePointsLeft);
cost = pathfinderHelper->getMovementCost(source, destination, moveAtNextTile); //cost must be updated, movement points changed :(
remains = moveAtNextTile - cost;
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
remains = pathfinderHelper->movementPointsAfterEmbark(moveAtNextTile, cost, (destination.action == EPathNodeAction::DISEMBARK));
cost = moveAtNextTile - remains;
// 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;
}
costAtNextTile += static_cast<float>(cost) / sourceLayerMaxMovePoints;
// pathfinder / priority queue does not supports negative costs
assert(destinationCost >= currentCost);
destination.cost = costAtNextTile;
destination.turn = turnAtNextTile;
destination.movementLeft = remains;
destination.cost = destinationCost;
destination.turn = destTurnsUsed;
destination.movementLeft = destMovePointsLeft;
if(destination.isBetterWay() &&
((source.node->turns == turnAtNextTile && remains) || pathfinderHelper->passOneTurnLimitCheck(source)))
((source.node->turns == destTurnsUsed && destMovePointsLeft) || pathfinderHelper->passOneTurnLimitCheck(source)))
{
pathfinderConfig->nodeStorage->commit(destination, source);
@ -157,7 +179,7 @@ void DestinationActionRule::process(
}
else if(destination.isGuardianTile)
action = EPathNodeAction::BATTLE;
else if(destination.nodeObject->blockVisit && !(pathfinderConfig->options.useCastleGate && destination.nodeObject->ID == Obj::TOWN))
else if(destination.nodeObject->isBlockedVisitable() && !(pathfinderConfig->options.useCastleGate && destination.nodeObject->ID == Obj::TOWN))
action = EPathNodeAction::BLOCKING_VISIT;
if(action == EPathNodeAction::NORMAL)
@ -301,7 +323,7 @@ PathfinderBlockingRule::BlockingReason MovementToDestinationRule::getBlockingRea
if(!destination.isNodeObjectVisitable())
return BlockingReason::DESTINATION_BLOCKED;
if(destination.nodeObject->ID != Obj::BOAT && !destination.nodeHero)
if(!destination.nodeHero && !destination.nodeObject->isCoastVisitable())
return BlockingReason::DESTINATION_BLOCKED;
}
else if(destination.isNodeObjectVisitable() && destination.nodeObject->ID == Obj::BOAT)

View File

@ -107,9 +107,9 @@ int TurnInfo::valOfBonuses(BonusType type, int subtype) const
int TurnInfo::getMaxMovePoints(const EPathfindingLayer & layer) const
{
if(maxMovePointsLand == -1)
maxMovePointsLand = hero->maxMovePointsCached(true, this);
maxMovePointsLand = hero->movementPointsLimitCached(true, this);
if(maxMovePointsWater == -1)
maxMovePointsWater = hero->maxMovePointsCached(false, this);
maxMovePointsWater = hero->movementPointsLimitCached(false, this);
return layer == EPathfindingLayer::SAIL ? maxMovePointsWater : maxMovePointsLand;
}

View File

@ -80,10 +80,10 @@ void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Re
{
SetMovePoints smp;
smp.hid = hero->id;
smp.val = hero->movement;
smp.val = hero->movementPointsRemaining();
if (info.reward.movePercentage >= 0) // percent from max
smp.val = hero->maxMovePoints(hero->boat && hero->boat->layer == EPathfindingLayer::SAIL) * info.reward.movePercentage / 100;
smp.val = hero->movementPointsLimit(hero->boat && hero->boat->layer == EPathfindingLayer::SAIL) * info.reward.movePercentage / 100;
smp.val = std::max<si32>(0, smp.val + info.reward.movePoints);
cb->setMovePoints(&smp);

View File

@ -297,7 +297,7 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm
return ESpellCastResult::ERROR;
}
if(parameters.caster->getHeroCaster()->movement <= 0) //unlike town portal non-zero MP is enough
if(parameters.caster->getHeroCaster()->movementPointsRemaining() <= 0) //unlike town portal non-zero MP is enough
{
env->complain("Hero needs movement points to cast Dimension Door!");
return ESpellCastResult::ERROR;
@ -335,8 +335,8 @@ ESpellCastResult DimensionDoorMechanics::applyAdventureEffects(SpellCastEnvironm
{
SetMovePoints smp;
smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId());
if(movementCost < static_cast<int>(parameters.caster->getHeroCaster()->movement))
smp.val = parameters.caster->getHeroCaster()->movement - movementCost;
if(movementCost < static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()))
smp.val = parameters.caster->getHeroCaster()->movementPointsRemaining() - movementCost;
else
smp.val = 0;
env->apply(&smp);
@ -369,7 +369,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
if(nullptr == destination)
return ESpellCastResult::ERROR;
if(static_cast<int>(parameters.caster->getHeroCaster()->movement) < moveCost)
if(static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost)
return ESpellCastResult::ERROR;
if(destination->visitingHero)
@ -419,7 +419,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
return ESpellCastResult::ERROR;
}
if(static_cast<int>(parameters.caster->getHeroCaster()->movement) < moveCost)
if(static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost)
{
env->complain("This hero has not enough movement points!");
return ESpellCastResult::ERROR;
@ -441,7 +441,7 @@ ESpellCastResult TownPortalMechanics::applyAdventureEffects(SpellCastEnvironment
{
SetMovePoints smp;
smp.hid = ObjectInstanceID(parameters.caster->getCasterUnitId());
smp.val = std::max<ui32>(0, parameters.caster->getHeroCaster()->movement - moveCost);
smp.val = std::max<ui32>(0, parameters.caster->getHeroCaster()->movementPointsRemaining() - moveCost);
env->apply(&smp);
}
return ESpellCastResult::OK;
@ -468,7 +468,7 @@ ESpellCastResult TownPortalMechanics::beginCast(SpellCastEnvironment * env, cons
const int moveCost = movementCost(parameters);
if(static_cast<int>(parameters.caster->getHeroCaster()->movement) < moveCost)
if(static_cast<int>(parameters.caster->getHeroCaster()->movementPointsRemaining()) < moveCost)
{
InfoWindow iw;
iw.player = parameters.caster->getCasterOwner();

View File

@ -1773,9 +1773,9 @@ void CGameHandler::newTurn()
if (hero->isInitialized() && hero->stacks.size())
{
// reset retreated or surrendered heroes
auto maxmove = hero->maxMovePoints(true);
auto maxmove = hero->movementPointsLimit(true);
// if movement is greater than maxmove, we should decrease it
if (hero->movement != maxmove || hero->mana < hero->manaLimit())
if (hero->movementPointsRemaining() != maxmove || hero->mana < hero->manaLimit())
{
NewTurn::Hero hth;
hth.id = hero->id;
@ -1864,7 +1864,7 @@ void CGameHandler::newTurn()
hth.id = h->id;
auto ti = std::make_unique<TurnInfo>(h, 1);
// TODO: this code executed when bonuses of previous day not yet updated (this happen in NewTurn::applyGs). See issue 2356
hth.move = h->maxMovePointsCached(gs->map->getTile(h->visitablePos()).terType->isLand(), ti.get());
hth.move = h->movementPointsLimitCached(gs->map->getTile(h->visitablePos()).terType->isLand(), ti.get());
hth.mana = h->getManaNewTurn();
n.heroes.insert(hth);
@ -2280,7 +2280,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
tmh.start = h->pos;
tmh.end = dst;
tmh.result = TryMoveHero::FAILED;
tmh.movePoints = h->movement;
tmh.movePoints = h->movementPointsRemaining();
//check if destination tile is available
auto pathfinderHelper = std::make_unique<CPathfinderHelper>(gs, h, PathfinderOptions());
@ -2288,13 +2288,13 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
const bool canFly = pathfinderHelper->hasBonusOfType(BonusType::FLYING_MOVEMENT) || (h->boat && h->boat->layer == EPathfindingLayer::AIR);
const bool canWalkOnSea = pathfinderHelper->hasBonusOfType(BonusType::WATER_WALKING) || (h->boat && h->boat->layer == EPathfindingLayer::WATER);
const int cost = pathfinderHelper->getMovementCost(h->visitablePos(), hmpos, nullptr, nullptr, h->movement);
const int cost = pathfinderHelper->getMovementCost(h->visitablePos(), hmpos, nullptr, nullptr, h->movementPointsRemaining());
//it's a rock or blocked and not visitable tile
//OR hero is on land and dest is water and (there is not present only one object - boat)
if (((!t.terType->isPassable() || (t.blocked && !t.visitable && !canFly))
&& complain("Cannot move hero, destination tile is blocked!"))
|| ((!h->boat && !canWalkOnSea && !canFly && t.terType->isWater() && (t.visitableObjects.size() < 1 || (t.visitableObjects.back()->ID != Obj::BOAT && t.visitableObjects.back()->ID != Obj::HERO))) //hero is not on boat/water walking and dst water tile doesn't contain boat/hero (objs visitable from land) -> we test back cause boat may be on top of another object (#276)
|| ((!h->boat && !canWalkOnSea && !canFly && t.terType->isWater() && (t.visitableObjects.size() < 1 || !t.visitableObjects.back()->isCoastVisitable())) //hero is not on boat/water walking and dst water tile doesn't contain boat/hero (objs visitable from land) -> we test back cause boat may be on top of another object (#276)
&& complain("Cannot move hero, destination tile is on water!"))
|| ((h->boat && h->boat->layer == EPathfindingLayer::SAIL && t.terType->isLand() && t.blocked)
&& complain("Cannot disembark hero, tile is blocked!"))
@ -2302,7 +2302,7 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
&& complain("Tiles are not neighboring!"))
|| ((h->inTownGarrison)
&& complain("Can not move garrisoned hero!"))
|| (((int)h->movement < cost && dst != h->pos && !teleporting)
|| (((int)h->movementPointsRemaining() < cost && dst != h->pos && !teleporting)
&& complain("Hero doesn't have any movement points left!"))
|| ((transit && !canFly && !CGTeleport::isTeleport(t.topVisitableObj()))
&& complain("Hero cannot transit over this tile!"))
@ -2369,10 +2369,10 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
{
for (CGObjectInstance *obj : t.visitableObjects)
{
if(h->boat && !obj->blockVisit && !h->boat->onboardVisitAllowed)
if(h->boat && !obj->isBlockedVisitable() && !h->boat->onboardVisitAllowed)
return doMove(TryMoveHero::SUCCESS, this->IGNORE_GUARDS, DONT_VISIT_DEST, REMAINING_ON_TILE);
if (obj != h && obj->blockVisit && !obj->passableFor(h->tempOwner))
if (obj != h && obj->isBlockedVisitable() && !obj->passableFor(h->tempOwner))
{
EVisitDest visitDest = VISIT_DEST;
if(h->boat && !h->boat->onboardVisitAllowed)
@ -2387,14 +2387,14 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
if (!transit && embarking)
{
tmh.movePoints = h->movementPointsAfterEmbark(h->movement, cost, false, ti);
tmh.movePoints = h->movementPointsAfterEmbark(h->movementPointsRemaining(), cost, false, ti);
return doMove(TryMoveHero::EMBARK, IGNORE_GUARDS, DONT_VISIT_DEST, LEAVING_TILE);
// In H3 embark ignore guards
}
if (disembarking)
{
tmh.movePoints = h->movementPointsAfterEmbark(h->movement, cost, true, ti);
tmh.movePoints = h->movementPointsAfterEmbark(h->movementPointsRemaining(), cost, true, ti);
return doMove(TryMoveHero::DISEMBARK, CHECK_FOR_GUARDS, VISIT_DEST, LEAVING_TILE);
}
@ -2420,8 +2420,8 @@ bool CGameHandler::moveHero(ObjectInstanceID hid, int3 dst, ui8 teleporting, boo
//still here? it is standard movement!
{
tmh.movePoints = (int)h->movement >= cost
? h->movement - cost
tmh.movePoints = (int)h->movementPointsRemaining() >= cost
? h->movementPointsRemaining() - cost
: 0;
EGuardLook lookForGuards = CHECK_FOR_GUARDS;