Pārlūkot izejas kodu

Merge pull request #2884 from vcmi/nkai-air-water-walking

NKAI: water and air walking
Andrii Danylchenko 2 gadi atpakaļ
vecāks
revīzija
20a944c551
27 mainītis faili ar 570 papildinājumiem un 49 dzēšanām
  1. 7 1
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  2. 35 0
      AI/Nullkiller/Analyzers/HeroManager.cpp
  3. 2 0
      AI/Nullkiller/Analyzers/HeroManager.h
  4. 13 7
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  5. 70 0
      AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp
  6. 39 0
      AI/Nullkiller/Behaviors/StayAtTownBehavior.h
  7. 6 0
      AI/Nullkiller/CMakeLists.txt
  8. 3 1
      AI/Nullkiller/Engine/Nullkiller.cpp
  9. 26 0
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  10. 1 0
      AI/Nullkiller/Engine/PriorityEvaluator.h
  11. 3 1
      AI/Nullkiller/Goals/AbstractGoal.h
  12. 52 0
      AI/Nullkiller/Goals/StayAtTown.cpp
  13. 36 0
      AI/Nullkiller/Goals/StayAtTown.h
  14. 3 1
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  15. 2 1
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  16. 1 0
      AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
  17. 82 0
      AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp
  18. 58 0
      AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h
  19. 2 2
      AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp
  20. 1 3
      AI/Nullkiller/Pathfinding/Actions/BoatActions.h
  21. 6 0
      AI/Nullkiller/Pathfinding/Actions/SpecialAction.h
  22. 94 30
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp
  23. 7 2
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h
  24. 13 0
      lib/pathfinder/CPathfinder.cpp
  25. 2 0
      lib/pathfinder/CPathfinder.h
  26. 1 0
      lib/pathfinder/PathfinderOptions.cpp
  27. 5 0
      lib/pathfinder/PathfinderOptions.h

+ 7 - 1
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -72,7 +72,13 @@ void DangerHitMapAnalyzer::updateHitMap()
 		if(ai->cb->getPlayerRelations(ai->playerID, pair.first) != PlayerRelations::ENEMIES)
 			continue;
 
-		ai->pathfinder->updatePaths(pair.second, PathfinderSettings());
+		PathfinderSettings ps;
+
+		ps.mainTurnDistanceLimit = 10;
+		ps.scoutTurnDistanceLimit = 10;
+		ps.useHeroChain = false;
+
+		ai->pathfinder->updatePaths(pair.second, ps);
 
 		boost::this_thread::interruption_point();
 

+ 35 - 0
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -190,6 +190,41 @@ bool HeroManager::heroCapReached() const
 		|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
 }
 
+float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
+{
+	auto hasFly = hero->spellbookContainsSpell(SpellID::FLY);
+	auto hasTownPortal = hero->spellbookContainsSpell(SpellID::TOWN_PORTAL);
+	auto manaLimit = hero->manaLimit();
+	auto spellPower = hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER);
+	auto hasEarth = hero->getSpellSchoolLevel(SpellID(SpellID::TOWN_PORTAL).toSpell()) > 0;
+
+	auto score = 0.0f;
+
+	for(auto spellId : hero->getSpellsInSpellbook())
+	{
+		auto spell = spellId.toSpell();
+		auto schoolLevel = hero->getSpellSchoolLevel(spell);
+
+		score += (spell->getLevel() + 1) * (schoolLevel + 1) * 0.05f;
+	}
+
+	vstd::amin(score, 1);
+
+	score *= std::min(1.0f, spellPower / 10.0f);
+
+	if(hasFly)
+		score += 0.3f;
+
+	if(hasTownPortal && hasEarth)
+		score += 0.6f;
+
+	vstd::amin(score, 1);
+
+	score *= std::min(1.0f, manaLimit / 100.0f);
+
+	return std::min(score, 1.0f);
+}
+
 bool HeroManager::canRecruitHero(const CGTownInstance * town) const
 {
 	if(!town)

+ 2 - 0
AI/Nullkiller/Analyzers/HeroManager.h

@@ -34,6 +34,7 @@ public:
 	virtual bool heroCapReached() const = 0;
 	virtual const CGHeroInstance * findHeroWithGrail() const = 0;
 	virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0;
+	virtual float getMagicStrength(const CGHeroInstance * hero) const = 0;
 };
 
 class DLL_EXPORT ISecondarySkillRule
@@ -76,6 +77,7 @@ public:
 	bool heroCapReached() const override;
 	const CGHeroInstance * findHeroWithGrail() const override;
 	const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override;
+	float getMagicStrength(const CGHeroInstance * hero) const override;
 
 private:
 	float evaluateFightingStrength(const CGHeroInstance * hero) const;

+ 13 - 7
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -94,16 +94,22 @@ const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) cons
 {
 	for(auto node = path.nodes.rbegin(); node != path.nodes.rend(); node++)
 	{
-		auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord);
-		auto blockers = ai->cb->getVisitableObjs(node->coord);
-		
-		if(guardPos.valid())
+		std::vector<const CGObjectInstance *> blockers = {};
+
+		if(node->layer == EPathfindingLayer::LAND || node->layer == EPathfindingLayer::SAIL)
 		{
-			auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node->coord));
+			auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord);
+			
+			blockers = ai->cb->getVisitableObjs(node->coord);
 
-			if(guard)
+			if(guardPos.valid())
 			{
-				blockers.insert(blockers.begin(), guard);
+				auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node->coord));
+
+				if(guard)
+				{
+					blockers.insert(blockers.begin(), guard);
+				}
 			}
 		}
 

+ 70 - 0
AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp

@@ -0,0 +1,70 @@
+/*
+* StartupBehavior.cpp, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#include "StdInc.h"
+#include "StayAtTownBehavior.h"
+#include "../AIGateway.h"
+#include "../AIUtility.h"
+#include "../Goals/StayAtTown.h"
+#include "../Goals/Composition.h"
+#include "../Goals/ExecuteHeroChain.h"
+#include "lib/mapObjects/MapObjects.h" //for victory conditions
+#include "../Engine/Nullkiller.h"
+
+namespace NKAI
+{
+
+using namespace Goals;
+
+std::string StayAtTownBehavior::toString() const
+{
+	return "StayAtTownBehavior";
+}
+
+Goals::TGoalVec StayAtTownBehavior::decompose() const
+{
+	Goals::TGoalVec tasks;
+	auto towns = cb->getTownsInfo();
+
+	if(!towns.size())
+		return tasks;
+
+	for(auto town : towns)
+	{
+		if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
+			continue;
+
+		auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos());
+
+		for(auto & path : paths)
+		{
+			if(town->visitingHero && town->visitingHero.get() != path.targetHero)
+				continue;
+
+			if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
+			{
+				if(path.targetHero->mana == path.targetHero->manaLimit())
+					continue;
+
+				Composition stayAtTown;
+
+				stayAtTown.addNextSequence({
+						sptr(ExecuteHeroChain(path)),
+						sptr(StayAtTown(town, path))
+					});
+
+				tasks.push_back(sptr(stayAtTown));
+			}
+		}
+	}
+
+	return tasks;
+}
+
+}

+ 39 - 0
AI/Nullkiller/Behaviors/StayAtTownBehavior.h

@@ -0,0 +1,39 @@
+/*
+* StayAtTownBehavior.h, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#pragma once
+
+#include "lib/VCMI_Lib.h"
+#include "../Goals/CGoal.h"
+#include "../AIUtility.h"
+
+namespace NKAI
+{
+namespace Goals
+{
+	class StayAtTownBehavior : public CGoal<StayAtTownBehavior>
+	{
+	public:
+		StayAtTownBehavior()
+			:CGoal(STAY_AT_TOWN_BEHAVIOR)
+		{
+		}
+
+		virtual TGoalVec decompose() const override;
+		virtual std::string toString() const override;
+
+		virtual bool operator==(const StayAtTownBehavior & other) const override
+		{
+			return true;
+		}
+	};
+}
+
+
+}

+ 6 - 0
AI/Nullkiller/CMakeLists.txt

@@ -9,6 +9,7 @@ set(Nullkiller_SRCS
 		Pathfinding/Actions/BuyArmyAction.cpp
 		Pathfinding/Actions/BoatActions.cpp
 		Pathfinding/Actions/TownPortalAction.cpp
+		Pathfinding/Actions/AdventureSpellCastMovementActions.cpp
 		Pathfinding/Rules/AILayerTransitionRule.cpp
 		Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
 		Pathfinding/Rules/AIMovementToDestinationRule.cpp
@@ -34,6 +35,7 @@ set(Nullkiller_SRCS
 		Goals/ExecuteHeroChain.cpp
 		Goals/ExchangeSwapTownHeroes.cpp
 		Goals/CompleteQuest.cpp
+		Goals/StayAtTown.cpp
 		Markers/ArmyUpgrade.cpp
 		Markers/HeroExchange.cpp
 		Markers/UnlockCluster.cpp
@@ -52,6 +54,7 @@ set(Nullkiller_SRCS
 		Behaviors/BuildingBehavior.cpp
 		Behaviors/GatherArmyBehavior.cpp
 		Behaviors/ClusterBehavior.cpp
+		Behaviors/StayAtTownBehavior.cpp
 		Helpers/ArmyFormation.cpp
 		AIGateway.cpp
 )
@@ -69,6 +72,7 @@ set(Nullkiller_HEADERS
 		Pathfinding/Actions/BuyArmyAction.h
 		Pathfinding/Actions/BoatActions.h
 		Pathfinding/Actions/TownPortalAction.h
+		Pathfinding/Actions/AdventureSpellCastMovementActions.h
 		Pathfinding/Rules/AILayerTransitionRule.h
 		Pathfinding/Rules/AIMovementAfterDestinationRule.h
 		Pathfinding/Rules/AIMovementToDestinationRule.h
@@ -97,6 +101,7 @@ set(Nullkiller_HEADERS
 		Goals/ExchangeSwapTownHeroes.h
 		Goals/CompleteQuest.h
 		Goals/Goals.h
+		Goals/StayAtTown.h
 		Markers/ArmyUpgrade.h
 		Markers/HeroExchange.h
 		Markers/UnlockCluster.h
@@ -115,6 +120,7 @@ set(Nullkiller_HEADERS
 		Behaviors/BuildingBehavior.h
 		Behaviors/GatherArmyBehavior.h
 		Behaviors/ClusterBehavior.h
+		Behaviors/StayAtTownBehavior.h
 		Helpers/ArmyFormation.h
 		AIGateway.h
 )

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

@@ -18,6 +18,7 @@
 #include "../Behaviors/BuildingBehavior.h"
 #include "../Behaviors/GatherArmyBehavior.h"
 #include "../Behaviors/ClusterBehavior.h"
+#include "../Behaviors/StayAtTownBehavior.h"
 #include "../Goals/Invalid.h"
 #include "../Goals/Composition.h"
 
@@ -262,7 +263,8 @@ void Nullkiller::makeTurn()
 			choseBestTask(sptr(CaptureObjectsBehavior()), 1),
 			choseBestTask(sptr(ClusterBehavior()), MAX_DEPTH),
 			choseBestTask(sptr(DefenceBehavior()), MAX_DEPTH),
-			choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH)
+			choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH),
+			choseBestTask(sptr(StayAtTownBehavior()), MAX_DEPTH)
 		};
 
 		if(cb->getDate(Date::DAY) == 1)

+ 26 - 0
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -22,6 +22,7 @@
 #include "../../../lib/filesystem/Filesystem.h"
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/BuildThis.h"
+#include "../Goals/StayAtTown.h"
 #include "../Goals/ExchangeSwapTownHeroes.h"
 #include "../Goals/DismissHero.h"
 #include "../Markers/UnlockCluster.h"
@@ -309,6 +310,9 @@ uint64_t RewardEvaluator::getArmyReward(
 			: 0;
 	case Obj::PANDORAS_BOX:
 		return 5000;
+	case Obj::MAGIC_WELL:
+	case Obj::MAGIC_SPRING:
+		return getManaRecoveryArmyReward(hero);
 	default:
 		return 0;
 	}
@@ -450,6 +454,11 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
 	return result;
 }
 
+uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
+{
+	return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit()));
+}
+
 float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const
 {
 	if(!target)
@@ -693,6 +702,22 @@ public:
 	}
 };
 
+class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
+{
+public:
+	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	{
+		if(task->goalType != Goals::STAY_AT_TOWN)
+			return;
+
+		Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task);
+
+		evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero().get());
+		evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
+		evaluationContext.movementCost += stayAtTown.getMovementWasted();
+	}
+};
+
 void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uint8_t turn, uint64_t ourStrength)
 {
 	HitMapInfo enemyDanger = evaluationContext.evaluator.getEnemyHeroDanger(tile, turn);
@@ -998,6 +1023,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai)
 	evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
 	evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai));
+	evaluationContextBuilders.push_back(std::make_shared<StayAtTownManaRecoveryEvaluator>());
 }
 
 EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const

+ 1 - 0
AI/Nullkiller/Engine/PriorityEvaluator.h

@@ -49,6 +49,7 @@ public:
 	uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
 	const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
 	uint64_t townArmyGrowth(const CGTownInstance * town) const;
+	uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
 };
 
 struct DLL_EXPORT EvaluationContext

+ 3 - 1
AI/Nullkiller/Goals/AbstractGoal.h

@@ -71,7 +71,9 @@ namespace Goals
 		ARMY_UPGRADE,
 		DEFEND_TOWN,
 		CAPTURE_OBJECT,
-		SAVE_RESOURCES
+		SAVE_RESOURCES,
+		STAY_AT_TOWN_BEHAVIOR,
+		STAY_AT_TOWN
 	};
 
 	class DLL_EXPORT TSubgoal : public std::shared_ptr<AbstractGoal>

+ 52 - 0
AI/Nullkiller/Goals/StayAtTown.cpp

@@ -0,0 +1,52 @@
+/*
+* ArmyUpgrade.cpp, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#include "StdInc.h"
+#include "StayAtTown.h"
+#include "../AIGateway.h"
+#include "../Engine/Nullkiller.h"
+#include "../AIUtility.h"
+
+namespace NKAI
+{
+
+using namespace Goals;
+
+StayAtTown::StayAtTown(const CGTownInstance * town, AIPath & path)
+	: ElementarGoal(Goals::STAY_AT_TOWN)
+{
+	sethero(path.targetHero);
+	settown(town);
+	movementWasted = static_cast<float>(hero->movementPointsRemaining()) / hero->movementPointsLimit(!hero->boat) - path.movementCost();
+	vstd::amax(movementWasted, 0);
+}
+
+bool StayAtTown::operator==(const StayAtTown & other) const
+{
+	return hero == other.hero && town == other.town;
+}
+
+std::string StayAtTown::toString() const
+{
+	return "Stay at town " + town->getNameTranslated()
+		+ " hero " + hero->getNameTranslated()
+		+ ", mana: " + std::to_string(hero->mana);
+}
+
+void StayAtTown::accept(AIGateway * ai)
+{
+	if(hero->visitedTown != town)
+	{
+		logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated());
+	}
+
+	ai->nullkiller->lockHero(hero.get(), HeroLockedReason::DEFENCE);
+}
+
+}

+ 36 - 0
AI/Nullkiller/Goals/StayAtTown.h

@@ -0,0 +1,36 @@
+/*
+* ArmyUpgrade.h, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#pragma once
+
+#include "../Goals/CGoal.h"
+#include "../Pathfinding/AINodeStorage.h"
+#include "../Analyzers/ArmyManager.h"
+#include "../Analyzers/DangerHitMapAnalyzer.h"
+
+namespace NKAI
+{
+namespace Goals
+{
+	class DLL_EXPORT StayAtTown : public ElementarGoal<StayAtTown>
+	{
+	private:
+		float movementWasted;
+
+	public:
+		StayAtTown(const CGTownInstance * town, AIPath & path);
+
+		virtual bool operator==(const StayAtTown & other) const override;
+		virtual std::string toString() const override;
+		void accept(AIGateway * ai) override;
+		float getMovementWasted() const { return movementWasted; }
+	};
+}
+
+}

+ 3 - 1
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -279,9 +279,10 @@ void AINodeStorage::commit(
 
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
 	logAi->trace(
-		"Commited %s -> %s, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld",
+		"Commited %s -> %s, layer: %d, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld",
 		source->coord.toString(),
 		destination->coord.toString(),
+		destination->layer,
 		destination->getCost(),
 		std::to_string(destination->turns),
 		destination->moveRemains,
@@ -1343,6 +1344,7 @@ void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int pa
 			pathNode.coord = node->coord;
 			pathNode.parentIndex = parentIndex;
 			pathNode.actionIsBlocked = false;
+			pathNode.layer = node->layer;
 
 			if(pathNode.specialAction)
 			{

+ 2 - 1
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -45,7 +45,7 @@ struct AIPathNode : public CGPathNode
 {
 	uint64_t danger;
 	uint64_t armyLoss;
-	uint32_t manaCost;
+	int32_t manaCost;
 	const AIPathNode * chainOther;
 	std::shared_ptr<const SpecialAction> specialAction;
 	const ChainActor * actor;
@@ -65,6 +65,7 @@ struct AIPathNodeInfo
 	float cost;
 	uint8_t turns;
 	int3 coord;
+	EPathfindingLayer layer;
 	uint64_t danger;
 	const CGHeroInstance * targetHero;
 	int parentIndex;

+ 1 - 0
AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp

@@ -44,6 +44,7 @@ namespace AIPathfinding
 		std::shared_ptr<AINodeStorage> nodeStorage)
 		:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), aiNodeStorage(nodeStorage)
 	{
+		options.canUseCast = true;
 	}
 
 	AIPathfinderConfig::~AIPathfinderConfig() = default;

+ 82 - 0
AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.cpp

@@ -0,0 +1,82 @@
+/*
+* AdventureSpellCastMovementActions.cpp, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+
+#include "StdInc.h"
+#include "../../AIGateway.h"
+#include "../../Goals/AdventureSpellCast.h"
+#include "../../Goals/CaptureObject.h"
+#include "../../Goals/Invalid.h"
+#include "../../Goals/BuildBoat.h"
+#include "../../../../lib/mapObjects/MapObjects.h"
+#include "AdventureSpellCastMovementActions.h"
+
+namespace NKAI
+{
+
+namespace AIPathfinding
+{
+	AdventureCastAction::AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero)
+		:spellToCast(spellToCast), hero(hero)
+	{
+		manaCost = hero->getSpellCost(spellToCast.toSpell());
+	}
+
+	WaterWalkingAction::WaterWalkingAction(const CGHeroInstance * hero)
+		:AdventureCastAction(SpellID::WATER_WALK, hero)
+	{ }
+
+	AirWalkingAction::AirWalkingAction(const CGHeroInstance * hero)
+		: AdventureCastAction(SpellID::FLY, hero)
+	{
+	}
+
+	void AdventureCastAction::applyOnDestination(
+		const CGHeroInstance * hero,
+		CDestinationNodeInfo & destination,
+		const PathNodeInfo & source,
+		AIPathNode * dstMode,
+		const AIPathNode * srcNode) const
+	{
+		dstMode->manaCost = srcNode->manaCost + manaCost;
+		dstMode->theNodeBefore = source.node;
+	}
+
+	void AdventureCastAction::execute(const CGHeroInstance * hero) const
+	{
+		assert(hero == this->hero);
+
+		Goals::AdventureSpellCast(hero, spellToCast).accept(ai);
+	}
+
+	bool AdventureCastAction::canAct(const AIPathNode * source) const
+	{
+		assert(hero == this->hero);
+
+		auto hero = source->actor->hero;
+
+#ifdef VCMI_TRACE_PATHFINDER
+		logAi->trace(
+			"Hero %s has %d mana and needed %d and already spent %d",
+			hero->name,
+			hero->mana,
+			getManaCost(hero),
+			source->manaCost);
+#endif
+
+		return hero->mana >= source->manaCost + manaCost;
+	}
+
+	std::string AdventureCastAction::toString() const
+	{
+		return "Cast " + spellToCast.toSpell()->getNameTranslated() + " by " + hero->getNameTranslated();
+	}
+}
+
+}

+ 58 - 0
AI/Nullkiller/Pathfinding/Actions/AdventureSpellCastMovementActions.h

@@ -0,0 +1,58 @@
+/*
+* AdventureSpellCastMovementActions.h, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+
+#pragma once
+
+#include "SpecialAction.h"
+#include "../../../../lib/mapObjects/MapObjects.h"
+
+namespace NKAI
+{
+
+namespace AIPathfinding
+{
+	class AdventureCastAction : public SpecialAction
+	{
+	private:
+		SpellID spellToCast;
+		const CGHeroInstance * hero;
+		int manaCost;
+
+	public:
+		AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero);
+
+		virtual void execute(const CGHeroInstance * hero) const override;
+
+		virtual void applyOnDestination(
+			const CGHeroInstance * hero,
+			CDestinationNodeInfo & destination,
+			const PathNodeInfo & source,
+			AIPathNode * dstMode,
+			const AIPathNode * srcNode) const override;
+
+		virtual bool canAct(const AIPathNode * source) const override;
+
+		virtual std::string toString() const override;
+	};
+
+	class WaterWalkingAction : public AdventureCastAction
+	{
+	public:
+		WaterWalkingAction(const CGHeroInstance * hero);
+	};
+
+	class AirWalkingAction : public AdventureCastAction
+	{
+	public:
+		AirWalkingAction(const CGHeroInstance * hero);
+	};
+}
+
+}

+ 2 - 2
AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp

@@ -114,7 +114,7 @@ namespace AIPathfinding
 			source->manaCost);
 #endif
 
-		return hero->mana >= (si32)(source->manaCost + getManaCost(hero));
+		return hero->mana >= source->manaCost + getManaCost(hero);
 	}
 
 	std::string SummonBoatAction::toString() const
@@ -122,7 +122,7 @@ namespace AIPathfinding
 		return "Summon Boat";
 	}
 
-	uint32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const
+	int32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const
 	{
 		SpellID summonBoat = SpellID::SUMMON_BOAT;
 

+ 1 - 3
AI/Nullkiller/Pathfinding/Actions/BoatActions.h

@@ -20,8 +20,6 @@ namespace AIPathfinding
 {
 	class VirtualBoatAction : public SpecialAction
 	{
-	public:
-		virtual const ChainActor * getActor(const ChainActor * sourceActor) const = 0;
 	};
 	
 	class SummonBoatAction : public VirtualBoatAction
@@ -43,7 +41,7 @@ namespace AIPathfinding
 		virtual std::string toString() const override;
 
 	private:
-		uint32_t getManaCost(const CGHeroInstance * hero) const;
+		int32_t getManaCost(const CGHeroInstance * hero) const;
 	};
 
 	class BuildBoatAction : public VirtualBoatAction

+ 6 - 0
AI/Nullkiller/Pathfinding/Actions/SpecialAction.h

@@ -22,6 +22,7 @@ namespace NKAI
 {
 
 struct AIPathNode;
+class ChainActor;
 
 class SpecialAction
 {
@@ -54,6 +55,11 @@ public:
 	{
 		return {};
 	}
+
+	virtual const ChainActor * getActor(const ChainActor * sourceActor) const
+	{
+		return sourceActor;
+	}
 };
 
 class CompositeAction : public SpecialAction

+ 94 - 30
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp

@@ -10,6 +10,8 @@
 #include "StdInc.h"
 #include "AILayerTransitionRule.h"
 #include "../../Engine/Nullkiller.h"
+#include "../../../../lib/pathfinder/CPathfinder.h"
+#include "../../../../lib/pathfinder/TurnInfo.h"
 
 namespace NKAI
 {
@@ -31,23 +33,79 @@ namespace AIPathfinding
 
 		if(!destination.blocked)
 		{
-			return;
+			if(source.node->layer == EPathfindingLayer::LAND
+				&& (destination.node->layer == EPathfindingLayer::AIR || destination.node->layer == EPathfindingLayer::WATER))
+			{
+				if(pathfinderHelper->getTurnInfo()->isLayerAvailable(destination.node->layer))
+					return;
+				else
+					destination.blocked = true;
+			}
+			else
+			{
+				return;
+			}
 		}
 
 		if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL)
 		{
 			std::shared_ptr<const VirtualBoatAction> virtualBoat = findVirtualBoat(destination, source);
 
-			if(virtualBoat && tryEmbarkVirtualBoat(destination, source, virtualBoat))
+			if(virtualBoat && tryUseSpecialAction(destination, source, virtualBoat, EPathNodeAction::EMBARK))
 			{
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 1
 				logAi->trace("Embarking to virtual boat while moving %s -> %s!", source.coord.toString(), destination.coord.toString());
+#endif
+			}
+		}
+
+		if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::WATER)
+		{
+			auto action = waterWalkingActions.find(nodeStorage->getHero(source.node));
+
+			if(action != waterWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL))
+			{
+#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
+				logAi->trace("Casting water walk while moving %s -> %s!", source.coord.toString(), destination.coord.toString());
+#endif
+			}
+		}
+
+		if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::AIR)
+		{
+			auto action = airWalkingActions.find(nodeStorage->getHero(source.node));
+
+			if(action != airWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL))
+			{
+#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
+				logAi->trace("Casting fly while moving %s -> %s!", source.coord.toString(), destination.coord.toString());
 #endif
 			}
 		}
 	}
 
 	void AILayerTransitionRule::setup()
+	{
+		SpellID waterWalk = SpellID::WATER_WALK;
+		SpellID airWalk = SpellID::FLY;
+
+		for(const CGHeroInstance * hero : nodeStorage->getAllHeroes())
+		{
+			if(hero->canCastThisSpell(waterWalk.toSpell()))
+			{
+				waterWalkingActions[hero] = std::make_shared<WaterWalkingAction>(hero);
+			}
+
+			if(hero->canCastThisSpell(airWalk.toSpell()))
+			{
+				airWalkingActions[hero] = std::make_shared<AirWalkingAction>(hero);
+			}
+		}
+
+		collectVirtualBoats();
+	}
+
+	void AILayerTransitionRule::collectVirtualBoats()
 	{
 		std::vector<const IShipyard *> shipyards;
 
@@ -113,50 +171,56 @@ namespace AIPathfinding
 		return virtualBoat;
 	}
 
-	bool AILayerTransitionRule::tryEmbarkVirtualBoat(
+	bool AILayerTransitionRule::tryUseSpecialAction(
 		CDestinationNodeInfo & destination,
 		const PathNodeInfo & source,
-		std::shared_ptr<const VirtualBoatAction> virtualBoat) const
+		std::shared_ptr<const SpecialAction> specialAction,
+		EPathNodeAction targetAction) const
 	{
 		bool result = false;
 
-		nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
+		if(!specialAction->canAct(nodeStorage->getAINode(source.node)))
 		{
-			auto boatNodeOptional = nodeStorage->getOrCreateNode(
-				node->coord,
-				node->layer,
-				virtualBoat->getActor(node->actor));
+			return false;
+		}
 
-			if(boatNodeOptional)
+		nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
 			{
-				AIPathNode * boatNode = boatNodeOptional.value();
+				auto castNodeOptional = nodeStorage->getOrCreateNode(
+					node->coord,
+					node->layer,
+					specialAction->getActor(node->actor));
 
-				if(boatNode->action == EPathNodeAction::UNKNOWN)
+				if(castNodeOptional)
 				{
-					boatNode->addSpecialAction(virtualBoat);
-					destination.blocked = false;
-					destination.action = EPathNodeAction::EMBARK;
-					destination.node = boatNode;
-					result = true;
+					AIPathNode * castNode = castNodeOptional.value();
+
+					if(castNode->action == EPathNodeAction::UNKNOWN)
+					{
+						castNode->addSpecialAction(specialAction);
+						destination.blocked = false;
+						destination.action = targetAction;
+						destination.node = castNode;
+						result = true;
+					}
+					else
+					{
+#if NKAI_PATHFINDER_TRACE_LEVEL >= 1
+						logAi->trace(
+							"Special transition node already allocated. Blocked moving %s -> %s",
+							source.coord.toString(),
+							destination.coord.toString());
+#endif
+					}
 				}
 				else
 				{
-#if NKAI_PATHFINDER_TRACE_LEVEL >= 1
-					logAi->trace(
-						"Special transition node already allocated. Blocked moving %s -> %s",
+					logAi->debug(
+						"Can not allocate special transition node while moving %s -> %s",
 						source.coord.toString(),
 						destination.coord.toString());
-#endif
 				}
-			}
-			else
-			{
-				logAi->debug(
-					"Can not allocate special transition node while moving %s -> %s",
-					source.coord.toString(),
-					destination.coord.toString());
-			}
-		});
+			});
 
 		return result;
 	}

+ 7 - 2
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h

@@ -13,6 +13,7 @@
 #include "../AINodeStorage.h"
 #include "../../AIGateway.h"
 #include "../Actions/BoatActions.h"
+#include "../Actions/AdventureSpellCastMovementActions.h"
 #include "../../../../CCallback.h"
 #include "../../../../lib/mapObjects/MapObjects.h"
 #include "../../../../lib/pathfinder/PathfindingRules.h"
@@ -29,6 +30,8 @@ namespace AIPathfinding
 		std::map<int3, std::shared_ptr<const BuildBoatAction>> virtualBoats;
 		std::shared_ptr<AINodeStorage> nodeStorage;
 		std::map<const CGHeroInstance *, std::shared_ptr<const SummonBoatAction>> summonableVirtualBoats;
+		std::map<const CGHeroInstance *, std::shared_ptr<const WaterWalkingAction>> waterWalkingActions;
+		std::map<const CGHeroInstance *, std::shared_ptr<const AirWalkingAction>> airWalkingActions;
 
 	public:
 		AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage);
@@ -41,15 +44,17 @@ namespace AIPathfinding
 
 	private:
 		void setup();
+		void collectVirtualBoats();
 
 		std::shared_ptr<const VirtualBoatAction> findVirtualBoat(
 			CDestinationNodeInfo & destination,
 			const PathNodeInfo & source) const;
 
-		bool tryEmbarkVirtualBoat(
+		bool tryUseSpecialAction(
 			CDestinationNodeInfo & destination,
 			const PathNodeInfo & source,
-			std::shared_ptr<const VirtualBoatAction> virtualBoat) const;
+			std::shared_ptr<const SpecialAction> specialAction,
+			EPathNodeAction targetAction) const;
 	};
 }
 

+ 13 - 0
lib/pathfinder/CPathfinder.cpp

@@ -20,6 +20,7 @@
 #include "../TerrainHandler.h"
 #include "../mapObjects/CGHeroInstance.h"
 #include "../mapping/CMap.h"
+#include "spells/CSpellHandler.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -472,6 +473,12 @@ CPathfinderHelper::CPathfinderHelper(CGameState * gs, const CGHeroInstance * Her
 	turnsInfo.reserve(16);
 	updateTurnInfo();
 	initializePatrol();
+
+	SpellID flySpell = SpellID::FLY;
+	canCastFly = Hero->canCastThisSpell(flySpell.toSpell());
+
+	SpellID waterWalk = SpellID::WATER_WALK;
+	canCastWaterWalk = Hero->canCastThisSpell(waterWalk.toSpell());
 }
 
 CPathfinderHelper::~CPathfinderHelper()
@@ -501,12 +508,18 @@ bool CPathfinderHelper::isLayerAvailable(const EPathfindingLayer & layer) const
 		if(!options.useFlying)
 			return false;
 
+		if(canCastFly && options.canUseCast)
+			return true;
+
 		break;
 
 	case EPathfindingLayer::WATER:
 		if(!options.useWaterWalking)
 			return false;
 
+		if(canCastWaterWalk && options.canUseCast)
+			return true;
+
 		break;
 	}
 

+ 2 - 0
lib/pathfinder/CPathfinder.h

@@ -72,6 +72,8 @@ public:
 	const CGHeroInstance * hero;
 	std::vector<TurnInfo *> turnsInfo;
 	const PathfinderOptions & options;
+	bool canCastFly;
+	bool canCastWaterWalk;
 
 	CPathfinderHelper(CGameState * gs, const CGHeroInstance * Hero, const PathfinderOptions & Options);
 	virtual ~CPathfinderHelper();

+ 1 - 0
lib/pathfinder/PathfinderOptions.cpp

@@ -31,6 +31,7 @@ PathfinderOptions::PathfinderOptions()
 	, oneTurnSpecialLayersLimit(true)
 	, originalMovementRules(false)
 	, turnLimit(std::numeric_limits<uint8_t>::max())
+	, canUseCast(false)
 {
 }
 

+ 5 - 0
lib/pathfinder/PathfinderOptions.h

@@ -71,6 +71,11 @@ struct DLL_LINKAGE PathfinderOptions
 	/// Max number of turns to compute. Default = infinite
 	uint8_t turnLimit;
 
+	/// <summary>
+	/// For AI. Allows water walk and fly layers if hero can cast appropriate spells
+	/// </summary>
+	bool canUseCast;
+
 	PathfinderOptions();
 };