فهرست منبع

Merge pull request #2247 from IvanSavenko/pathfinding_fixes

Fix accumulated issues with pathfinding
Ivan Savenko 2 سال پیش
والد
کامیت
5c3cacd290
49فایلهای تغییر یافته به همراه297 افزوده شده و 232 حذف شده
  1. 2 2
      AI/Nullkiller/AIGateway.cpp
  2. 1 1
      AI/Nullkiller/AIUtility.cpp
  3. 1 1
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  4. 1 1
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  5. 1 1
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  6. 4 4
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  7. 2 2
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  8. 2 2
      AI/Nullkiller/Pathfinding/Actors.cpp
  9. 1 1
      AI/VCAI/AIUtility.cpp
  10. 2 2
      AI/VCAI/Goals/Explore.cpp
  11. 2 2
      AI/VCAI/Pathfinding/AINodeStorage.cpp
  12. 5 0
      AI/VCAI/Pathfinding/AIPathfinderConfig.cpp
  13. 9 9
      AI/VCAI/VCAI.cpp
  14. 33 30
      client/CPlayerInterface.cpp
  15. 1 1
      client/ClientCommandManager.cpp
  16. 6 0
      client/NetPacksClient.cpp
  17. 1 1
      client/PlayerLocalState.cpp
  18. 7 1
      client/adventureMap/AdventureMapInterface.cpp
  19. 2 2
      client/adventureMap/AdventureMapShortcuts.cpp
  20. 1 1
      client/adventureMap/CList.cpp
  21. 14 0
      config/gameConfig.json
  22. 3 0
      config/objects/creatureBanks.json
  23. 1 69
      config/schemas/settings.json
  24. 1 1
      lib/CGameState.cpp
  25. 7 0
      lib/GameSettings.cpp
  26. 9 1
      lib/GameSettings.h
  27. 7 9
      lib/NetPacksLib.cpp
  28. 4 0
      lib/mapObjectConstructors/CBankInstanceConstructor.cpp
  29. 10 2
      lib/mapObjectConstructors/CBankInstanceConstructor.h
  30. 5 0
      lib/mapObjects/CBank.cpp
  31. 3 0
      lib/mapObjects/CBank.h
  32. 22 7
      lib/mapObjects/CGHeroInstance.cpp
  33. 9 4
      lib/mapObjects/CGHeroInstance.h
  34. 10 0
      lib/mapObjects/CGObjectInstance.cpp
  35. 11 3
      lib/mapObjects/CGObjectInstance.h
  36. 0 1
      lib/mapObjects/CQuest.h
  37. 5 0
      lib/mapObjects/MiscObjects.cpp
  38. 1 0
      lib/mapObjects/MiscObjects.h
  39. 1 1
      lib/mapping/CMap.cpp
  40. 1 1
      lib/pathfinder/CGPathNode.h
  41. 1 1
      lib/pathfinder/CPathfinder.cpp
  42. 1 1
      lib/pathfinder/NodeStorage.cpp
  43. 13 14
      lib/pathfinder/PathfinderOptions.cpp
  44. 2 3
      lib/pathfinder/PathfinderUtil.h
  45. 48 26
      lib/pathfinder/PathfindingRules.cpp
  46. 2 2
      lib/pathfinder/TurnInfo.cpp
  47. 2 2
      lib/rewardable/Interface.cpp
  48. 7 7
      lib/spells/AdventureSpellMechanics.cpp
  49. 13 13
      server/CGameHandler.cpp

+ 2 - 2
AI/Nullkiller/AIGateway.cpp

@@ -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
 	}

+ 1 - 1
AI/Nullkiller/AIUtility.cpp

@@ -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;
 	}
 

+ 1 - 1
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -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;
 

+ 1 - 1
AI/Nullkiller/Behaviors/StartupBehavior.cpp

@@ -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)
 			{

+ 1 - 1
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -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;

+ 4 - 4
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -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;

+ 2 - 2
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -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();
 

+ 2 - 2
AI/Nullkiller/Pathfinding/Actors.cpp

@@ -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

+ 1 - 1
AI/VCAI/AIUtility.cpp

@@ -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;
 	}
 

+ 2 - 2
AI/VCAI/Goals/Explore.cpp

@@ -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
 		});
 	}
 

+ 2 - 2
AI/VCAI/Pathfinding/AINodeStorage.cpp

@@ -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;
 		}

+ 5 - 0
AI/VCAI/Pathfinding/AIPathfinderConfig.cpp

@@ -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;

+ 9 - 9
AI/VCAI/VCAI.cpp

@@ -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

+ 33 - 30
client/CPlayerInterface.cpp

@@ -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

+ 1 - 1
client/ClientCommandManager.cpp

@@ -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)

+ 6 - 0
client/NetPacksClient.cpp

@@ -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)

+ 1 - 1
client/PlayerLocalState.cpp

@@ -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)

+ 7 - 1
client/adventureMap/AdventureMapInterface.cpp

@@ -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;
 

+ 2 - 2
client/adventureMap/AdventureMapShortcuts.cpp

@@ -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()

+ 1 - 1
client/adventureMap/CList.cpp

@@ -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();
 }

+ 14 - 0
config/gameConfig.json

@@ -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" : 

+ 3 - 0
config/objects/creatureBanks.json

@@ -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" : {

+ 1 - 69
config/schemas/settings.json

@@ -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,

+ 1 - 1
lib/CGameState.cpp

@@ -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);

+ 7 - 0
lib/GameSettings.cpp

@@ -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"    },
 	};

+ 9 - 1
lib/GameSettings.h

@@ -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;
 

+ 7 - 9
lib/NetPacksLib.cpp

@@ -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;
 	}
 

+ 4 - 0
lib/mapObjectConstructors/CBankInstanceConstructor.cpp

@@ -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)

+ 10 - 2
lib/mapObjectConstructors/CBankInstanceConstructor.h

@@ -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);
 	}
 };

+ 5 - 0
lib/mapObjects/CBank.cpp

@@ -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

+ 3 - 0
lib/mapObjects/CBank.h

@@ -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;

+ 22 - 7
lib/mapObjects/CGHeroInstance.cpp

@@ -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;

+ 9 - 4
lib/mapObjects/CGHeroInstance.h

@@ -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

+ 10 - 0
lib/mapObjects/CGObjectInstance.cpp

@@ -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;

+ 11 - 3
lib/mapObjects/CGObjectInstance.h

@@ -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;
 

+ 0 - 1
lib/mapObjects/CQuest.h

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

+ 5 - 0
lib/mapObjects/MiscObjects.cpp

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

+ 1 - 0
lib/mapObjects/MiscObjects.h

@@ -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)
 	{

+ 1 - 1
lib/mapping/CMap.cpp

@@ -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;

+ 1 - 1
lib/pathfinder/CGPathNode.h

@@ -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;

+ 1 - 1
lib/pathfinder/CPathfinder.cpp

@@ -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())

+ 1 - 1
lib/pathfinder/NodeStorage.cpp

@@ -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())

+ 13 - 14
lib/pathfinder/PathfinderOptions.cpp

@@ -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):

+ 2 - 3
lib/pathfinder/PathfinderUtil.h

@@ -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;
 		}

+ 48 - 26
lib/pathfinder/PathfindingRules.cpp

@@ -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);
-
-	if(remains < 0)
+	const float currentCost = destination.cost;
+	const int currentTurnsUsed = destination.turn;
+	const int currentMovePointsLeft = destination.movementLeft;
+	const int sourceLayerMaxMovePoints = pathfinderHelper->getMaxMovePoints(source.node->layer);
+
+	int moveCostPoints = pathfinderHelper->getMovementCost(source, destination, currentMovePointsLeft);
+	float destinationCost = currentCost;
+	int destTurnsUsed = currentTurnsUsed;
+	int destMovePointsLeft = currentMovePointsLeft;
+
+	if(currentMovePointsLeft < moveCostPoints)
 	{
-		//occurs rarely, when hero with low movepoints tries to leave the road
-		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)

+ 2 - 2
lib/pathfinder/TurnInfo.cpp

@@ -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;
 }

+ 2 - 2
lib/rewardable/Interface.cpp

@@ -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);

+ 7 - 7
lib/spells/AdventureSpellMechanics.cpp

@@ -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();

+ 13 - 13
server/CGameHandler.cpp

@@ -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;