Browse Source

AI: CompleteQuest goal and summon boat spell support

Andrii Danylchenko 7 years ago
parent
commit
5d022ba77c

+ 4 - 0
AI/VCAI/CMakeLists.txt

@@ -29,6 +29,7 @@ set(VCAI_SRCS
 		Goals/GatherArmy.cpp
 		Goals/GatherTroops.cpp
 		Goals/BuyArmy.cpp
+		Goals/AdventureSpellCast.cpp
 		Goals/Win.cpp
 		Goals/VisitTile.cpp
 		Goals/VisitObj.cpp
@@ -41,6 +42,7 @@ set(VCAI_SRCS
 		Goals/DigAtTile.cpp
 		Goals/GetArtOfType.cpp
 		Goals/FindObj.cpp
+		Goals/CompleteQuest.cpp
 		main.cpp
 		VCAI.cpp
 )
@@ -71,6 +73,7 @@ set(VCAI_HEADERS
 		Goals/GatherArmy.h
 		Goals/GatherTroops.h
 		Goals/BuyArmy.h
+		Goals/AdventureSpellCast.h
 		Goals/Win.h
 		Goals/VisitTile.h
 		Goals/VisitObj.h
@@ -83,6 +86,7 @@ set(VCAI_HEADERS
 		Goals/DigAtTile.h
 		Goals/GetArtOfType.h
 		Goals/FindObj.h
+		Goals/CompleteQuest.h
 		Goals/Goals.h
 		VCAI.h
 )

+ 13 - 0
AI/VCAI/FuzzyHelper.cpp

@@ -93,6 +93,19 @@ float FuzzyHelper::evaluate(Goals::BuildBoat & g)
 	return g.parent->accept(this) - buildBoatPenalty;
 }
 
+float FuzzyHelper::evaluate(Goals::CompleteQuest & g)
+{
+	// TODO: How to evaluate quest complexity?
+	const float questPenalty = 0.2;
+
+	if(!g.parent)
+	{
+		return 0;
+	}
+
+	return g.parent->accept(this) * questPenalty;
+}
+
 float FuzzyHelper::evaluate(Goals::VisitObj & g)
 {
 	return visitObjEngine.evaluate(g);

+ 1 - 0
AI/VCAI/FuzzyHelper.h

@@ -32,6 +32,7 @@ public:
 	float evaluate(Goals::BuildBoat & g);
 	float evaluate(Goals::GatherArmy & g);
 	float evaluate(Goals::ClearWayTo & g);
+	float evaluate(Goals::CompleteQuest & g);
 	float evaluate(Goals::Invalid & g);
 	float evaluate(Goals::AbstractGoal & g);
 	void setPriority(Goals::TSubgoal & g);

+ 0 - 4
AI/VCAI/Goals/AbstractGoal.cpp

@@ -40,8 +40,6 @@ std::string AbstractGoal::name() const //TODO: virtualize
 		return "INVALID";
 	case WIN:
 		return "WIN";
-	case DO_NOT_LOSE:
-		return "DO NOT LOOSE";
 	case CONQUER:
 		return "CONQUER";
 	case BUILD:
@@ -95,8 +93,6 @@ std::string AbstractGoal::name() const //TODO: virtualize
 	case GET_ART_TYPE:
 		desc = "GET ARTIFACT OF TYPE " + VLC->arth->artifacts[aid]->Name();
 		break;
-	case ISSUE_COMMAND:
-		return "ISSUE COMMAND (unsupported)";
 	case VISIT_TILE:
 		desc = "VISIT TILE " + tile.toString();
 		break;

+ 16 - 16
AI/VCAI/Goals/AbstractGoal.h

@@ -37,21 +37,13 @@ namespace Goals
 	class ClearWayTo;
 	class Invalid;
 	class Trade;
-
-	class DLL_EXPORT TSubgoal : public std::shared_ptr<AbstractGoal>
-	{
-	public:
-		bool operator==(const TSubgoal & rhs) const;
-		bool operator<(const TSubgoal & rhs) const;
-		//TODO: serialize?
-	};
-
-	typedef std::vector<TSubgoal> TGoalVec;
+	class CompleteQuest;
+	class AdventureSpellCast;
 
 	enum EGoals
 	{
 		INVALID = -1,
-		WIN, DO_NOT_LOSE, CONQUER, BUILD, //build needs to get a real reasoning
+		WIN, CONQUER, BUILD, //build needs to get a real reasoning
 		EXPLORE, GATHER_ARMY,
 		BOOST_HERO,
 		RECRUIT_HERO,
@@ -59,24 +51,32 @@ namespace Goals
 		COLLECT_RES,
 		GATHER_TROOPS, // val of creatures with objid
 
-		OBJECT_GOALS_BEGIN,
 		VISIT_OBJ, //visit or defeat or collect the object
 		FIND_OBJ, //find and visit any obj with objid + resid //TODO: consider universal subid for various types (aid, bid)
 		VISIT_HERO, //heroes can move around - set goal abstract and track hero every turn
 
 		GET_ART_TYPE,
 
-		//BUILD_STRUCTURE,
-		ISSUE_COMMAND,
-
 		VISIT_TILE, //tile, in conjunction with hero elementar; assumes tile is reachable
 		CLEAR_WAY_TO,
 		DIG_AT_TILE,//elementar with hero on tile
 		BUY_ARMY, //at specific town
 		TRADE, //val resID at object objid
-		BUILD_BOAT
+		BUILD_BOAT,
+		COMPLETE_QUEST,
+		ADVENTURE_SPELL_CAST
 	};
 
+	class DLL_EXPORT TSubgoal : public std::shared_ptr<AbstractGoal>
+	{
+	public:
+		bool operator==(const TSubgoal & rhs) const;
+		bool operator<(const TSubgoal & rhs) const;
+		//TODO: serialize?
+	};
+
+	typedef std::vector<TSubgoal> TGoalVec;
+
 	//method chaining + clone pattern
 #define VSETTER(type, field) virtual AbstractGoal & set ## field(const type &rhs) {field = rhs; return *this;};
 #define OSETTER(type, field) CGoal<T> & set ## field(const type &rhs) override { field = rhs; return *this; };

+ 63 - 0
AI/VCAI/Goals/AdventureSpellCast.cpp

@@ -0,0 +1,63 @@
+/*
+* AdventureSpellCast.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 "AdventureSpellCast.h"
+#include "../VCAI.h"
+#include "../FuzzyHelper.h"
+#include "../AIhelper.h"
+#include "../../lib/mapping/CMap.h" //for victory conditions
+#include "../../lib/CPathfinder.h"
+
+extern boost::thread_specific_ptr<CCallback> cb;
+extern boost::thread_specific_ptr<VCAI> ai;
+extern FuzzyHelper * fh;
+
+using namespace Goals;
+
+bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const
+{
+	return hero.h == other.hero.h;
+}
+
+TSubgoal AdventureSpellCast::whatToDoToAchieve()
+{
+	if(!hero.validAndSet())
+		throw cannotFulfillGoalException("Invalid hero!");
+
+	auto spell = spellID.toSpell();
+
+	logAi->trace("Decomposing adventure spell cast of %s for hero %s", spell->name, hero->name);
+
+	if(!spell->isAdventureSpell())
+		throw cannotFulfillGoalException(spell->name + " is not an adventure spell.");
+
+	if(!vstd::contains(hero->spells, spellID))
+		throw cannotFulfillGoalException("Hero has no " + spell->name);
+
+	if(hero->mana < hero->getSpellCost(spellID.toSpell()))
+		throw cannotFulfillGoalException("Hero has not enough mana to cast " + spell->name);
+
+	return iAmElementar();
+}
+
+void AdventureSpellCast::accept(VCAI * ai)
+{
+	cb->castSpell(hero.h, spellID, tile);
+}
+
+std::string AdventureSpellCast::name() const
+{
+	return "AdventureSpellCast " + spellID.toSpell()->name;
+}
+
+std::string AdventureSpellCast::completeMessage() const
+{
+	return "Spell casted successfully  " + spellID.toSpell()->name;
+}

+ 39 - 0
AI/VCAI/Goals/AdventureSpellCast.h

@@ -0,0 +1,39 @@
+/*
+* AdventureSpellCast.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 "CGoal.h"
+
+namespace Goals
+{
+	class DLL_EXPORT AdventureSpellCast : public CGoal<AdventureSpellCast>
+	{
+	private:
+		SpellID spellID;
+
+	public:
+		AdventureSpellCast(HeroPtr hero, SpellID spellID)
+			: CGoal(Goals::ADVENTURE_SPELL_CAST), spellID(spellID)
+		{
+			sethero(hero);
+		}
+
+		TGoalVec getAllPossibleSubgoals() override
+		{
+			return TGoalVec();
+		}
+
+		TSubgoal whatToDoToAchieve() override;
+		void accept(VCAI * ai) override;
+		std::string name() const override;
+		std::string completeMessage() const override;
+		virtual bool operator==(const AdventureSpellCast & other) const override;
+	};
+}

+ 5 - 3
AI/VCAI/Goals/CGoal.h

@@ -60,11 +60,13 @@ namespace Goals
 		{
 			return new T(static_cast<T const &>(*this)); //casting enforces template instantiation
 		}
-		TSubgoal iAmElementar()
+		TSubgoal iAmElementar() const
 		{
-			setisElementar(true); //FIXME: it's not const-correct, maybe we shoudl only set returned clone?
 			TSubgoal ptr;
+
 			ptr.reset(clone());
+			ptr->setisElementar(true);
+
 			return ptr;
 		}
 		template<typename Handler> void serialize(Handler & h, const int version)
@@ -79,7 +81,7 @@ namespace Goals
 			if(goalType != g.goalType)
 				return false;
 
-			return (*this) == (dynamic_cast<const T &>(g));
+			return (*this) == (static_cast<const T &>(g));
 		}
 
 		virtual bool operator==(const T & other) const = 0;

+ 276 - 0
AI/VCAI/Goals/CompleteQuest.cpp

@@ -0,0 +1,276 @@
+/*
+* CompleteQuest.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 "Goals.h"
+#include "../VCAI.h"
+#include "../FuzzyHelper.h"
+#include "../AIhelper.h"
+#include "../../lib/mapping/CMap.h" //for victory conditions
+#include "../../lib/CPathfinder.h"
+
+extern boost::thread_specific_ptr<CCallback> cb;
+extern boost::thread_specific_ptr<VCAI> ai;
+extern FuzzyHelper * fh;
+
+using namespace Goals;
+
+bool CompleteQuest::operator==(const CompleteQuest & other) const
+{
+	return q.quest->qid == other.q.quest->qid;
+}
+
+TGoalVec CompleteQuest::getAllPossibleSubgoals()
+{
+	TGoalVec solutions;
+
+	if(q.quest->missionType && q.quest->progress != CQuest::COMPLETE)
+	{
+		logAi->debug("Trying to realize quest: %s", questToString());
+
+		switch(q.quest->missionType)
+		{
+		case CQuest::MISSION_ART:
+			return missionArt();
+
+		case CQuest::MISSION_HERO:
+			return missionHero();
+
+		case CQuest::MISSION_ARMY:
+			return missionArmy();
+
+		case CQuest::MISSION_RESOURCES:
+			return missionResources();
+
+		case CQuest::MISSION_KILL_HERO:
+		case CQuest::MISSION_KILL_CREATURE:
+			return missionDestroyObj();
+
+		case CQuest::MISSION_PRIMARY_STAT:
+			return missionIncreasePrimaryStat();
+
+		case CQuest::MISSION_LEVEL:
+			return missionLevel();
+
+		case CQuest::MISSION_PLAYER:
+			if(ai->playerID.getNum() != q.quest->m13489val)
+				logAi->debug("Can't be player of color %d", q.quest->m13489val);
+
+			break;
+		
+		case CQuest::MISSION_KEYMASTER:
+			return missionKeymaster();
+
+		} //end of switch
+	}
+
+	return TGoalVec();
+}
+
+TSubgoal CompleteQuest::whatToDoToAchieve()
+{
+	if(q.quest->missionType == CQuest::MISSION_NONE)
+	{
+		throw cannotFulfillGoalException("Can not complete inactive quest");
+	}
+
+	TGoalVec solutions = getAllPossibleSubgoals();
+
+	if(solutions.empty())
+		throw cannotFulfillGoalException("Can not complete quest " + questToString());
+
+	TSubgoal result = fh->chooseSolution(solutions);
+
+	logAi->trace(
+		"Returning %s, tile: %s, objid: %d, hero: %s",
+		result->name(),
+		result->tile.toString(),
+		result->objid,
+		result->hero.validAndSet() ? result->hero->name : "not specified");
+
+	return result;
+}
+
+std::string CompleteQuest::name() const
+{
+	return "CompleteQuest";
+}
+
+std::string CompleteQuest::completeMessage() const
+{
+	return "Completed quest " + questToString();
+}
+
+std::string CompleteQuest::questToString() const
+{
+	if(q.quest->missionType == CQuest::MISSION_NONE)
+		return "inactive quest";
+
+	MetaString ms;
+	q.quest->getRolloverText(ms, false);
+
+	return ms.toString();
+}
+
+TGoalVec CompleteQuest::tryCompleteQuest() const
+{
+	TGoalVec solutions;
+
+	auto heroes = cb->getHeroesInfo(); //TODO: choose best / free hero from among many possibilities?
+
+	for(auto hero : heroes)
+	{
+		if(q.quest->checkQuest(hero))
+		{
+			vstd::concatenate(solutions, ai->ah->howToVisitObj(hero, ObjectIdRef(q.obj->id)));
+		}
+	}
+
+	return solutions;
+}
+
+TGoalVec CompleteQuest::missionArt() const
+{
+	TGoalVec solutions = tryCompleteQuest();
+
+	if(!solutions.empty())
+		return solutions;
+
+	for(auto art : q.quest->m5arts)
+	{
+		solutions.push_back(sptr(GetArtOfType(art))); //TODO: transport?
+	}
+
+	return solutions;
+}
+
+TGoalVec CompleteQuest::missionHero() const
+{
+	TGoalVec solutions = tryCompleteQuest();
+
+	if(solutions.empty())
+	{
+		//rule of a thumb - quest heroes usually are locked in prisons
+		solutions.push_back(sptr(FindObj(Obj::PRISON)));
+	}
+
+	return solutions;
+}
+
+TGoalVec CompleteQuest::missionArmy() const
+{
+	TGoalVec solutions = tryCompleteQuest();
+
+	if(!solutions.empty())
+		return solutions;
+
+	for(auto creature : q.quest->m6creatures)
+	{
+		solutions.push_back(sptr(GatherTroops(creature.type->idNumber, creature.count)));
+	}
+
+	return solutions;
+}
+
+TGoalVec CompleteQuest::missionIncreasePrimaryStat() const
+{
+	TGoalVec solutions = tryCompleteQuest();
+
+	if(solutions.empty())
+	{
+		for(int i = 0; i < q.quest->m2stats.size(); ++i)
+		{
+			// TODO: library, school and other boost objects
+			logAi->debug("Don't know how to increase primary stat %d", i);
+		}
+	}
+
+	return solutions;
+}
+
+TGoalVec CompleteQuest::missionLevel() const
+{
+	TGoalVec solutions = tryCompleteQuest();
+
+	if(solutions.empty())
+	{
+		logAi->debug("Don't know how to reach hero level %d", q.quest->m13489val);
+	}
+
+	return solutions;
+}
+
+TGoalVec CompleteQuest::missionKeymaster() const
+{
+	TGoalVec solutions = tryCompleteQuest();
+
+	if(solutions.empty())
+	{
+		solutions.push_back(sptr(Goals::FindObj(Obj::KEYMASTER, q.obj->subID)));
+	}
+
+	return solutions;
+}
+
+TGoalVec CompleteQuest::missionResources() const
+{
+	TGoalVec solutions;
+
+	auto heroes = cb->getHeroesInfo(); //TODO: choose best / free hero from among many possibilities?
+
+	if(heroes.size())
+	{
+		if(q.quest->checkQuest(heroes.front())) //it doesn't matter which hero it is
+		{
+			return ai->ah->howToVisitObj(q.obj);
+		}
+		else
+		{
+			for(int i = 0; i < q.quest->m7resources.size(); ++i)
+			{
+				if(q.quest->m7resources[i])
+					solutions.push_back(sptr(CollectRes(i, q.quest->m7resources[i])));
+			}
+		}
+	}
+	else
+	{
+		solutions.push_back(sptr(Goals::RecruitHero())); //FIXME: checkQuest requires any hero belonging to player :(
+	}
+
+	return solutions;
+}
+
+TGoalVec CompleteQuest::missionDestroyObj() const
+{
+	TGoalVec solutions;
+
+	auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val);
+
+	if(!obj)
+		return ai->ah->howToVisitObj(q.obj);
+
+	if(obj->ID == Obj::HERO)
+	{
+		auto relations = cb->getPlayerRelations(ai->playerID, obj->tempOwner);
+
+		if(relations == PlayerRelations::SAME_PLAYER)
+		{
+			auto heroToProtect = cb->getHero(obj->id);
+
+			solutions.push_back(sptr(GatherArmy().sethero(heroToProtect)));
+		}
+		else if(relations == PlayerRelations::ENEMIES)
+		{
+			solutions = ai->ah->howToVisitObj(obj);
+		}
+	}
+
+	return solutions;
+}

+ 46 - 0
AI/VCAI/Goals/CompleteQuest.h

@@ -0,0 +1,46 @@
+/*
+* CompleteQuest.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 "CGoal.h"
+#include "../../../lib/VCMI_Lib.h"
+
+namespace Goals
+{
+	class DLL_EXPORT CompleteQuest : public CGoal<CompleteQuest>
+	{
+	private:
+		const QuestInfo q;
+
+	public:
+		CompleteQuest(const QuestInfo quest)
+			: CGoal(Goals::COMPLETE_QUEST), q(quest)
+		{
+		}
+
+		TGoalVec getAllPossibleSubgoals() override;
+		TSubgoal whatToDoToAchieve() override;
+		std::string name() const override;
+		std::string completeMessage() const override;
+		virtual bool operator==(const CompleteQuest & other) const override;
+
+	private:
+		TGoalVec tryCompleteQuest() const;
+		TGoalVec missionArt() const;
+		TGoalVec missionHero() const;
+		TGoalVec missionArmy() const;
+		TGoalVec missionResources() const;
+		TGoalVec missionDestroyObj() const;
+		TGoalVec missionIncreasePrimaryStat() const;
+		TGoalVec missionLevel() const;
+		TGoalVec missionKeymaster() const;
+		std::string questToString() const;
+	};
+}

+ 2 - 0
AI/VCAI/Goals/Conquer.cpp

@@ -33,6 +33,8 @@ bool Conquer::operator==(const Conquer & other) const
 
 TSubgoal Conquer::whatToDoToAchieve()
 {
+	logAi->trace("Entering goal CONQUER");
+
 	return fh->chooseSolution(getAllPossibleSubgoals());
 }
 

+ 2 - 0
AI/VCAI/Goals/GatherTroops.cpp

@@ -48,6 +48,8 @@ int GatherTroops::getCreaturesCount(const CArmedInstance * army)
 
 TSubgoal GatherTroops::whatToDoToAchieve()
 {
+	logAi->trace("Entering GatherTroops::whatToDoToAchieve");
+
 	auto heroes = cb->getHeroesInfo(true);
 
 	for(auto hero : heroes)

+ 3 - 1
AI/VCAI/Goals/Goals.h

@@ -29,4 +29,6 @@
 #include "GetArtOfType.h"
 #include "ClearWayTo.h"
 #include "DigAtTile.h"
-#include "FindObj.h"
+#include "FindObj.h"
+#include "CompleteQuest.h"
+#include "AdventureSpellCast.h"

+ 9 - 1
AI/VCAI/Pathfinding/AINodeStorage.cpp

@@ -82,6 +82,7 @@ void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, CGPat
 
 		heroNode.chainMask = 0;
 		heroNode.danger = 0;
+		heroNode.manaCost = 0;
 		heroNode.specialAction.reset();
 		heroNode.update(coord, layer, accessibility);
 	}
@@ -97,6 +98,12 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf
 		dstNode->danger = srcNode->danger;
 		dstNode->action = destination.action;
 		dstNode->theNodeBefore = srcNode->theNodeBefore;
+		dstNode->manaCost = srcNode->manaCost;
+		
+		if(dstNode->specialAction)
+		{
+			dstNode->specialAction->applyOnDestination(getHero(), destination, source, dstNode, srcNode);
+		}
 	});
 }
 
@@ -186,6 +193,7 @@ std::vector<AIPath> AINodeStorage::getChainInfo(int3 pos, bool isOnLand) const
 {
 	std::vector<AIPath> paths;
 	auto chains = nodes[pos.x][pos.y][pos.z][isOnLand ? EPathfindingLayer::LAND : EPathfindingLayer::SAIL];
+	auto initialPos = hero->visitablePos();
 
 	for(const AIPathNode & node : chains)
 	{
@@ -197,7 +205,7 @@ std::vector<AIPath> AINodeStorage::getChainInfo(int3 pos, bool isOnLand) const
 		AIPath path;
 		const AIPathNode * current = &node;
 
-		while(current != nullptr)
+		while(current != nullptr && current->coord != initialPos)
 		{
 			AIPathNodeInfo pathNode;
 

+ 12 - 0
AI/VCAI/Pathfinding/AINodeStorage.h

@@ -15,16 +15,28 @@
 #include "../AIUtility.h"
 #include "../Goals/AbstractGoal.h"
 
+class AIPathNode;
+
 class ISpecialAction
 {
 public:
 	virtual Goals::TSubgoal whatToDo(HeroPtr hero) const = 0;
+
+	virtual void applyOnDestination(
+		HeroPtr hero,
+		CDestinationNodeInfo & destination, 
+		const PathNodeInfo & source,
+		AIPathNode * dstMode,
+		const AIPathNode * srcNode) const
+	{
+	}
 };
 
 struct AIPathNode : public CGPathNode
 {
 	uint32_t chainMask;
 	uint64_t danger;
+	uint32_t manaCost;
 	std::shared_ptr<const ISpecialAction> specialAction;
 };
 

+ 1 - 1
AI/VCAI/Pathfinding/AIPathfinder.cpp

@@ -48,10 +48,10 @@ std::vector<AIPath> AIPathfinder::getPathInfo(HeroPtr hero, int3 tile)
 		}
 
 		storageMap[hero] = nodeStorage;
+		nodeStorage->setHero(hero.get());
 		
 		auto config = std::make_shared<AIPathfinding::AIPathfinderConfig>(cb, ai, nodeStorage);
 
-		nodeStorage->setHero(hero.get());
 		cb->calculatePaths(config, hero.get());
 	}
 	else

+ 148 - 30
AI/VCAI/Pathfinding/AIPathfinderConfig.cpp

@@ -16,14 +16,31 @@
 
 namespace AIPathfinding
 {
-	class BuildBoatAction : public ISpecialAction
+	class VirtualBoatAction : public ISpecialAction
+	{
+	private:
+		uint64_t specialChain;
+
+	public:
+		VirtualBoatAction(uint64_t specialChain)
+			:specialChain(specialChain)
+		{
+		}
+
+		uint64_t getSpecialChain() const
+		{
+			return specialChain;
+		}
+	};
+
+	class BuildBoatAction : public VirtualBoatAction
 	{
 	private:
 		const IShipyard * shipyard;
 
 	public:
 		BuildBoatAction(const IShipyard * shipyard)
-			:shipyard(shipyard)
+			:VirtualBoatAction(AINodeStorage::RESOURCE_CHAIN), shipyard(shipyard)
 		{
 		}
 
@@ -33,6 +50,51 @@ namespace AIPathfinding
 		}
 	};
 
+	class SummonBoatAction : public VirtualBoatAction
+	{
+	public:
+		SummonBoatAction()
+			:VirtualBoatAction(AINodeStorage::CAST_CHAIN)
+		{
+		}
+
+		virtual Goals::TSubgoal whatToDo(HeroPtr hero) const override
+		{
+			return sptr(Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT));
+		}
+
+		virtual void applyOnDestination(
+			HeroPtr hero,
+			CDestinationNodeInfo & destination,
+			const PathNodeInfo & source,
+			AIPathNode * dstMode,
+			const AIPathNode * srcNode) const override
+		{
+			dstMode->manaCost = srcNode->manaCost + getManaCost(hero);
+			dstMode->theNodeBefore = source.node;
+		}
+
+		bool isAffordableBy(HeroPtr hero, const AIPathNode * source) const
+		{
+			logAi->trace(
+				"Hero %s has %d mana and needed %d and already spent %d", 
+				hero->name, 
+				hero->mana, 
+				getManaCost(hero),
+				source->manaCost);
+
+			return hero->mana >= source->manaCost + getManaCost(hero);
+		}
+
+	private:
+		uint32_t getManaCost(HeroPtr hero) const
+		{
+			SpellID summonBoat = SpellID::SUMMON_BOAT;
+
+			return hero->getSpellCost(summonBoat.toSpell());
+		}
+	};
+
 	class BattleAction : public ISpecialAction
 	{
 	private:
@@ -58,6 +120,7 @@ namespace AIPathfinding
 		VCAI * ai;
 		std::map<int3, std::shared_ptr<const BuildBoatAction>> virtualBoats;
 		std::shared_ptr<AINodeStorage> nodeStorage;
+		std::shared_ptr<const SummonBoatAction> summonableVirtualBoat;
 
 	public:
 		AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, VCAI * ai, std::shared_ptr<AINodeStorage> nodeStorage)
@@ -79,37 +142,14 @@ namespace AIPathfinding
 				return;
 			}
 
-			if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL
-				&& vstd::contains(virtualBoats, destination.coord))
+			if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL)
 			{
-				logAi->trace("Bypassing virtual boat at %s!", destination.coord.toString());
+				std::shared_ptr<const VirtualBoatAction> virtualBoat = findVirtualBoat(destination, source);
 
-				nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
+				if(virtualBoat && tryEmbarkVirtualBoat(destination, source, virtualBoat))
 				{
-					std::shared_ptr<const BuildBoatAction> virtualBoat = virtualBoats.at(destination.coord);
-
-					auto boatNodeOptional = nodeStorage->getOrCreateNode(
-						node->coord,
-						node->layer,
-						node->chainMask | AINodeStorage::RESOURCE_CHAIN);
-
-					if(boatNodeOptional)
-					{
-						AIPathNode * boatNode = boatNodeOptional.get();
-
-						boatNode->specialAction = virtualBoat;
-						destination.blocked = false;
-						destination.action = CGPathNode::ENodeAction::EMBARK;
-						destination.node = boatNode;
-					}
-					else
-					{
-						logAi->trace(
-							"Can not allocate boat node while moving %s -> %s",
-							source.coord.toString(),
-							destination.coord.toString());
-					}
-				});
+					logAi->trace("Embarking to virtual boat while moving %s -> %s!", source.coord.toString(), destination.coord.toString());
+				}
 			}
 		}
 
@@ -142,6 +182,84 @@ namespace AIPathfinding
 					logAi->debug("Virtual boat added at %s", boatLocation.toString());
 				}
 			}
+
+			auto hero = nodeStorage->getHero();
+
+			if(vstd::contains(hero->spells, SpellID::SUMMON_BOAT))
+			{
+				auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell();
+
+				if(hero->getSpellSchoolLevel(summonBoatSpell) == SecSkillLevel::EXPERT)
+				{
+					summonableVirtualBoat.reset(new SummonBoatAction());
+				}
+			}
+		}
+
+		std::shared_ptr<const VirtualBoatAction> findVirtualBoat(
+			CDestinationNodeInfo &destination,
+			const PathNodeInfo &source) const
+		{
+			std::shared_ptr<const VirtualBoatAction> virtualBoat;
+
+			if(vstd::contains(virtualBoats, destination.coord))
+			{
+				virtualBoat = virtualBoats.at(destination.coord);
+			}
+			else if(
+				summonableVirtualBoat
+				&& summonableVirtualBoat->isAffordableBy(nodeStorage->getHero(), nodeStorage->getAINode(source.node)))
+			{
+				virtualBoat = summonableVirtualBoat;
+			}
+
+			return virtualBoat;
+		}
+
+		bool tryEmbarkVirtualBoat(
+			CDestinationNodeInfo &destination, 
+			const PathNodeInfo &source,
+			std::shared_ptr<const VirtualBoatAction> virtualBoat) const
+		{
+			bool result = false;
+
+			nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
+			{
+				auto boatNodeOptional = nodeStorage->getOrCreateNode(
+					node->coord,
+					node->layer,
+					node->chainMask | virtualBoat->getSpecialChain());
+
+				if(boatNodeOptional)
+				{
+					AIPathNode * boatNode = boatNodeOptional.get();
+
+					if(boatNode->action == CGPathNode::NOT_SET)
+					{
+						boatNode->specialAction = virtualBoat;
+						destination.blocked = false;
+						destination.action = CGPathNode::ENodeAction::EMBARK;
+						destination.node = boatNode;
+						result = true;
+					}
+					else
+					{
+						logAi->trace(
+							"Special transition node already allocated. Blocked moving %s -> %s",
+							source.coord.toString(),
+							destination.coord.toString());
+					}
+				}
+				else
+				{
+					logAi->trace(
+						"Can not allocate special transition node while moving %s -> %s",
+						source.coord.toString(),
+						destination.coord.toString());
+				}
+			});
+
+			return result;
 		}
 	};
 

+ 12 - 8
AI/VCAI/Pathfinding/PathfindingManager.cpp

@@ -11,7 +11,7 @@
 #include "PathfindingManager.h"
 #include "AIPathfinder.h"
 #include "AIPathfinderConfig.h"
-#include "Goals/Goals.h"
+#include "../Goals/Goals.h"
 #include "../../../lib/CGameInfoCallback.h"
 #include "../../../lib/mapping/CMap.h"
 
@@ -130,8 +130,6 @@ Goals::TGoalVec PathfindingManager::findPath(
 
 			if(isSafeToVisit(hero, danger))
 			{
-				logAi->trace("It's safe for %s to visit tile %s with danger %s", hero->name, dest.toString(), std::to_string(danger));
-
 				Goals::TSubgoal solution;
 
 				if(path.specialAction)
@@ -153,6 +151,8 @@ Goals::TGoalVec PathfindingManager::findPath(
 				
 				solution->evaluationContext.movementCost += path.movementCost();
 
+				logAi->trace("It's safe for %s to visit tile %s with danger %s, goal %s", hero->name, dest.toString(), std::to_string(danger), solution->name());
+
 				result.push_back(solution);
 
 				continue;
@@ -212,11 +212,15 @@ Goals::TSubgoal PathfindingManager::clearWayTo(HeroPtr hero, int3 firstTileToGet
 				return sptr(Goals::VisitObj(topObj->id.getNum()).sethero(hero));
 			}
 
-			//TODO: we should be able to return apriopriate quest here
-			//ret.push_back(ai->questToGoal());
-			//however, visiting obj for firts time will give us quest
-			//do not access quets guard if we can't complete the quest
-			logAi->trace("Can not visit this quest guard! Not ready!");
+			auto questObj = dynamic_cast<const IQuestObject*>(topObj);
+			
+			if(questObj)
+			{
+				auto questInfo = QuestInfo(questObj->quest, topObj, topObj->visitablePos());
+
+				return sptr(Goals::CompleteQuest(questInfo));
+			}
+
 			return sptr(Goals::Invalid());
 		}
 	}

+ 3 - 165
AI/VCAI/VCAI.cpp

@@ -873,7 +873,7 @@ void VCAI::mainLoop()
 		basicGoals.push_back(ah->whatToDo());
 	for (auto quest : myCb->getMyQuests())
 	{
-		basicGoals.push_back(questToGoal(quest));
+		basicGoals.push_back(sptr(Goals::CompleteQuest(quest)));
 	}
 	basicGoals.push_back(sptr(Goals::Build()));
 
@@ -1013,11 +1013,13 @@ void VCAI::mainLoop()
 			//remove goals we couldn't decompose
 			for (auto goal : goalsToRemove)
 				vstd::erase_if_present(basicGoals, goal);
+			
 			//add abstract goals
 			boost::sort(goalsToAdd, [](const Goals::TSubgoal & lhs, const Goals::TSubgoal & rhs) -> bool
 			{
 				return lhs->priority > rhs->priority; //highest priority at the beginning
 			});
+			
 			//max number of goals = 10
 			int i = 0;
 			while (basicGoals.size() < 10 && goalsToAdd.size() > i)
@@ -2434,170 +2436,6 @@ Goals::TSubgoal VCAI::decomposeGoal(Goals::TSubgoal ultimateGoal)
 	return abstractGoal;
 }
 
-Goals::TSubgoal VCAI::questToGoal(const QuestInfo & q)
-{
-	Goals::TSubgoal result = sptr(Goals::Invalid());
-
-	if (q.quest->missionType && q.quest->progress != CQuest::COMPLETE)
-	{
-		MetaString ms;
-		q.quest->getRolloverText(ms, false);
-		logAi->debug("Trying to realize quest: %s", ms.toString());
-		auto heroes = cb->getHeroesInfo(); //TODO: choose best / free hero from among many possibilities?
-
-		switch (q.quest->missionType)
-		{
-		case CQuest::MISSION_ART:
-		{
-			for (auto hero : heroes)
-			{
-				if (q.quest->checkQuest(hero))
-				{
-					return sptr(Goals::VisitObj(q.obj->id.getNum()).sethero(hero));
-				}
-			}
-			for (auto art : q.quest->m5arts)
-			{
-				return sptr(Goals::GetArtOfType(art)); //TODO: transport?
-			}
-			break;
-		}
-		case CQuest::MISSION_HERO:
-		{
-			//striveToGoal (CGoal(RECRUIT_HERO));
-			for (auto hero : heroes)
-			{
-				if (q.quest->checkQuest(hero))
-				{
-					return sptr(Goals::VisitObj(q.obj->id.getNum()).sethero(hero));
-				}
-			}
-			return sptr(Goals::FindObj(Obj::PRISON)); //rule of a thumb - quest heroes usually are locked in prisons
-															 //BNLOG ("Don't know how to recruit hero with id %d\n", q.quest->m13489val);
-		}
-		case CQuest::MISSION_ARMY:
-		{
-			for (auto hero : heroes)
-			{
-				if (q.quest->checkQuest(hero)) //very bad info - stacks can be split between multiple heroes :(
-				{
-					result = sptr(Goals::VisitObj(q.obj->id.getNum()).sethero(hero));
-					break;
-				}
-			}
-
-			if(result->invalid())
-			{
-				for(auto creature : q.quest->m6creatures)
-				{
-					result = sptr(Goals::GatherTroops(creature.type->idNumber, creature.count));
-					break;
-				}
-			}
-			//TODO: exchange armies... oh my
-			//BNLOG ("Don't know how to recruit %d of %s\n", (int)(creature.count) % creature.type->namePl);
-			break;
-		}
-		case CQuest::MISSION_RESOURCES:
-		{
-			if (heroes.size())
-			{
-				if (q.quest->checkQuest(heroes.front())) //it doesn't matter which hero it is
-				{
-					return sptr(Goals::VisitObj(q.obj->id.getNum()));
-				}
-				else
-				{
-					for (int i = 0; i < q.quest->m7resources.size(); ++i)
-					{
-						if (q.quest->m7resources[i])
-							return sptr(Goals::CollectRes(i, q.quest->m7resources[i]));
-					}
-				}
-			}
-			else
-				return sptr(Goals::RecruitHero()); //FIXME: checkQuest requires any hero belonging to player :(
-			break;
-		}
-		case CQuest::MISSION_KILL_HERO:
-		case CQuest::MISSION_KILL_CREATURE:
-		{
-			auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val);
-
-			if(!obj)
-				return sptr(Goals::VisitObj(q.obj->id.getNum())); //visit seer hut
-
-			if(obj->ID == Obj::HERO)
-			{
-				auto relations = myCb->getPlayerRelations(playerID, obj->tempOwner);
-
-				if(relations == PlayerRelations::SAME_PLAYER)
-				{
-					auto heroToProtect = cb->getHero(obj->id);
-
-					return sptr(Goals::GatherArmy().sethero(heroToProtect));
-				}
-				else if(relations == PlayerRelations::ALLIES)
-				{
-					break;
-				}
-			}
-
-			return sptr(Goals::VisitObj(obj->id.getNum()));
-		}
-		case CQuest::MISSION_PRIMARY_STAT:
-		{
-			auto heroes = cb->getHeroesInfo();
-			for (auto hero : heroes)
-			{
-				if (q.quest->checkQuest(hero))
-				{
-					return sptr(Goals::VisitObj(q.obj->id.getNum()).sethero(hero));
-				}
-			}
-			for (int i = 0; i < q.quest->m2stats.size(); ++i)
-			{
-				logAi->debug("Don't know how to increase primary stat %d", i);
-			}
-			break;
-		}
-		case CQuest::MISSION_LEVEL:
-		{
-			auto heroes = cb->getHeroesInfo();
-			for (auto hero : heroes)
-			{
-				if (q.quest->checkQuest(hero))
-				{
-					return sptr(Goals::VisitObj(q.obj->id.getNum()).sethero(hero)); //TODO: causes infinite loop :/
-				}
-			}
-			logAi->debug("Don't know how to reach hero level %d", q.quest->m13489val);
-			break;
-		}
-		case CQuest::MISSION_PLAYER:
-		{
-			if (playerID.getNum() != q.quest->m13489val)
-				logAi->debug("Can't be player of color %d", q.quest->m13489val);
-			break;
-		}
-		case CQuest::MISSION_KEYMASTER:
-		{
-			return sptr(Goals::FindObj(Obj::KEYMASTER, q.obj->subID));
-			break;
-		}
-		} //end of switch
-	}
-
-	logAi->trace(
-		"Returning %s, tile: %s, objid: %d, hero: %s", 
-		result->name(), 
-		result->tile.toString(), 
-		result->objid, 
-		result->hero.validAndSet() ? result->hero->name : "not specified");
-
-	return result;
-}
-
 void VCAI::performTypicalActions()
 {
 	for(auto h : getUnblockedHeroes())

+ 0 - 1
AI/VCAI/VCAI.h

@@ -210,7 +210,6 @@ public:
 	void setGoal(HeroPtr h, Goals::TSubgoal goal);
 	void evaluateGoal(HeroPtr h); //evaluates goal assigned to hero, if any
 	void completeGoal(Goals::TSubgoal goal); //safely removes goal from reserved hero
-	Goals::TSubgoal questToGoal(const QuestInfo & q);
 
 	void recruitHero(const CGTownInstance * t, bool throwing = false);
 	bool isGoodForVisit(const CGObjectInstance * obj, HeroPtr h, boost::optional<uint32_t> movementCostLimit = boost::none);