Explorar o código

Merged master into develop

Ivan Savenko %!s(int64=2) %!d(string=hai) anos
pai
achega
537f9fa048
Modificáronse 90 ficheiros con 1766 adicións e 717 borrados
  1. 2 2
      AI/BattleAI/BattleAI.cpp
  2. 1 1
      AI/BattleAI/BattleAI.h
  3. 41 37
      AI/Nullkiller/AIGateway.cpp
  4. 1 2
      AI/Nullkiller/AIGateway.h
  5. 0 4
      AI/Nullkiller/AIUtility.cpp
  6. 60 8
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  7. 34 8
      AI/Nullkiller/Analyzers/ArmyManager.h
  8. 39 13
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  9. 161 17
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  10. 12 1
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h
  11. 36 7
      AI/Nullkiller/Analyzers/HeroManager.cpp
  12. 4 0
      AI/Nullkiller/Analyzers/HeroManager.h
  13. 6 1
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  14. 10 6
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp
  15. 236 115
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  16. 5 0
      AI/Nullkiller/Behaviors/DefenceBehavior.h
  17. 105 20
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  18. 21 0
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  19. 2 0
      AI/Nullkiller/CMakeLists.txt
  20. 4 6
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  21. 42 13
      AI/Nullkiller/Engine/Nullkiller.cpp
  22. 6 2
      AI/Nullkiller/Engine/Nullkiller.h
  23. 205 76
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  24. 6 0
      AI/Nullkiller/Engine/PriorityEvaluator.h
  25. 1 1
      AI/Nullkiller/Goals/BuyArmy.cpp
  26. 49 9
      AI/Nullkiller/Goals/Composition.cpp
  27. 2 6
      AI/Nullkiller/Goals/Composition.h
  28. 14 0
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  29. 12 9
      AI/Nullkiller/Goals/RecruitHero.cpp
  30. 7 5
      AI/Nullkiller/Goals/RecruitHero.h
  31. 68 0
      AI/Nullkiller/Helpers/ArmyFormation.cpp
  32. 38 0
      AI/Nullkiller/Helpers/ArmyFormation.h
  33. 7 0
      AI/Nullkiller/Markers/ArmyUpgrade.cpp
  34. 1 0
      AI/Nullkiller/Markers/ArmyUpgrade.h
  35. 2 2
      AI/Nullkiller/Markers/DefendTown.cpp
  36. 4 1
      AI/Nullkiller/Markers/DefendTown.h
  37. 1 1
      AI/Nullkiller/Markers/HeroExchange.cpp
  38. 5 1
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  39. 3 3
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  40. 5 0
      AI/Nullkiller/Pathfinding/AIPathfinder.cpp
  41. 1 0
      AI/Nullkiller/Pathfinding/Actors.cpp
  42. 1 1
      AI/StupidAI/StupidAI.cpp
  43. 1 1
      AI/StupidAI/StupidAI.h
  44. 12 8
      AI/VCAI/VCAI.cpp
  45. 1 1
      AI/VCAI/VCAI.h
  46. 4 0
      CMakeLists.txt
  47. 10 1
      ChangeLog.md
  48. 19 1
      Mods/vcmi/config/vcmi/polish.json
  49. 1 2
      README.md
  50. 16 18
      client/CPlayerInterface.cpp
  51. 1 3
      client/CPlayerInterface.h
  52. 1 1
      client/Client.cpp
  53. 2 2
      client/battle/BattleWindow.cpp
  54. 1 1
      client/gui/ShortcutHandler.cpp
  55. 13 22
      client/renderSDL/CursorHardware.cpp
  56. 5 0
      client/widgets/Buttons.cpp
  57. 9 0
      client/windows/CCastleInterface.cpp
  58. 2 0
      client/windows/CCastleInterface.h
  59. 155 105
      config/ai/object-priorities.txt
  60. 1 1
      config/objects/rewardableOncePerWeek.json
  61. 2 2
      config/objects/rewardableOnceVisitable.json
  62. 1 1
      config/objects/rewardablePickable.json
  63. 2 2
      config/schemas/settings.json
  64. 0 0
      config/widgets/battleWindow2.json
  65. 3 3
      config/widgets/settings/adventureOptionsTab.json
  66. 1 1
      debian/changelog
  67. 1 1
      launcher/eu.vcmi.VCMI.metainfo.xml
  68. 4 4
      launcher/translation/german.ts
  69. 8 8
      launcher/translation/polish.ts
  70. 2 2
      lib/CGameInterface.cpp
  71. 1 1
      lib/CGameInterface.h
  72. 12 0
      lib/CGeneralTextHandler.cpp
  73. 4 4
      lib/CGeneralTextHandler.h
  74. 6 2
      lib/CTownHandler.cpp
  75. 1 1
      lib/CTownHandler.h
  76. 1 1
      lib/IGameEventsReceiver.h
  77. 4 6
      lib/NetPacksLib.cpp
  78. 1 0
      lib/battle/BattleInfo.cpp
  79. 5 0
      lib/battle/BattleInfo.h
  80. 1 2
      lib/gameState/CGameState.cpp
  81. 14 7
      lib/gameState/CGameStateCampaign.cpp
  82. 25 10
      lib/mapObjects/CGHeroInstance.cpp
  83. 1 0
      lib/mapObjects/CGHeroInstance.h
  84. 26 2
      lib/rmg/modificators/ObjectManager.cpp
  85. 1 1
      lib/serializer/CSerializer.h
  86. 28 28
      mapeditor/translation/german.ts
  87. 61 61
      mapeditor/translation/polish.ts
  88. 30 17
      server/CGameHandler.cpp
  89. 1 1
      server/CQuery.cpp
  90. 5 4
      server/HeroPoolProcessor.cpp

+ 2 - 2
AI/BattleAI/BattleAI.cpp

@@ -826,7 +826,7 @@ void CBattleAI::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcas
 	ps.value = totalGain;
 	ps.value = totalGain;
 }
 }
 
 
-void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side)
+void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
 {
 {
 	LOG_TRACE(logAi);
 	LOG_TRACE(logAi);
 	side = Side;
 	side = Side;
@@ -863,7 +863,7 @@ std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
 
 
 	bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size();
 	bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size();
 
 
-	if(!bs.canFlee || !bs.canSurrender)
+	if(!bs.canFlee && !bs.canSurrender)
 	{
 	{
 		return std::nullopt;
 		return std::nullopt;
 	}
 	}

+ 1 - 1
AI/BattleAI/BattleAI.h

@@ -83,7 +83,7 @@ public:
 	BattleAction selectStackAction(const CStack * stack);
 	BattleAction selectStackAction(const CStack * stack);
 	std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack *stack);
 	std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack *stack);
 
 
-	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side) override;
+	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side, bool replayAllowed) override;
 	//void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
 	//void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
 	//void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
 	//void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
 	//void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
 	//void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack

+ 41 - 37
AI/Nullkiller/AIGateway.cpp

@@ -29,7 +29,7 @@ namespace NKAI
 {
 {
 
 
 // our to enemy strength ratio constants
 // our to enemy strength ratio constants
-const float SAFE_ATTACK_CONSTANT = 1.2f;
+const float SAFE_ATTACK_CONSTANT = 1.1f;
 const float RETREAT_THRESHOLD = 0.3f;
 const float RETREAT_THRESHOLD = 0.3f;
 const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
 const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
 
 
@@ -90,9 +90,11 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose)
 	LOG_TRACE(logAi);
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
 	NET_EVENT_HANDLER;
 
 
-	validateObject(details.id); //enemy hero may have left visible area
 	auto hero = cb->getHero(details.id);
 	auto hero = cb->getHero(details.id);
 
 
+	if(!hero)
+		validateObject(details.id); //enemy hero may have left visible area
+
 	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
 	const int3 from = hero ? hero->convertToVisitablePos(details.start) : (details.start - int3(0,1,0));;
 	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
 	const int3 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
 
 
@@ -777,28 +779,21 @@ void AIGateway::makeTurn()
 	boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
 	boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
 	setThreadName("AIGateway::makeTurn");
 	setThreadName("AIGateway::makeTurn");
 
 
+	cb->sendMessage("vcmieagles");
+
+	retrieveVisitableObjs();
+
 	if(cb->getDate(Date::DAY_OF_WEEK) == 1)
 	if(cb->getDate(Date::DAY_OF_WEEK) == 1)
 	{
 	{
-		std::vector<const CGObjectInstance *> objs;
-		retrieveVisitableObjs(objs, true);
-
-		for(const CGObjectInstance * obj : objs)
+		for(const CGObjectInstance * obj : nullkiller->memory->visitableObjs)
 		{
 		{
 			if(isWeeklyRevisitable(obj))
 			if(isWeeklyRevisitable(obj))
 			{
 			{
-				addVisitableObj(obj);
 				nullkiller->memory->markObjectUnvisited(obj);
 				nullkiller->memory->markObjectUnvisited(obj);
 			}
 			}
 		}
 		}
 	}
 	}
 
 
-	cb->sendMessage("vcmieagles");
-
-	if(cb->getDate(Date::DAY) == 1)
-	{
-		retrieveVisitableObjs();
-	}
-
 #if NKAI_TRACE_LEVEL == 0
 #if NKAI_TRACE_LEVEL == 0
 	try
 	try
 	{
 	{
@@ -809,7 +804,7 @@ void AIGateway::makeTurn()
 		for (auto h : cb->getHeroesInfo())
 		for (auto h : cb->getHeroesInfo())
 		{
 		{
 			if (h->movementPointsRemaining())
 			if (h->movementPointsRemaining())
-				logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
+				logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
 		}
 		}
 #if NKAI_TRACE_LEVEL == 0
 #if NKAI_TRACE_LEVEL == 0
 	}
 	}
@@ -872,6 +867,19 @@ void AIGateway::pickBestCreatures(const CArmedInstance * destinationArmy, const
 
 
 	auto bestArmy = nullkiller->armyManager->getBestArmy(destinationArmy, destinationArmy, source);
 	auto bestArmy = nullkiller->armyManager->getBestArmy(destinationArmy, destinationArmy, source);
 
 
+	for(auto army : armies)
+	{
+		// move first stack at first slot if empty to avoid can not take away last creature
+		if(!army->hasStackAtSlot(SlotID(0)) && army->stacksCount() > 0)
+		{
+			cb->mergeOrSwapStacks(
+				army,
+				army,
+				SlotID(0),
+				army->Slots().begin()->first);
+		}
+	}
+
 	//foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs
 	//foreach best type -> iterate over slots in both armies and if it's the appropriate type, send it to the slot where it belongs
 	for(SlotID i = SlotID(0); i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot
 	for(SlotID i = SlotID(0); i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot
 	{
 	{
@@ -1059,20 +1067,25 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
 		int count = d->creatures[i].first;
 		int count = d->creatures[i].first;
 		CreatureID creID = d->creatures[i].second.back();
 		CreatureID creID = d->creatures[i].second.back();
 
 
+		if(!recruiter->getSlotFor(creID).validSlot())
+		{
+			continue;
+		}
+
 		vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost());
 		vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost());
 		if(count > 0)
 		if(count > 0)
 			cb->recruitCreatures(d, recruiter, creID, count, i);
 			cb->recruitCreatures(d, recruiter, creID, count, i);
 	}
 	}
 }
 }
 
 
-void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side)
+void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
 {
 {
 	NET_EVENT_HANDLER;
 	NET_EVENT_HANDLER;
 	assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE);
 	assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE);
 	status.setBattle(ONGOING_BATTLE);
 	status.setBattle(ONGOING_BATTLE);
 	const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
 	const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
 	battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
 	battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
-	CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side);
+	CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed);
 }
 }
 
 
 void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
 void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
@@ -1083,12 +1096,16 @@ void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
 	bool won = br->winner == myCb->battleGetMySide();
 	bool won = br->winner == myCb->battleGetMySide();
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename);
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename);
 	battlename.clear();
 	battlename.clear();
-	status.addQuery(queryID, "Combat result dialog");
-	const int confirmAction = 0;
-	requestActionASAP([=]()
+
+	if (queryID != -1)
 	{
 	{
-		answerQuery(queryID, confirmAction);
-	});
+		status.addQuery(queryID, "Combat result dialog");
+		const int confirmAction = 0;
+		requestActionASAP([=]()
+		{
+			answerQuery(queryID, confirmAction);
+		});
+	}
 	CAdventureAI::battleEnd(br, queryID);
 	CAdventureAI::battleEnd(br, queryID);
 }
 }
 
 
@@ -1098,26 +1115,13 @@ void AIGateway::waitTillFree()
 	status.waitTillFree();
 	status.waitTillFree();
 }
 }
 
 
-void AIGateway::retrieveVisitableObjs(std::vector<const CGObjectInstance *> & out, bool includeOwned) const
-{
-	foreach_tile_pos([&](const int3 & pos)
-	{
-		for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
-		{
-			if(includeOwned || obj->tempOwner != playerID)
-				out.push_back(obj);
-		}
-	});
-}
-
 void AIGateway::retrieveVisitableObjs()
 void AIGateway::retrieveVisitableObjs()
 {
 {
 	foreach_tile_pos([&](const int3 & pos)
 	foreach_tile_pos([&](const int3 & pos)
 	{
 	{
 		for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
 		for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
 		{
 		{
-			if(obj->tempOwner != playerID)
-				addVisitableObj(obj);
+			addVisitableObj(obj);
 		}
 		}
 	});
 	});
 }
 }
@@ -1175,7 +1179,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 	if(startHpos == dst)
 	if(startHpos == dst)
 	{
 	{
 		//FIXME: this assertion fails also if AI moves onto defeated guarded object
 		//FIXME: this assertion fails also if AI moves onto defeated guarded object
-		assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
+		//assert(cb->getVisitableObjs(dst).size() > 1); //there's no point in revisiting tile where there is no visitable object
 		cb->moveHero(*h, h->convertFromVisitablePos(dst));
 		cb->moveHero(*h, h->convertFromVisitablePos(dst));
 		afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly?
 		afterMovementCheck(); // TODO: is it feasible to hero get killed there if game work properly?
 		// If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared
 		// If revisiting, teleport probing is never done, and so the entries into the list would remain unused and uncleared

+ 1 - 2
AI/Nullkiller/AIGateway.h

@@ -169,7 +169,7 @@ public:
 	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
 	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
 	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
 	std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
 
 
-	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override;
+	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override;
 	void battleEnd(const BattleResult * br, QueryID queryID) override;
 	void battleEnd(const BattleResult * br, QueryID queryID) override;
 
 
 	void makeTurn();
 	void makeTurn();
@@ -195,7 +195,6 @@ public:
 
 
 	void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it
 	void validateObject(const CGObjectInstance * obj); //checks if object is still visible and if not, removes references to it
 	void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it
 	void validateObject(ObjectIdRef obj); //checks if object is still visible and if not, removes references to it
-	void retrieveVisitableObjs(std::vector<const CGObjectInstance *> & out, bool includeOwned = false) const;
 	void retrieveVisitableObjs();
 	void retrieveVisitableObjs();
 	virtual std::vector<const CGObjectInstance *> getFlaggedObjects() const;
 	virtual std::vector<const CGObjectInstance *> getFlaggedObjects() const;
 
 

+ 0 - 4
AI/Nullkiller/AIUtility.cpp

@@ -323,13 +323,9 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj)
 
 
 	if(dynamic_cast<const CGDwelling *>(obj))
 	if(dynamic_cast<const CGDwelling *>(obj))
 		return true;
 		return true;
-	if(dynamic_cast<const CBank *>(obj)) //banks tend to respawn often in mods
-		return true;
 
 
 	switch(obj->ID)
 	switch(obj->ID)
 	{
 	{
-	case Obj::STABLES:
-	case Obj::MAGIC_WELL:
 	case Obj::HILL_FORT:
 	case Obj::HILL_FORT:
 		return true;
 		return true;
 	case Obj::BORDER_GATE:
 	case Obj::BORDER_GATE:

+ 60 - 8
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -13,6 +13,7 @@
 #include "../Engine/Nullkiller.h"
 #include "../Engine/Nullkiller.h"
 #include "../../../CCallback.h"
 #include "../../../CCallback.h"
 #include "../../../lib/mapObjects/MapObjects.h"
 #include "../../../lib/mapObjects/MapObjects.h"
+#include "../../../lib/GameConstants.h"
 
 
 namespace NKAI
 namespace NKAI
 {
 {
@@ -33,6 +34,45 @@ public:
 	}
 	}
 };
 };
 
 
+void ArmyUpgradeInfo::addArmyToBuy(std::vector<SlotInfo> army)
+{
+	for(auto slot : army)
+	{
+		resultingArmy.push_back(slot);
+
+		upgradeValue += slot.power;
+		upgradeCost += slot.creature->getFullRecruitCost() * slot.count;
+	}
+}
+
+void ArmyUpgradeInfo::addArmyToGet(std::vector<SlotInfo> army)
+{
+	for(auto slot : army)
+	{
+		resultingArmy.push_back(slot);
+
+		upgradeValue += slot.power;
+	}
+}
+
+std::vector<SlotInfo> ArmyManager::toSlotInfo(std::vector<creInfo> army) const
+{
+	std::vector<SlotInfo> result;
+
+	for(auto i : army)
+	{
+		SlotInfo slot;
+
+		slot.creature = VLC->creh->objects[i.cre->getId()];
+		slot.count = i.count;
+		slot.power = evaluateStackPower(i.cre, i.count);
+
+		result.push_back(slot);
+	}
+
+	return result;
+}
+
 uint64_t ArmyManager::howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const
 uint64_t ArmyManager::howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const
 {
 {
 	return howManyReinforcementsCanGet(hero, hero, source);
 	return howManyReinforcementsCanGet(hero, hero, source);
@@ -136,7 +176,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 		{
 		{
 			if(vstd::contains(allowedFactions, slot.creature->getFaction()))
 			if(vstd::contains(allowedFactions, slot.creature->getFaction()))
 			{
 			{
-				auto slotID = newArmyInstance.getSlotFor(slot.creature);
+				auto slotID = newArmyInstance.getSlotFor(slot.creature->getId());
 
 
 				if(slotID.validSlot())
 				if(slotID.validSlot())
 				{
 				{
@@ -238,7 +278,8 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
 ui64 ArmyManager::howManyReinforcementsCanBuy(
 ui64 ArmyManager::howManyReinforcementsCanBuy(
 	const CCreatureSet * targetArmy,
 	const CCreatureSet * targetArmy,
 	const CGDwelling * dwelling,
 	const CGDwelling * dwelling,
-	const TResources & availableResources) const
+	const TResources & availableResources,
+	uint8_t turn) const
 {
 {
 	ui64 aivalue = 0;
 	ui64 aivalue = 0;
 	auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources);
 	auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources);
@@ -259,17 +300,29 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(const CCreatureSet * her
 std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 	const CCreatureSet * hero,
 	const CCreatureSet * hero,
 	const CGDwelling * dwelling,
 	const CGDwelling * dwelling,
-	TResources availableRes) const
+	TResources availableRes,
+	uint8_t turn) const
 {
 {
 	std::vector<creInfo> creaturesInDwellings;
 	std::vector<creInfo> creaturesInDwellings;
 	int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount();
 	int freeHeroSlots = GameConstants::ARMY_SIZE - hero->stacksCount();
+	bool countGrowth = (cb->getDate(Date::DAY_OF_WEEK) + turn) > 7;
+
+	const CGTownInstance * town = dwelling->ID == Obj::TOWN
+		? dynamic_cast<const CGTownInstance *>(dwelling)
+		: nullptr;
 
 
 	for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
 	for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
 	{
 	{
 		auto ci = infoFromDC(dwelling->creatures[i]);
 		auto ci = infoFromDC(dwelling->creatures[i]);
 
 
-		if(!ci.count || ci.creID == -1)
-			continue;
+		if(ci.creID == -1) continue;
+
+		if(i < GameConstants::CREATURES_PER_TOWN && countGrowth)
+		{
+			ci.count += town ? town->creatureGrowth(i) : ci.cre->getGrowth();
+		}
+
+		if(!ci.count) continue;
 
 
 		SlotID dst = hero->getSlotFor(ci.creID);
 		SlotID dst = hero->getSlotFor(ci.creID);
 		if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
 		if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
@@ -282,8 +335,7 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 
 
 		vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford
 		vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford
 
 
-		if(!ci.count)
-			continue;
+		if(!ci.count) continue;
 
 
 		ci.level = i; //this is important for Dungeon Summoning Portal
 		ci.level = i; //this is important for Dungeon Summoning Portal
 		creaturesInDwellings.push_back(ci);
 		creaturesInDwellings.push_back(ci);
@@ -307,7 +359,7 @@ ui64 ArmyManager::howManyReinforcementsCanGet(const IBonusBearer * armyCarrier,
 	return newArmy > oldArmy ? newArmy - oldArmy : 0;
 	return newArmy > oldArmy ? newArmy - oldArmy : 0;
 }
 }
 
 
-uint64_t ArmyManager::evaluateStackPower(const CCreature * creature, int count) const
+uint64_t ArmyManager::evaluateStackPower(const Creature * creature, int count) const
 {
 {
 	return creature->getAIValue() * count;
 	return creature->getAIValue() * count;
 }
 }

+ 34 - 8
AI/Nullkiller/Analyzers/ArmyManager.h

@@ -34,6 +34,9 @@ struct ArmyUpgradeInfo
 	std::vector<SlotInfo> resultingArmy;
 	std::vector<SlotInfo> resultingArmy;
 	uint64_t upgradeValue = 0;
 	uint64_t upgradeValue = 0;
 	TResources upgradeCost;
 	TResources upgradeCost;
+
+	void addArmyToBuy(std::vector<SlotInfo> army);
+	void addArmyToGet(std::vector<SlotInfo> army);
 };
 };
 
 
 class DLL_EXPORT IArmyManager //: public: IAbstractManager
 class DLL_EXPORT IArmyManager //: public: IAbstractManager
@@ -45,20 +48,33 @@ public:
 	virtual	ui64 howManyReinforcementsCanBuy(
 	virtual	ui64 howManyReinforcementsCanBuy(
 		const CCreatureSet * targetArmy,
 		const CCreatureSet * targetArmy,
 		const CGDwelling * dwelling,
 		const CGDwelling * dwelling,
-		const TResources & availableResources) const = 0;
+		const TResources & availableResources,
+		uint8_t turn = 0) const = 0;
+
 	virtual ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const = 0;
 	virtual ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const = 0;
-	virtual ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0;
+	virtual ui64 howManyReinforcementsCanGet(
+		const IBonusBearer * armyCarrier,
+		const CCreatureSet * target,
+		const CCreatureSet * source) const = 0;
+
 	virtual std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0;
 	virtual std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0;
 	virtual std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const = 0;
 	virtual std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const = 0;
 	virtual std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const = 0;
 	virtual std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const = 0;
-	virtual std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling, TResources availableRes) const = 0;
-	virtual uint64_t evaluateStackPower(const CCreature * creature, int count) const = 0;
+	virtual std::vector<SlotInfo> toSlotInfo(std::vector<creInfo> creatures) const = 0;
+
+	virtual std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0;
+	virtual std::vector<creInfo> getArmyAvailableToBuy(
+		const CCreatureSet * hero,
+		const CGDwelling * dwelling,
+		TResources availableRes,
+		uint8_t turn = 0) const = 0;
+
+	virtual uint64_t evaluateStackPower(const Creature * creature, int count) const = 0;
 	virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0;
 	virtual SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const = 0;
 	virtual ArmyUpgradeInfo calculateCreaturesUpgrade(
 	virtual ArmyUpgradeInfo calculateCreaturesUpgrade(
 		const CCreatureSet * army,
 		const CCreatureSet * army,
 		const CGObjectInstance * upgrader,
 		const CGObjectInstance * upgrader,
 		const TResources & availableResources) const = 0;
 		const TResources & availableResources) const = 0;
-	virtual std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const = 0;
 	virtual std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const = 0;
 	virtual std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const = 0;
 };
 };
 
 
@@ -74,20 +90,30 @@ private:
 public:
 public:
 	ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {}
 	ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {}
 	void update() override;
 	void update() override;
+
 	ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override;
 	ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override;
 	ui64 howManyReinforcementsCanBuy(
 	ui64 howManyReinforcementsCanBuy(
 		const CCreatureSet * targetArmy,
 		const CCreatureSet * targetArmy,
 		const CGDwelling * dwelling,
 		const CGDwelling * dwelling,
-		const TResources & availableResources) const override;
+		const TResources & availableResources,
+		uint8_t turn = 0) const override;
+
 	ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const override;
 	ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const override;
 	ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
 	ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
 	std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
 	std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
 	std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const override;
 	std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const override;
 	std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override;
 	std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override;
-	std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling, TResources availableRes) const override;
+	std::vector<SlotInfo> toSlotInfo(std::vector<creInfo> creatures) const override;
+
 	std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override;
 	std::vector<creInfo> getArmyAvailableToBuy(const CCreatureSet * hero, const CGDwelling * dwelling) const override;
+	std::vector<creInfo> getArmyAvailableToBuy(
+		const CCreatureSet * hero,
+		const CGDwelling * dwelling,
+		TResources availableRes,
+		uint8_t turn = 0) const override;
+
 	std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const override;
 	std::shared_ptr<CCreatureSet> getArmyAvailableToBuyAsCCreatureSet(const CGDwelling * dwelling, TResources availableRes) const override;
-	uint64_t evaluateStackPower(const CCreature * creature, int count) const override;
+	uint64_t evaluateStackPower(const Creature * creature, int count) const override;
 	SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override;
 	SlotInfo getTotalCreaturesAvailable(CreatureID creatureID) const override;
 	ArmyUpgradeInfo calculateCreaturesUpgrade(
 	ArmyUpgradeInfo calculateCreaturesUpgrade(
 		const CCreatureSet * army, 
 		const CCreatureSet * army, 

+ 39 - 13
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -68,19 +68,22 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
 	logAi->trace("Checking other buildings");
 	logAi->trace("Checking other buildings");
 
 
 	std::vector<std::vector<BuildingID>> otherBuildings = {
 	std::vector<std::vector<BuildingID>> otherBuildings = {
-		{BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL}
+		{BuildingID::TOWN_HALL, BuildingID::CITY_HALL, BuildingID::CAPITOL},
+		{BuildingID::MAGES_GUILD_3, BuildingID::MAGES_GUILD_5}
 	};
 	};
 
 
 	if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
 	if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
 	{
 	{
 		otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
 		otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
+		otherBuildings.push_back({BuildingID::HORDE_1});
+		otherBuildings.push_back({BuildingID::HORDE_2});
 	}
 	}
 
 
 	for(auto & buildingSet : otherBuildings)
 	for(auto & buildingSet : otherBuildings)
 	{
 	{
 		for(auto & buildingID : buildingSet)
 		for(auto & buildingID : buildingSet)
 		{
 		{
-			if(!developmentInfo.town->hasBuilt(buildingID))
+			if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->town->buildings.count(buildingID))
 			{
 			{
 				developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID));
 				developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID));
 
 
@@ -163,8 +166,8 @@ void BuildAnalyzer::update()
 	}
 	}
 	else
 	else
 	{
 	{
-		goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 10000.0f
-			+ (float)armyCost[EGameResID::GOLD] / (1 + ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
+		goldPreasure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
+			+ (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
 	}
 	}
 
 
 	logAi->trace("Gold preasure: %f", goldPreasure);
 	logAi->trace("Gold preasure: %f", goldPreasure);
@@ -190,12 +193,28 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 	const CCreature * creature = nullptr;
 	const CCreature * creature = nullptr;
 	CreatureID baseCreatureID;
 	CreatureID baseCreatureID;
 
 
+	int creatureLevel = -1;
+	int creatureUpgrade = 0;
+
 	if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST)
 	if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST)
 	{
 	{
-		int level = toBuild - BuildingID::DWELL_FIRST;
-		auto creatures = townInfo->creatures.at(level % GameConstants::CREATURES_PER_TOWN);
-		auto creatureID = creatures.size() > level / GameConstants::CREATURES_PER_TOWN
-			? creatures.at(level / GameConstants::CREATURES_PER_TOWN)
+		creatureLevel = (toBuild - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN;
+		creatureUpgrade = (toBuild - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN;
+	}
+	else if(toBuild == BuildingID::HORDE_1 || toBuild == BuildingID::HORDE_1_UPGR)
+	{
+		creatureLevel = townInfo->hordeLvl.at(0);
+	}
+	else if(toBuild == BuildingID::HORDE_2 || toBuild == BuildingID::HORDE_2_UPGR)
+	{
+		creatureLevel = townInfo->hordeLvl.at(1);
+	}
+
+	if(creatureLevel >=  0)
+	{
+		auto creatures = townInfo->creatures.at(creatureLevel);
+		auto creatureID = creatures.size() > creatureUpgrade
+			? creatures.at(creatureUpgrade)
 			: creatures.front();
 			: creatures.front();
 
 
 		baseCreatureID = creatures.front();
 		baseCreatureID = creatures.front();
@@ -366,12 +385,19 @@ BuildingInfo::BuildingInfo(
 		}
 		}
 		else
 		else
 		{
 		{
-			creatureGrows = creature->getGrowth();
+			if(BuildingID::DWELL_FIRST <= id && id <= BuildingID::DWELL_UP_LAST)
+			{
+				creatureGrows = creature->getGrowth();
 
 
-			if(town->hasBuilt(BuildingID::CASTLE))
-				creatureGrows *= 2;
-			else if(town->hasBuilt(BuildingID::CITADEL))
-				creatureGrows += creatureGrows / 2;
+				if(town->hasBuilt(BuildingID::CASTLE))
+					creatureGrows *= 2;
+				else if(town->hasBuilt(BuildingID::CITADEL))
+					creatureGrows += creatureGrows / 2;
+			}
+			else
+			{
+				creatureGrows = creature->getHorde();
+			}
 		}
 		}
 
 
 		armyStrength = ai->armyManager->evaluateStackPower(creature, creatureGrows);
 		armyStrength = ai->armyManager->evaluateStackPower(creature, creatureGrows);

+ 161 - 17
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -17,20 +17,29 @@ namespace NKAI
 
 
 HitMapInfo HitMapInfo::NoTreat;
 HitMapInfo HitMapInfo::NoTreat;
 
 
+double HitMapInfo::value() const
+{
+	return danger / std::sqrt(turn / 3.0f + 1);
+}
+
 void DangerHitMapAnalyzer::updateHitMap()
 void DangerHitMapAnalyzer::updateHitMap()
 {
 {
-	if(upToDate)
+	if(hitMapUpToDate)
 		return;
 		return;
 
 
 	logAi->trace("Update danger hitmap");
 	logAi->trace("Update danger hitmap");
 
 
-	upToDate = true;
+	hitMapUpToDate = true;
 	auto start = std::chrono::high_resolution_clock::now();
 	auto start = std::chrono::high_resolution_clock::now();
 
 
 	auto cb = ai->cb.get();
 	auto cb = ai->cb.get();
 	auto mapSize = ai->cb->getMapSize();
 	auto mapSize = ai->cb->getMapSize();
-	hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]);
+	
+	if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z)
+		hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]);
+
 	enemyHeroAccessibleObjects.clear();
 	enemyHeroAccessibleObjects.clear();
+	townTreats.clear();
 
 
 	std::map<PlayerColor, std::map<const CGHeroInstance *, HeroRole>> heroes;
 	std::map<PlayerColor, std::map<const CGHeroInstance *, HeroRole>> heroes;
 
 
@@ -67,27 +76,26 @@ void DangerHitMapAnalyzer::updateHitMap()
 				if(path.getFirstBlockedAction())
 				if(path.getFirstBlockedAction())
 					continue;
 					continue;
 
 
-				auto tileDanger = path.getHeroStrength();
-				auto turn = path.turn();
 				auto & node = hitMap[pos.x][pos.y][pos.z];
 				auto & node = hitMap[pos.x][pos.y][pos.z];
 
 
-				if(tileDanger / (turn / 3 + 1) > node.maximumDanger.danger / (node.maximumDanger.turn / 3 + 1)
-					|| (tileDanger == node.maximumDanger.danger && node.maximumDanger.turn > turn))
+				HitMapInfo newTreat;
+
+				newTreat.hero = path.targetHero;
+				newTreat.turn = path.turn();
+				newTreat.danger = path.getHeroStrength();
+
+				if(newTreat.value() > node.maximumDanger.value())
 				{
 				{
-					node.maximumDanger.danger = tileDanger;
-					node.maximumDanger.turn = turn;
-					node.maximumDanger.hero = path.targetHero;
+					node.maximumDanger = newTreat;
 				}
 				}
 
 
-				if(turn < node.fastestDanger.turn
-					|| (turn == node.fastestDanger.turn && node.fastestDanger.danger < tileDanger))
+				if(newTreat.turn < node.fastestDanger.turn
+					|| (newTreat.turn == node.fastestDanger.turn && node.fastestDanger.danger < newTreat.danger))
 				{
 				{
-					node.fastestDanger.danger = tileDanger;
-					node.fastestDanger.turn = turn;
-					node.fastestDanger.hero = path.targetHero;
+					node.fastestDanger = newTreat;
 				}
 				}
 
 
-				if(turn == 0)
+				if(newTreat.turn == 0)
 				{
 				{
 					auto objects = cb->getVisitableObjs(pos, false);
 					auto objects = cb->getVisitableObjs(pos, false);
 					
 					
@@ -95,6 +103,26 @@ void DangerHitMapAnalyzer::updateHitMap()
 					{
 					{
 						if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES)
 						if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES)
 							enemyHeroAccessibleObjects[path.targetHero].insert(obj);
 							enemyHeroAccessibleObjects[path.targetHero].insert(obj);
+
+						if(obj->ID == Obj::TOWN && obj->getOwner() == ai->playerID)
+						{
+							auto & treats = townTreats[obj->id];
+							auto treat = std::find_if(treats.begin(), treats.end(), [&](const HitMapInfo & i) -> bool
+								{
+									return i.hero.hid == path.targetHero->id;
+								});
+
+							if(treat == treats.end())
+							{
+								treats.emplace_back();
+								treat = std::prev(treats.end(), 1);
+							}
+
+							if(newTreat.value() > treat->value())
+							{
+								*treat = newTreat;
+							}
+						}
 					}
 					}
 				}
 				}
 			}
 			}
@@ -104,6 +132,122 @@ void DangerHitMapAnalyzer::updateHitMap()
 	logAi->trace("Danger hit map updated in %ld", timeElapsed(start));
 	logAi->trace("Danger hit map updated in %ld", timeElapsed(start));
 }
 }
 
 
+void DangerHitMapAnalyzer::calculateTileOwners()
+{
+	if(tileOwnersUpToDate) return;
+
+	tileOwnersUpToDate = true;
+
+	auto cb = ai->cb.get();
+	auto mapSize = ai->cb->getMapSize();
+
+	if(hitMap.shape()[0] != mapSize.x || hitMap.shape()[1] != mapSize.y || hitMap.shape()[2] != mapSize.z)
+		hitMap.resize(boost::extents[mapSize.x][mapSize.y][mapSize.z]);
+
+	std::map<const CGHeroInstance *, HeroRole> townHeroes;
+	std::map<const CGHeroInstance *, const CGTownInstance *> heroTownMap;
+	PathfinderSettings pathfinderSettings;
+
+	pathfinderSettings.mainTurnDistanceLimit = 5;
+
+	auto addTownHero = [&](const CGTownInstance * town)
+	{
+			auto townHero = new CGHeroInstance();
+			CRandomGenerator rng;
+			auto visitablePos = town->visitablePos();
+			
+			townHero->setOwner(ai->playerID); // lets avoid having multiple colors
+			townHero->initHero(rng, static_cast<HeroTypeID>(0));
+			townHero->pos = townHero->convertFromVisitablePos(visitablePos);
+			townHero->initObj(rng);
+			
+			heroTownMap[townHero] = town;
+			townHeroes[townHero] = HeroRole::MAIN;
+	};
+
+	for(auto obj : ai->memory->visitableObjs)
+	{
+		if(obj && obj->ID == Obj::TOWN)
+		{
+			addTownHero(dynamic_cast<const CGTownInstance *>(obj));
+		}
+	}
+
+	for(auto town : cb->getTownsInfo())
+	{
+		addTownHero(town);
+	}
+
+	ai->pathfinder->updatePaths(townHeroes, PathfinderSettings());
+
+	pforeachTilePos(mapSize, [&](const int3 & pos)
+		{
+			float ourDistance = std::numeric_limits<float>::max();
+			float enemyDistance = std::numeric_limits<float>::max();
+			const CGTownInstance * enemyTown = nullptr;
+			const CGTownInstance * ourTown = nullptr;
+
+			for(AIPath & path : ai->pathfinder->getPathInfo(pos))
+			{
+				if(!path.targetHero || path.getFirstBlockedAction())
+					continue;
+
+				auto town = heroTownMap[path.targetHero];
+
+				if(town->getOwner() == ai->playerID)
+				{
+					if(ourDistance > path.movementCost())
+					{
+						ourDistance = path.movementCost();
+						ourTown = town;
+					}
+				}
+				else
+				{
+					if(enemyDistance > path.movementCost())
+					{
+						enemyDistance = path.movementCost();
+						enemyTown = town;
+					}
+				}
+			}
+
+			if(ourDistance == enemyDistance)
+			{
+				hitMap[pos.x][pos.y][pos.z].closestTown = nullptr;
+			}
+			else if(!enemyTown || ourDistance < enemyDistance)
+			{
+				hitMap[pos.x][pos.y][pos.z].closestTown = ourTown;
+			}
+			else
+			{
+				hitMap[pos.x][pos.y][pos.z].closestTown = enemyTown;
+			}
+		});
+}
+
+const std::vector<HitMapInfo> & DangerHitMapAnalyzer::getTownTreats(const CGTownInstance * town) const
+{
+	static const std::vector<HitMapInfo> empty = {};
+
+	auto result = townTreats.find(town->id);
+
+	return result == townTreats.end() ? empty : result->second;
+}
+
+PlayerColor DangerHitMapAnalyzer::getTileOwner(const int3 & tile) const
+{
+	auto town = hitMap[tile.x][tile.y][tile.z].closestTown;
+
+	return town ? town->getOwner() : PlayerColor::NEUTRAL;
+}
+
+const CGTownInstance * DangerHitMapAnalyzer::getClosestTown(const int3 & tile) const
+{
+	return hitMap[tile.x][tile.y][tile.z].closestTown;
+}
+
 uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const
 uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath & path) const
 {
 {
 	int3 tile = path.targetTile();
 	int3 tile = path.targetTile();
@@ -144,7 +288,7 @@ const std::set<const CGObjectInstance *> & DangerHitMapAnalyzer::getOneTurnAcces
 
 
 void DangerHitMapAnalyzer::reset()
 void DangerHitMapAnalyzer::reset()
 {
 {
-	upToDate = false;
+	hitMapUpToDate = false;
 }
 }
 
 
 }
 }

+ 12 - 1
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h

@@ -35,6 +35,8 @@ struct HitMapInfo
 		turn = 255;
 		turn = 255;
 		hero = HeroPtr();
 		hero = HeroPtr();
 	}
 	}
+
+	double value() const;
 };
 };
 
 
 struct HitMapNode
 struct HitMapNode
@@ -42,6 +44,8 @@ struct HitMapNode
 	HitMapInfo maximumDanger;
 	HitMapInfo maximumDanger;
 	HitMapInfo fastestDanger;
 	HitMapInfo fastestDanger;
 
 
+	const CGTownInstance * closestTown = nullptr;
+
 	HitMapNode() = default;
 	HitMapNode() = default;
 
 
 	void reset()
 	void reset()
@@ -56,18 +60,25 @@ class DangerHitMapAnalyzer
 private:
 private:
 	boost::multi_array<HitMapNode, 3> hitMap;
 	boost::multi_array<HitMapNode, 3> hitMap;
 	std::map<const CGHeroInstance *, std::set<const CGObjectInstance *>> enemyHeroAccessibleObjects;
 	std::map<const CGHeroInstance *, std::set<const CGObjectInstance *>> enemyHeroAccessibleObjects;
-	bool upToDate;
+	bool hitMapUpToDate = false;
+	bool tileOwnersUpToDate = false;
 	const Nullkiller * ai;
 	const Nullkiller * ai;
+	std::map<ObjectInstanceID, std::vector<HitMapInfo>> townTreats;
 
 
 public:
 public:
 	DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {}
 	DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {}
 
 
 	void updateHitMap();
 	void updateHitMap();
+	void calculateTileOwners();
 	uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const;
 	uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const;
 	const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const;
 	const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const;
 	const HitMapNode & getTileTreat(const int3 & tile) const;
 	const HitMapNode & getTileTreat(const int3 & tile) const;
 	const std::set<const CGObjectInstance *> & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const;
 	const std::set<const CGObjectInstance *> & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const;
 	void reset();
 	void reset();
+	void resetTileOwners() { tileOwnersUpToDate = false; }
+	PlayerColor getTileOwner(const int3 & tile) const;
+	const CGTownInstance * getClosestTown(const int3 & tile) const;
+	const std::vector<HitMapInfo> & getTownTreats(const CGTownInstance * town) const;
 };
 };
 
 
 }
 }

+ 36 - 7
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -125,6 +125,7 @@ void HeroManager::update()
 	}
 	}
 
 
 	std::sort(myHeroes.begin(), myHeroes.end(), scoreSort);
 	std::sort(myHeroes.begin(), myHeroes.end(), scoreSort);
+	heroRoles.clear();
 
 
 	for(auto hero : myHeroes)
 	for(auto hero : myHeroes)
 	{
 	{
@@ -180,6 +181,15 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
 	return evaluateFightingStrength(hero);
 	return evaluateFightingStrength(hero);
 }
 }
 
 
+bool HeroManager::heroCapReached() const
+{
+	const bool includeGarnisoned = true;
+	int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
+
+	return heroCount >= ALLOWED_ROAMING_HEROES
+		|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
+}
+
 bool HeroManager::canRecruitHero(const CGTownInstance * town) const
 bool HeroManager::canRecruitHero(const CGTownInstance * town) const
 {
 {
 	if(!town)
 	if(!town)
@@ -191,13 +201,7 @@ bool HeroManager::canRecruitHero(const CGTownInstance * town) const
 	if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST)
 	if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST)
 		return false;
 		return false;
 
 
-	const bool includeGarnisoned = true;
-	int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
-
-	if(heroCount >= ALLOWED_ROAMING_HEROES)
-		return false;
-
-	if(heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
+	if(heroCapReached())
 		return false;
 		return false;
 
 
 	if(!cb->getAvailableHeroes(town).size())
 	if(!cb->getAvailableHeroes(town).size())
@@ -225,6 +229,31 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const
 	return nullptr;
 	return nullptr;
 }
 }
 
 
+const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const
+{
+	const CGHeroInstance * weakestHero = nullptr;
+	auto myHeroes = ai->cb->getHeroesInfo();
+
+	for(auto existingHero : myHeroes)
+	{
+		if(ai->getHeroLockedReason(existingHero) == HeroLockedReason::DEFENCE
+			|| existingHero->getArmyStrength() >armyLimit
+			|| getHeroRole(existingHero) == HeroRole::MAIN
+			|| existingHero->movementPointsRemaining()
+			|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
+		{
+			continue;
+		}
+
+		if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
+		{
+			weakestHero = existingHero;
+		}
+	}
+
+	return weakestHero;
+}
+
 SecondarySkillScoreMap::SecondarySkillScoreMap(std::map<SecondarySkill, float> scoreMap)
 SecondarySkillScoreMap::SecondarySkillScoreMap(std::map<SecondarySkill, float> scoreMap)
 	:scoreMap(scoreMap)
 	:scoreMap(scoreMap)
 {
 {

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

@@ -31,7 +31,9 @@ public:
 	virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
 	virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
 	virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
 	virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
 	virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0;
 	virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0;
+	virtual bool heroCapReached() const = 0;
 	virtual const CGHeroInstance * findHeroWithGrail() const = 0;
 	virtual const CGHeroInstance * findHeroWithGrail() const = 0;
+	virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0;
 };
 };
 
 
 class DLL_EXPORT ISecondarySkillRule
 class DLL_EXPORT ISecondarySkillRule
@@ -71,7 +73,9 @@ public:
 	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
 	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
 	float evaluateHero(const CGHeroInstance * hero) const override;
 	float evaluateHero(const CGHeroInstance * hero) const override;
 	bool canRecruitHero(const CGTownInstance * t = nullptr) const override;
 	bool canRecruitHero(const CGTownInstance * t = nullptr) const override;
+	bool heroCapReached() const override;
 	const CGHeroInstance * findHeroWithGrail() const override;
 	const CGHeroInstance * findHeroWithGrail() const override;
+	const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override;
 
 
 private:
 private:
 	float evaluateFightingStrength(const CGHeroInstance * hero) const;
 	float evaluateFightingStrength(const CGHeroInstance * hero) const;

+ 6 - 1
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -227,7 +227,12 @@ void ObjectClusterizer::clusterize()
 			auto obj = objs[i];
 			auto obj = objs[i];
 
 
 			if(!shouldVisitObject(obj))
 			if(!shouldVisitObject(obj))
-				return;
+			{
+#if NKAI_TRACE_LEVEL >= 2
+				logAi->trace("Skip object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
+#endif
+				continue;
+			}
 
 
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());
 			logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());

+ 10 - 6
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -56,7 +56,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 
 
 	tasks.reserve(paths.size());
 	tasks.reserve(paths.size());
 
 
-	const AIPath * closestWay = nullptr;
+	std::unordered_map<HeroRole, const AIPath *> closestWaysByRole;
 	std::vector<ExecuteHeroChain *> waysToVisitObj;
 	std::vector<ExecuteHeroChain *> waysToVisitObj;
 
 
 	for(auto & path : paths)
 	for(auto & path : paths)
@@ -128,8 +128,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 
 
 			auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
 			auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
 
 
-			if(heroRole == HeroRole::SCOUT
-				&& (!closestWay || closestWay->movementCost() > path.movementCost()))
+			auto & closestWay = closestWaysByRole[heroRole];
+
+			if(!closestWay || closestWay->movementCost() > path.movementCost())
 			{
 			{
 				closestWay = &path;
 				closestWay = &path;
 			}
 			}
@@ -142,9 +143,12 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 		}
 		}
 	}
 	}
 
 
-	if(closestWay)
+	for(auto way : waysToVisitObj)
 	{
 	{
-		for(auto way : waysToVisitObj)
+		auto heroRole = ai->nullkiller->heroManager->getHeroRole(way->getPath().targetHero);
+		auto closestWay = closestWaysByRole[heroRole];
+
+		if(closestWay)
 		{
 		{
 			way->closestWayRatio
 			way->closestWayRatio
 				= closestWay->movementCost() / way->getPath().movementCost();
 				= closestWay->movementCost() / way->getPath().movementCost();
@@ -209,7 +213,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 	{
 	{
 		captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects());
 		captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects());
 
 
-		if(tasks.empty() || ai->nullkiller->getScanDepth() == ScanDepth::FULL)
+		if(tasks.empty() || ai->nullkiller->getScanDepth() != ScanDepth::SMALL)
 			captureObjects(ai->nullkiller->objectClusterizer->getFarObjects());
 			captureObjects(ai->nullkiller->objectClusterizer->getFarObjects());
 	}
 	}
 
 

+ 236 - 115
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -49,37 +49,119 @@ Goals::TGoalVec DefenceBehavior::decompose() const
 	return tasks;
 	return tasks;
 }
 }
 
 
-void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const
+bool isTreatUnderControl(const CGTownInstance * town, const HitMapInfo & treat, const std::vector<AIPath> & paths)
 {
 {
-	logAi->trace("Evaluating defence for %s", town->getNameTranslated());
-
-	auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
-	auto treats = { treatNode.maximumDanger, treatNode.fastestDanger };
-
 	int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
 	int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
 
 
-	if(town->garrisonHero)
+	for(const AIPath & path : paths)
 	{
 	{
-		if(!ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
+		bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO;
+		bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7;
+
+		if(treatIsWeak && !needToSaveGrowth)
 		{
 		{
-			if(!town->visitingHero && cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
+			if((path.exchangeCount == 1 && path.turn() < treat.turn)
+				|| path.turn() < treat.turn - 1
+				|| (path.turn() < treat.turn && treat.turn >= 2))
 			{
 			{
+#if NKAI_TRACE_LEVEL >= 1
 				logAi->trace(
 				logAi->trace(
-					"Extracting hero %s from garrison of town %s",
-					town->garrisonHero->getNameTranslated(),
-					town->getNameTranslated());
-
-				tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
+					"Hero %s can eliminate danger for town %s using path %s.",
+					path.targetHero->getObjectName(),
+					town->getObjectName(),
+					path.toString());
+#endif
 
 
-				return;
+				return true;
 			}
 			}
 		}
 		}
+	}
+
+	return false;
+}
+
+void handleCounterAttack(
+	const CGTownInstance * town,
+	const HitMapInfo & treat,
+	const HitMapInfo & maximumDanger,
+	Goals::TGoalVec & tasks)
+{
+	if(treat.hero.validAndSet()
+		&& treat.turn <= 1
+		&& (treat.danger == maximumDanger.danger || treat.turn < maximumDanger.turn))
+	{
+		auto heroCapturingPaths = ai->nullkiller->pathfinder->getPathInfo(treat.hero->visitablePos());
+		auto goals = CaptureObjectsBehavior::getVisitGoals(heroCapturingPaths, treat.hero.get());
+
+		for(int i = 0; i < heroCapturingPaths.size(); i++)
+		{
+			AIPath & path = heroCapturingPaths[i];
+			TSubgoal goal = goals[i];
+
+			if(!goal || goal->invalid() || !goal->isElementar()) continue;
+
+			Composition composition;
+
+			composition.addNext(DefendTown(town, treat, path, true)).addNext(goal);
+
+			tasks.push_back(Goals::sptr(composition));
+		}
+	}
+}
 
 
+bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoalVec & tasks)
+{
+	if(ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
+	{
 		logAi->trace(
 		logAi->trace(
 			"Hero %s in garrison of town %s is suposed to defend the town",
 			"Hero %s in garrison of town %s is suposed to defend the town",
 			town->garrisonHero->getNameTranslated(),
 			town->garrisonHero->getNameTranslated(),
 			town->getNameTranslated());
 			town->getNameTranslated());
 
 
+		return true;
+	}
+
+	if(!town->visitingHero)
+	{
+		if(cb->getHeroCount(ai->playerID, false) < GameConstants::MAX_HEROES_PER_PLAYER)
+		{
+			logAi->trace(
+				"Extracting hero %s from garrison of town %s",
+				town->garrisonHero->getNameTranslated(),
+				town->getNameTranslated());
+
+			tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
+
+			return true;
+		}
+		else if(ai->nullkiller->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
+		{
+			auto armyDismissLimit = 1000;
+			auto heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(armyDismissLimit);
+
+			if(heroToDismiss)
+			{
+				tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
+
+				return true;
+			}
+		}
+	}
+
+	return false;
+}
+
+void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const
+{
+	logAi->trace("Evaluating defence for %s", town->getNameTranslated());
+
+	auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
+	std::vector<HitMapInfo> treats = ai->nullkiller->dangerHitMap->getTownTreats(town);
+	
+	treats.push_back(treatNode.fastestDanger); // no guarantee that fastest danger will be there
+
+	if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks))
+	{
 		return;
 		return;
 	}
 	}
 
 
@@ -109,103 +191,15 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			std::to_string(treat.turn),
 			std::to_string(treat.turn),
 			treat.hero->getNameTranslated());
 			treat.hero->getNameTranslated());
 
 
-		bool treatIsUnderControl = false;
+		handleCounterAttack(town, treat, treatNode.maximumDanger, tasks);
 
 
-		for(AIPath & path : paths)
+		if(isTreatUnderControl(town, treat, paths))
 		{
 		{
-			if(town->visitingHero && path.targetHero != town->visitingHero.get())
-				continue;
-
-			if(town->visitingHero && path.getHeroStrength() < town->visitingHero->getHeroStrength())
-				continue;
-
-			if(treat.hero.validAndSet()
-				&& treat.turn <= 1
-				&& (treat.danger == treatNode.maximumDanger.danger || treat.turn < treatNode.maximumDanger.turn)
-				&& isSafeToVisit(path.targetHero, path.heroArmy, treat.danger))
-			{
-				Composition composition;
-
-				composition.addNext(DefendTown(town, treat, path)).addNext(CaptureObject(treat.hero.get()));
-
-				tasks.push_back(Goals::sptr(composition));
-			}
-
-			bool treatIsWeak = path.getHeroStrength() / (float)treat.danger > TREAT_IGNORE_RATIO;
-			bool needToSaveGrowth = treat.turn == 0 && dayOfWeek == 7;
-
-			if(treatIsWeak && !needToSaveGrowth)
-			{
-				if((path.exchangeCount == 1 && path.turn() < treat.turn)
-					|| path.turn() < treat.turn - 1
-					|| (path.turn() < treat.turn && treat.turn >= 2))
-				{
-#if NKAI_TRACE_LEVEL >= 1
-					logAi->trace(
-						"Hero %s can eliminate danger for town %s using path %s.",
-						path.targetHero->getObjectName(),
-						town->getObjectName(),
-						path.toString());
-#endif
-
-					treatIsUnderControl = true;
-
-					break;
-				}
-			}
-		}
-
-		if(treatIsUnderControl)
 			continue;
 			continue;
-
-		if(!town->visitingHero
-			&& town->hasBuilt(BuildingID::TAVERN)
-			&& cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
-		{
-			auto heroesInTavern = cb->getAvailableHeroes(town);
-
-			for(auto hero : heroesInTavern)
-			{
-				if(hero->getTotalStrength() > treat.danger)
-				{
-					auto myHeroes = cb->getHeroesInfo();
-
-					if(cb->getHeroesInfo().size() < ALLOWED_ROAMING_HEROES)
-					{
-#if NKAI_TRACE_LEVEL >= 1
-						logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName());
-#endif
-						tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(1)));
-						continue;
-					}
-					else
-					{
-						const CGHeroInstance * weakestHero = nullptr;
-
-						for(auto existingHero : myHeroes)
-						{
-							if(ai->nullkiller->isHeroLocked(existingHero)
-								|| existingHero->getArmyStrength() > hero->getArmyStrength()
-								|| ai->nullkiller->heroManager->getHeroRole(existingHero) == HeroRole::MAIN
-								|| existingHero->movementPointsRemaining()
-								|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
-								continue;
-
-							if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
-							{
-								weakestHero = existingHero;
-							}
-
-							if(weakestHero)
-							{
-								tasks.push_back(Goals::sptr(Goals::DismissHero(weakestHero)));
-							}
-						}
-					}
-				}
-			}
 		}
 		}
 
 
+		evaluateRecruitingHero(tasks, treat, town);
+
 		if(paths.empty())
 		if(paths.empty())
 		{
 		{
 			logAi->trace("No ways to defend town %s", town->getNameTranslated());
 			logAi->trace("No ways to defend town %s", town->getNameTranslated());
@@ -229,6 +223,22 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				path.movementCost(),
 				path.movementCost(),
 				path.toString());
 				path.toString());
 #endif
 #endif
+
+			auto townDefenseStrength = town->garrisonHero
+				? town->garrisonHero->getTotalStrength()
+				: (town->visitingHero ? town->visitingHero->getTotalStrength() : town->getUpperArmy()->getArmyStrength());
+
+			if(town->visitingHero && path.targetHero == town->visitingHero.get())
+			{
+				if(path.getHeroStrength() < townDefenseStrength)
+					continue;
+			}
+			else if(town->garrisonHero && path.targetHero == town->garrisonHero.get())
+			{
+				if(path.getHeroStrength() < townDefenseStrength)
+					continue;
+			}
+
 			if(path.turn() <= treat.turn - 2)
 			if(path.turn() <= treat.turn - 2)
 			{
 			{
 #if NKAI_TRACE_LEVEL >= 1
 #if NKAI_TRACE_LEVEL >= 1
@@ -275,9 +285,11 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				tasks.push_back(
 				tasks.push_back(
 					Goals::sptr(Composition()
 					Goals::sptr(Composition()
 						.addNext(DefendTown(town, treat, path))
 						.addNext(DefendTown(town, treat, path))
-						.addNext(ExchangeSwapTownHeroes(town, town->visitingHero.get()))
-						.addNext(ExecuteHeroChain(path, town))
-						.addNext(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE))));
+						.addNextSequence({
+								sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())),
+								sptr(ExecuteHeroChain(path, town)),
+								sptr(ExchangeSwapTownHeroes(town, path.targetHero, HeroLockedReason::DEFENCE))
+							})));
 
 
 				continue;
 				continue;
 			}
 			}
@@ -313,15 +325,58 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 					continue;
 					continue;
 				}
 				}
 			}
 			}
+			Composition composition;
+
+			composition.addNext(DefendTown(town, treat, path));
+			TGoalVec sequence;
+
+			if(town->garrisonHero && path.targetHero == town->garrisonHero.get() && path.exchangeCount == 1)
+			{
+				composition.addNext(ExchangeSwapTownHeroes(town, town->garrisonHero.get(), HeroLockedReason::DEFENCE));
+				tasks.push_back(Goals::sptr(composition));
 
 
 #if NKAI_TRACE_LEVEL >= 1
 #if NKAI_TRACE_LEVEL >= 1
-			logAi->trace("Move %s to defend town %s",
-				path.targetHero->getObjectName(),
-				town->getObjectName());
+				logAi->trace("Locking hero %s in garrison of %s",
+					town->garrisonHero.get()->getObjectName(),
+					town->getObjectName());
 #endif
 #endif
-			Composition composition;
 
 
-			composition.addNext(DefendTown(town, treat, path)).addNext(ExecuteHeroChain(path, town));
+				continue;
+			}
+			else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
+			{
+				if(town->garrisonHero)
+				{
+					if(ai->nullkiller->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT
+						&& town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20)
+					{
+						if(path.turn() == 0)
+							sequence.push_back(sptr(DismissHero(town->visitingHero.get())));
+					}
+					else
+					{
+#if NKAI_TRACE_LEVEL >= 1
+						logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
+							path.targetHero->getObjectName(),
+							town->getObjectName());
+#endif
+						continue;
+					}
+				}
+				else if(path.turn() == 0)
+				{
+					sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())));
+				}
+			}
+
+#if NKAI_TRACE_LEVEL >= 1
+				logAi->trace("Move %s to defend town %s",
+					path.targetHero->getObjectName(),
+					town->getObjectName());
+#endif
+
+			sequence.push_back(sptr(ExecuteHeroChain(path, town)));
+			composition.addNextSequence(sequence);
 
 
 			auto firstBlockedAction = path.getFirstBlockedAction();
 			auto firstBlockedAction = path.getFirstBlockedAction();
 			if(firstBlockedAction)
 			if(firstBlockedAction)
@@ -350,4 +405,70 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	logAi->debug("Found %d tasks", tasks.size());
 	logAi->debug("Found %d tasks", tasks.size());
 }
 }
 
 
+void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const
+{
+	if(town->hasBuilt(BuildingID::TAVERN)
+		&& cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
+	{
+		auto heroesInTavern = cb->getAvailableHeroes(town);
+
+		for(auto hero : heroesInTavern)
+		{
+			if(hero->getTotalStrength() < treat.danger)
+				continue;
+
+			auto myHeroes = cb->getHeroesInfo();
+
+#if NKAI_TRACE_LEVEL >= 1
+			logAi->trace("Hero %s can be recruited to defend %s", hero->getObjectName(), town->getObjectName());
+#endif
+			bool needSwap = false;
+			const CGHeroInstance * heroToDismiss = nullptr;
+
+			if(town->visitingHero)
+			{
+				if(!town->garrisonHero)
+					needSwap = true;
+				else
+				{
+					if(town->visitingHero->getArmyStrength() < town->garrisonHero->getArmyStrength())
+					{
+						if(town->visitingHero->getArmyStrength() >= hero->getArmyStrength())
+							continue;
+
+						heroToDismiss = town->visitingHero.get();
+					}
+					else if(town->garrisonHero->getArmyStrength() >= hero->getArmyStrength())
+						continue;
+					else
+					{
+						needSwap = true;
+						heroToDismiss = town->garrisonHero.get();
+					}
+				}
+			}
+			else if(ai->nullkiller->heroManager->heroCapReached())
+			{
+				heroToDismiss = ai->nullkiller->heroManager->findWeakHeroToDismiss(hero->getArmyStrength());
+
+				if(!heroToDismiss)
+					continue;
+			}
+
+			TGoalVec sequence;
+			Goals::Composition recruitHeroComposition;
+
+			if(needSwap)
+				sequence.push_back(sptr(ExchangeSwapTownHeroes(town, town->visitingHero.get())));
+
+			if(heroToDismiss)
+				sequence.push_back(sptr(DismissHero(heroToDismiss)));
+
+			sequence.push_back(sptr(Goals::RecruitHero(town, hero)));
+
+			tasks.push_back(sptr(Goals::Composition().addNext(DefendTown(town, treat, hero)).addNextSequence(sequence)));
+		}
+	}
+}
+
 }
 }

+ 5 - 0
AI/Nullkiller/Behaviors/DefenceBehavior.h

@@ -15,8 +15,12 @@
 
 
 namespace NKAI
 namespace NKAI
 {
 {
+
+struct HitMapInfo;
+
 namespace Goals
 namespace Goals
 {
 {
+
 	class DefenceBehavior : public CGoal<DefenceBehavior>
 	class DefenceBehavior : public CGoal<DefenceBehavior>
 	{
 	{
 	public:
 	public:
@@ -35,6 +39,7 @@ namespace Goals
 
 
 	private:
 	private:
 		void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const;
 		void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const;
+		void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const;
 	};
 	};
 }
 }
 
 

+ 105 - 20
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

@@ -12,10 +12,13 @@
 #include "../Engine/Nullkiller.h"
 #include "../Engine/Nullkiller.h"
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/Composition.h"
 #include "../Goals/Composition.h"
+#include "../Goals/RecruitHero.h"
 #include "../Markers/HeroExchange.h"
 #include "../Markers/HeroExchange.h"
 #include "../Markers/ArmyUpgrade.h"
 #include "../Markers/ArmyUpgrade.h"
 #include "GatherArmyBehavior.h"
 #include "GatherArmyBehavior.h"
+#include "CaptureObjectsBehavior.h"
 #include "../AIUtility.h"
 #include "../AIUtility.h"
+#include "../Goals/ExchangeSwapTownHeroes.h"
 
 
 namespace NKAI
 namespace NKAI
 {
 {
@@ -78,20 +81,27 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 	for(const AIPath & path : paths)
 	for(const AIPath & path : paths)
 	{
 	{
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
-		logAi->trace("Path found %s", path.toString());
+		logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
 #endif
 #endif
 		
 		
-		if(path.containsHero(hero)) continue;
+		if(path.containsHero(hero))
+		{
+#if NKAI_TRACE_LEVEL >= 2
+			logAi->trace("Selfcontaining path. Ignore");
+#endif
+			continue;
+		}
+
+		bool garrisoned = false;
 
 
 		if(path.turn() == 0 && hero->inTownGarrison)
 		if(path.turn() == 0 && hero->inTownGarrison)
 		{
 		{
 #if NKAI_TRACE_LEVEL >= 1
 #if NKAI_TRACE_LEVEL >= 1
-			logAi->trace("Skipping garnisoned hero %s, %s", hero->getObjectName(), pos.toString());
+			garrisoned = true;
 #endif
 #endif
-			continue;
 		}
 		}
 
 
-		if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
+		if(path.turn() > 0 && ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
 		{
 		{
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
 			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
@@ -109,10 +119,11 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 
 
 		HeroExchange heroExchange(hero, path);
 		HeroExchange heroExchange(hero, path);
 
 
-		float armyValue = (float)heroExchange.getReinforcementArmyStrength() / hero->getArmyStrength();
+		uint64_t armyValue = heroExchange.getReinforcementArmyStrength();
+		float armyRatio = (float)armyValue / hero->getArmyStrength();
 
 
 		// avoid transferring very small amount of army
 		// avoid transferring very small amount of army
-		if(armyValue < 0.1f && armyValue < 20000)
+		if((armyRatio < 0.1f && armyValue < 20000) || armyValue < 500)
 		{
 		{
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Army value is too small.");
 			logAi->trace("Army value is too small.");
@@ -172,7 +183,21 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 			exchangePath.closestWayRatio = 1;
 			exchangePath.closestWayRatio = 1;
 
 
 			composition.addNext(heroExchange);
 			composition.addNext(heroExchange);
-			composition.addNext(exchangePath);
+
+			if(garrisoned && path.turn() == 0)
+			{
+				auto lockReason = ai->nullkiller->getHeroLockedReason(hero);
+
+				composition.addNextSequence({
+					sptr(ExchangeSwapTownHeroes(hero->visitedTown)),
+					sptr(exchangePath),
+					sptr(ExchangeSwapTownHeroes(hero->visitedTown, hero, lockReason))
+				});
+			}
+			else
+			{
+				composition.addNext(exchangePath);
+			}
 
 
 			auto blockedAction = path.getFirstBlockedAction();
 			auto blockedAction = path.getFirstBlockedAction();
 
 
@@ -212,18 +237,42 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 #endif
 #endif
 	
 	
 	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
 	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
+	auto goals = CaptureObjectsBehavior::getVisitGoals(paths);
+
 	std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
 	std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
 
 
 #if NKAI_TRACE_LEVEL >= 1
 #if NKAI_TRACE_LEVEL >= 1
 	logAi->trace("Found %d paths", paths.size());
 	logAi->trace("Found %d paths", paths.size());
 #endif
 #endif
 
 
+	bool hasMainAround = false;
+
 	for(const AIPath & path : paths)
 	for(const AIPath & path : paths)
 	{
 	{
+		auto heroRole = ai->nullkiller->heroManager->getHeroRole(path.targetHero);
+
+		if(heroRole == HeroRole::MAIN && path.turn() < SCOUT_TURN_DISTANCE_LIMIT)
+			hasMainAround = true;
+	}
+
+	for(int i = 0; i < paths.size(); i++)
+	{
+		auto & path = paths[i];
+		auto visitGoal = goals[i];
+
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
-		logAi->trace("Path found %s", path.toString());
+		logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
 #endif
 #endif
-		if(upgrader->visitingHero && upgrader->visitingHero.get() != path.targetHero)
+
+		if(visitGoal->invalid())
+		{
+#if NKAI_TRACE_LEVEL >= 2
+			logAi->trace("Ignore path. Not valid way.");
+#endif
+			continue;
+		}
+
+		if(upgrader->visitingHero && (upgrader->visitingHero.get() != path.targetHero || path.exchangeCount == 1))
 		{
 		{
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Ignore path. Town has visiting hero.");
 			logAi->trace("Ignore path. Town has visiting hero.");
@@ -261,18 +310,58 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 
 
 		auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
 		auto upgrade = ai->nullkiller->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
 
 
-		if(!upgrader->garrisonHero && ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN)
+		if(!upgrader->garrisonHero
+			&& (
+				hasMainAround
+				|| ai->nullkiller->heroManager->getHeroRole(path.targetHero) == HeroRole::MAIN))
 		{
 		{
-			upgrade.upgradeValue +=	
-				ai->nullkiller->armyManager->howManyReinforcementsCanGet(
+			ArmyUpgradeInfo armyToGetOrBuy;
+
+			armyToGetOrBuy.addArmyToGet(
+				ai->nullkiller->armyManager->getBestArmy(
 					path.targetHero,
 					path.targetHero,
 					path.heroArmy,
 					path.heroArmy,
-					upgrader->getUpperArmy());	
+					upgrader->getUpperArmy()));
+
+			armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
+
+			armyToGetOrBuy.addArmyToBuy(
+				ai->nullkiller->armyManager->toSlotInfo(
+					ai->nullkiller->armyManager->getArmyAvailableToBuy(
+						path.heroArmy,
+						upgrader,
+						ai->nullkiller->getFreeResources(),
+						path.turn())));
+
+			upgrade.upgradeValue += armyToGetOrBuy.upgradeValue;
+			upgrade.upgradeCost += armyToGetOrBuy.upgradeCost;
+			vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy);
+
+			if(!upgrade.upgradeValue
+				&& armyToGetOrBuy.upgradeValue > 20000
+				&& ai->nullkiller->heroManager->canRecruitHero(town)
+				&& path.turn() < SCOUT_TURN_DISTANCE_LIMIT)
+			{
+				for(auto hero : cb->getAvailableHeroes(town))
+				{
+					auto scoutReinforcement =  ai->nullkiller->armyManager->howManyReinforcementsCanBuy(hero, town)
+						+ ai->nullkiller->armyManager->howManyReinforcementsCanGet(hero, town);
+
+					if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
+						&& ai->nullkiller->getFreeGold() >20000
+						&& ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE)
+					{
+						Composition recruitHero;
+
+						recruitHero.addNext(ArmyUpgrade(path.targetHero, town, armyToGetOrBuy)).addNext(RecruitHero(town, hero));
+					}
+				}
+			}
 		}
 		}
 
 
 		auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength();
 		auto armyValue = (float)upgrade.upgradeValue / path.getHeroStrength();
 
 
-		if((armyValue < 0.1f && armyValue < 20000) || upgrade.upgradeValue < 300) // avoid small upgrades
+		if((armyValue < 0.25f && upgrade.upgradeValue < 40000) || upgrade.upgradeValue < 2000) // avoid small upgrades
 		{
 		{
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Ignore path. Army value is too small (%f)", armyValue);
 			logAi->trace("Ignore path. Army value is too small (%f)", armyValue);
@@ -297,11 +386,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 
 
 		if(isSafe)
 		if(isSafe)
 		{
 		{
-			ExecuteHeroChain newWay(path, upgrader);
-			
-			newWay.closestWayRatio = 1;
-
-			tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(newWay)));
+			tasks.push_back(sptr(Composition().addNext(ArmyUpgrade(path, upgrader, upgrade)).addNext(visitGoal)));
 		}
 		}
 	}
 	}
 
 

+ 21 - 0
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

@@ -66,6 +66,27 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const
 				}
 				}
 			}
 			}
 
 
+			int treasureSourcesCount = 0;
+
+			for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects())
+			{
+				if((obj->ID == Obj::RESOURCE)
+					|| obj->ID == Obj::TREASURE_CHEST
+					|| obj->ID == Obj::CAMPFIRE
+					|| isWeeklyRevisitable(obj)
+					|| obj->ID ==Obj::ARTIFACT)
+				{
+					auto tile = obj->visitablePos();
+					auto closestTown = ai->nullkiller->dangerHitMap->getClosestTown(tile);
+
+					if(town == closestTown)
+						treasureSourcesCount++;
+				}
+			}
+
+			if(treasureSourcesCount < 5)
+				continue;
+
 			if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1
 			if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1
 				|| (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000
 				|| (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000
 					&& ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE))
 					&& ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE))

+ 2 - 0
AI/Nullkiller/CMakeLists.txt

@@ -52,6 +52,7 @@ set(Nullkiller_SRCS
 		Behaviors/BuildingBehavior.cpp
 		Behaviors/BuildingBehavior.cpp
 		Behaviors/GatherArmyBehavior.cpp
 		Behaviors/GatherArmyBehavior.cpp
 		Behaviors/ClusterBehavior.cpp
 		Behaviors/ClusterBehavior.cpp
+		Helpers/ArmyFormation.cpp
 		AIGateway.cpp
 		AIGateway.cpp
 )
 )
 
 
@@ -114,6 +115,7 @@ set(Nullkiller_HEADERS
 		Behaviors/BuildingBehavior.h
 		Behaviors/BuildingBehavior.h
 		Behaviors/GatherArmyBehavior.h
 		Behaviors/GatherArmyBehavior.h
 		Behaviors/ClusterBehavior.h
 		Behaviors/ClusterBehavior.h
+		Helpers/ArmyFormation.h
 		AIGateway.h
 		AIGateway.h
 )
 )
 
 

+ 4 - 6
AI/Nullkiller/Engine/FuzzyHelper.cpp

@@ -150,17 +150,15 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 	case Obj::MINE:
 	case Obj::MINE:
 	case Obj::ABANDONED_MINE:
 	case Obj::ABANDONED_MINE:
 	case Obj::PANDORAS_BOX:
 	case Obj::PANDORAS_BOX:
-	{
-		const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
-		return a->getArmyStrength();
-	}
 	case Obj::CRYPT: //crypt
 	case Obj::CRYPT: //crypt
 	case Obj::CREATURE_BANK: //crebank
 	case Obj::CREATURE_BANK: //crebank
 	case Obj::DRAGON_UTOPIA:
 	case Obj::DRAGON_UTOPIA:
 	case Obj::SHIPWRECK: //shipwreck
 	case Obj::SHIPWRECK: //shipwreck
 	case Obj::DERELICT_SHIP: //derelict ship
 	case Obj::DERELICT_SHIP: //derelict ship
-							 //	case Obj::PYRAMID:
-		return estimateBankDanger(dynamic_cast<const CBank *>(obj));
+	{
+		const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
+		return a->getArmyStrength();
+	}
 	case Obj::PYRAMID:
 	case Obj::PYRAMID:
 	{
 	{
 		if(obj->subID == 0)
 		if(obj->subID == 0)

+ 42 - 13
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -61,6 +61,7 @@ void Nullkiller::init(std::shared_ptr<CCallback> cb, PlayerColor playerID)
 	armyManager.reset(new ArmyManager(cb.get(), this));
 	armyManager.reset(new ArmyManager(cb.get(), this));
 	heroManager.reset(new HeroManager(cb.get(), this));
 	heroManager.reset(new HeroManager(cb.get(), this));
 	decomposer.reset(new DeepDecomposer());
 	decomposer.reset(new DeepDecomposer());
+	armyFormation.reset(new ArmyFormation(cb, this));
 }
 }
 
 
 Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
 Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
@@ -117,7 +118,7 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi
 void Nullkiller::resetAiState()
 void Nullkiller::resetAiState()
 {
 {
 	lockedResources = TResources();
 	lockedResources = TResources();
-	scanDepth = ScanDepth::FULL;
+	scanDepth = ScanDepth::MAIN_FULL;
 	playerID = ai->playerID;
 	playerID = ai->playerID;
 	lockedHeroes.clear();
 	lockedHeroes.clear();
 	dangerHitMap->reset();
 	dangerHitMap->reset();
@@ -133,10 +134,14 @@ void Nullkiller::updateAiState(int pass, bool fast)
 	activeHero = nullptr;
 	activeHero = nullptr;
 	setTargetObject(-1);
 	setTargetObject(-1);
 
 
+	decomposer->reset();
+	buildAnalyzer->update();
+
 	if(!fast)
 	if(!fast)
 	{
 	{
 		memory->removeInvisibleObjects(cb.get());
 		memory->removeInvisibleObjects(cb.get());
 
 
+		dangerHitMap->calculateTileOwners();
 		dangerHitMap->updateHitMap();
 		dangerHitMap->updateHitMap();
 
 
 		boost::this_thread::interruption_point();
 		boost::this_thread::interruption_point();
@@ -156,11 +161,15 @@ void Nullkiller::updateAiState(int pass, bool fast)
 
 
 		PathfinderSettings cfg;
 		PathfinderSettings cfg;
 		cfg.useHeroChain = useHeroChain;
 		cfg.useHeroChain = useHeroChain;
-		cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT;
 
 
-		if(scanDepth != ScanDepth::FULL)
+		if(scanDepth == ScanDepth::SMALL)
+		{
+			cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT;
+		}
+
+		if(scanDepth != ScanDepth::ALL_FULL)
 		{
 		{
-			cfg.mainTurnDistanceLimit = MAIN_TURN_DISTANCE_LIMIT * ((int)scanDepth + 1);
+			cfg.scoutTurnDistanceLimit = SCOUT_TURN_DISTANCE_LIMIT;
 		}
 		}
 
 
 		boost::this_thread::interruption_point();
 		boost::this_thread::interruption_point();
@@ -173,8 +182,6 @@ void Nullkiller::updateAiState(int pass, bool fast)
 	}
 	}
 
 
 	armyManager->update();
 	armyManager->update();
-	buildAnalyzer->update();
-	decomposer->reset();
 
 
 	logAi->debug("AI state updated in %ld", timeElapsed(start));
 	logAi->debug("AI state updated in %ld", timeElapsed(start));
 }
 }
@@ -222,7 +229,7 @@ void Nullkiller::makeTurn()
 	boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
 	boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
 
 
 	const int MAX_DEPTH = 10;
 	const int MAX_DEPTH = 10;
-	const float FAST_TASK_MINIMAL_PRIORITY = 0.7;
+	const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
 
 
 	resetAiState();
 	resetAiState();
 
 
@@ -231,8 +238,8 @@ void Nullkiller::makeTurn()
 		updateAiState(i);
 		updateAiState(i);
 
 
 		Goals::TTask bestTask = taskptr(Goals::Invalid());
 		Goals::TTask bestTask = taskptr(Goals::Invalid());
-		
-		do
+
+		for(;i <= MAXPASS; i++)
 		{
 		{
 			Goals::TTaskVec fastTasks = {
 			Goals::TTaskVec fastTasks = {
 				choseBestTask(sptr(BuyArmyBehavior()), 1),
 				choseBestTask(sptr(BuyArmyBehavior()), 1),
@@ -246,7 +253,11 @@ void Nullkiller::makeTurn()
 				executeTask(bestTask);
 				executeTask(bestTask);
 				updateAiState(i, true);
 				updateAiState(i, true);
 			}
 			}
-		} while(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY);
+			else
+			{
+				break;
+			}
+		}
 
 
 		Goals::TTaskVec bestTasks = {
 		Goals::TTaskVec bestTasks = {
 			bestTask,
 			bestTask,
@@ -265,7 +276,6 @@ void Nullkiller::makeTurn()
 		bestTask = choseBestTask(bestTasks);
 		bestTask = choseBestTask(bestTasks);
 
 
 		HeroPtr hero = bestTask->getHero();
 		HeroPtr hero = bestTask->getHero();
-
 		HeroRole heroRole = HeroRole::MAIN;
 		HeroRole heroRole = HeroRole::MAIN;
 
 
 		if(hero.validAndSet())
 		if(hero.validAndSet())
@@ -274,20 +284,39 @@ void Nullkiller::makeTurn()
 		if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
 		if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
 			useHeroChain = false;
 			useHeroChain = false;
 
 
+		// TODO: better to check turn distance here instead of priority
 		if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
 		if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
-			&& scanDepth == ScanDepth::FULL)
+			&& scanDepth == ScanDepth::MAIN_FULL)
 		{
 		{
 			useHeroChain = false;
 			useHeroChain = false;
 			scanDepth = ScanDepth::SMALL;
 			scanDepth = ScanDepth::SMALL;
 
 
 			logAi->trace(
 			logAi->trace(
-				"Goal %s has too low priority %f so increasing scan depth",
+				"Goal %s has low priority %f so decreasing  scan depth to gain performance.",
 				bestTask->toString(),
 				bestTask->toString(),
 				bestTask->priority);
 				bestTask->priority);
 		}
 		}
 
 
 		if(bestTask->priority < MIN_PRIORITY)
 		if(bestTask->priority < MIN_PRIORITY)
 		{
 		{
+			auto heroes = cb->getHeroesInfo();
+			auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
+				{
+					return h->movementPointsRemaining() > 100;
+				});
+
+			if(hasMp && scanDepth != ScanDepth::ALL_FULL)
+			{
+				logAi->trace(
+					"Goal %s has too low priority %f so increasing scan depth to full.",
+					bestTask->toString(),
+					bestTask->priority);
+
+				scanDepth = ScanDepth::ALL_FULL;
+				useHeroChain = false;
+				continue;
+			}
+
 			logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", bestTask->toString());
 			logAi->trace("Goal %s has too low priority. It is not worth doing it. Ending turn.", bestTask->toString());
 
 
 			return;
 			return;

+ 6 - 2
AI/Nullkiller/Engine/Nullkiller.h

@@ -18,6 +18,7 @@
 #include "../Analyzers/ArmyManager.h"
 #include "../Analyzers/ArmyManager.h"
 #include "../Analyzers/HeroManager.h"
 #include "../Analyzers/HeroManager.h"
 #include "../Analyzers/ObjectClusterizer.h"
 #include "../Analyzers/ObjectClusterizer.h"
+#include "../Helpers/ArmyFormation.h"
 
 
 namespace NKAI
 namespace NKAI
 {
 {
@@ -39,9 +40,11 @@ enum class HeroLockedReason
 
 
 enum class ScanDepth
 enum class ScanDepth
 {
 {
-	FULL = 0,
+	MAIN_FULL = 0,
 
 
-	SMALL = 1
+	SMALL = 1,
+
+	ALL_FULL = 2
 };
 };
 
 
 class Nullkiller
 class Nullkiller
@@ -67,6 +70,7 @@ public:
 	std::unique_ptr<AIMemory> memory;
 	std::unique_ptr<AIMemory> memory;
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
 	std::unique_ptr<DeepDecomposer> decomposer;
 	std::unique_ptr<DeepDecomposer> decomposer;
+	std::unique_ptr<ArmyFormation> armyFormation;
 	PlayerColor playerID;
 	PlayerColor playerID;
 	std::shared_ptr<CCallback> cb;
 	std::shared_ptr<CCallback> cb;
 
 

+ 205 - 76
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -23,6 +23,7 @@
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/BuildThis.h"
 #include "../Goals/BuildThis.h"
 #include "../Goals/ExchangeSwapTownHeroes.h"
 #include "../Goals/ExchangeSwapTownHeroes.h"
+#include "../Goals/DismissHero.h"
 #include "../Markers/UnlockCluster.h"
 #include "../Markers/UnlockCluster.h"
 #include "../Markers/HeroExchange.h"
 #include "../Markers/HeroExchange.h"
 #include "../Markers/ArmyUpgrade.h"
 #include "../Markers/ArmyUpgrade.h"
@@ -33,6 +34,7 @@ namespace NKAI
 
 
 #define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter
 #define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter
 #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
 #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
+const float MIN_CRITICAL_VALUE = 2.0f;
 
 
 EvaluationContext::EvaluationContext(const Nullkiller * ai)
 EvaluationContext::EvaluationContext(const Nullkiller * ai)
 	: movementCost(0.0),
 	: movementCost(0.0),
@@ -49,10 +51,16 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
 	turn(0),
 	turn(0),
 	strategicalValue(0),
 	strategicalValue(0),
 	evaluator(ai),
 	evaluator(ai),
-	enemyHeroDangerRatio(0)
+	enemyHeroDangerRatio(0),
+	armyGrowth(0)
 {
 {
 }
 }
 
 
+void EvaluationContext::addNonCriticalStrategicalValue(float value)
+{
+	vstd::amax(strategicalValue, std::min(value, MIN_CRITICAL_VALUE));
+}
+
 PriorityEvaluator::~PriorityEvaluator()
 PriorityEvaluator::~PriorityEvaluator()
 {
 {
 	delete engine;
 	delete engine;
@@ -64,6 +72,7 @@ void PriorityEvaluator::initVisitTile()
 	std::string str = std::string((char *)file.first.get(), file.second);
 	std::string str = std::string((char *)file.first.get(), file.second);
 	engine = fl::FllImporter().fromString(str);
 	engine = fl::FllImporter().fromString(str);
 	armyLossPersentageVariable = engine->getInputVariable("armyLoss");
 	armyLossPersentageVariable = engine->getInputVariable("armyLoss");
+	armyGrowthVariable = engine->getInputVariable("armyGrowth");
 	heroRoleVariable = engine->getInputVariable("heroRole");
 	heroRoleVariable = engine->getInputVariable("heroRole");
 	dangerVariable = engine->getInputVariable("danger");
 	dangerVariable = engine->getInputVariable("danger");
 	turnVariable = engine->getInputVariable("turn");
 	turnVariable = engine->getInputVariable("turn");
@@ -99,7 +108,8 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons
 	auto town = cb->getTown(target->id);
 	auto town = cb->getTown(target->id);
 	auto fortLevel = town->fortLevel();
 	auto fortLevel = town->fortLevel();
 
 
-	if(town->hasCapitol()) return booster * 2000;
+	if(town->hasCapitol())
+		return booster * 2000;
 
 
 	// probably well developed town will have city hall
 	// probably well developed town will have city hall
 	if(fortLevel == CGTownInstance::CASTLE) return booster * 750;
 	if(fortLevel == CGTownInstance::CASTLE) return booster * 750;
@@ -153,18 +163,18 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero
 		{
 		{
 			result += (c.data.type->getAIValue() * c.data.count) * c.chance;
 			result += (c.data.type->getAIValue() * c.data.count) * c.chance;
 		}
 		}
-		else
+		/*else
 		{
 		{
 			//we will need to discard the weakest stack
 			//we will need to discard the weakest stack
 			result += (c.data.type->getAIValue() * c.data.count - weakestStackPower) * c.chance;
 			result += (c.data.type->getAIValue() * c.data.count - weakestStackPower) * c.chance;
-		}
+		}*/
 	}
 	}
 	result /= 100; //divide by total chance
 	result /= 100; //divide by total chance
 
 
 	return result;
 	return result;
 }
 }
 
 
-uint64_t getDwellingScore(CCallback * cb, const CGObjectInstance * target, bool checkGold)
+uint64_t getDwellingArmyValue(CCallback * cb, const CGObjectInstance * target, bool checkGold)
 {
 {
 	auto dwelling = dynamic_cast<const CGDwelling *>(target);
 	auto dwelling = dynamic_cast<const CGDwelling *>(target);
 	uint64_t score = 0;
 	uint64_t score = 0;
@@ -185,6 +195,27 @@ uint64_t getDwellingScore(CCallback * cb, const CGObjectInstance * target, bool
 	return score;
 	return score;
 }
 }
 
 
+uint64_t getDwellingArmyGrowth(CCallback * cb, const CGObjectInstance * target, PlayerColor myColor)
+{
+	auto dwelling = dynamic_cast<const CGDwelling *>(target);
+	uint64_t score = 0;
+
+	if(dwelling->getOwner() == myColor)
+		return 0;
+
+	for(auto & creLevel : dwelling->creatures)
+	{
+		if(creLevel.second.size())
+		{
+			auto creature = creLevel.second.back().toCreature();
+
+			score += creature->getAIValue() * creature->getGrowth();
+		}
+	}
+
+	return score;
+}
+
 int getDwellingArmyCost(const CGObjectInstance * target)
 int getDwellingArmyCost(const CGObjectInstance * target)
 {
 {
 	auto dwelling = dynamic_cast<const CGDwelling *>(target);
 	auto dwelling = dynamic_cast<const CGDwelling *>(target);
@@ -247,23 +278,13 @@ uint64_t RewardEvaluator::getArmyReward(
 {
 {
 	const float enemyArmyEliminationRewardRatio = 0.5f;
 	const float enemyArmyEliminationRewardRatio = 0.5f;
 
 
+	auto relations = ai->cb->getPlayerRelations(target->tempOwner, ai->playerID);
+
 	if(!target)
 	if(!target)
 		return 0;
 		return 0;
 
 
 	switch(target->ID)
 	switch(target->ID)
 	{
 	{
-	case Obj::TOWN:
-	{
-		auto town = dynamic_cast<const CGTownInstance *>(target);
-		auto fortLevel = town->fortLevel();
-		auto booster = isAnotherAi(town, *ai->cb) ? 1 : 2;
-
-		if(fortLevel < CGTownInstance::CITADEL)
-			return town->hasFort() ? booster * 500 : 0;
-		else
-			return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000);
-	}
-
 	case Obj::HILL_FORT:
 	case Obj::HILL_FORT:
 		return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue;
 		return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue;
 	case Obj::CREATURE_BANK:
 	case Obj::CREATURE_BANK:
@@ -272,7 +293,7 @@ uint64_t RewardEvaluator::getArmyReward(
 	case Obj::CREATURE_GENERATOR2:
 	case Obj::CREATURE_GENERATOR2:
 	case Obj::CREATURE_GENERATOR3:
 	case Obj::CREATURE_GENERATOR3:
 	case Obj::CREATURE_GENERATOR4:
 	case Obj::CREATURE_GENERATOR4:
-		return getDwellingScore(ai->cb.get(), target, checkGold);
+		return getDwellingArmyValue(ai->cb.get(), target, checkGold);
 	case Obj::CRYPT:
 	case Obj::CRYPT:
 	case Obj::SHIPWRECK:
 	case Obj::SHIPWRECK:
 	case Obj::SHIPWRECK_SURVIVOR:
 	case Obj::SHIPWRECK_SURVIVOR:
@@ -283,7 +304,7 @@ uint64_t RewardEvaluator::getArmyReward(
 	case Obj::DRAGON_UTOPIA:
 	case Obj::DRAGON_UTOPIA:
 		return 10000;
 		return 10000;
 	case Obj::HERO:
 	case Obj::HERO:
-		return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
+		return  relations == PlayerRelations::ENEMIES
 			? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength()
 			? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength()
 			: 0;
 			: 0;
 	case Obj::PANDORAS_BOX:
 	case Obj::PANDORAS_BOX:
@@ -293,6 +314,47 @@ uint64_t RewardEvaluator::getArmyReward(
 	}
 	}
 }
 }
 
 
+uint64_t RewardEvaluator::getArmyGrowth(
+	const CGObjectInstance * target,
+	const CGHeroInstance * hero,
+	const CCreatureSet * army) const
+{
+	if(!target)
+		return 0;
+
+	auto relations = ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner);
+
+	if(relations != PlayerRelations::ENEMIES)
+		return 0;
+
+	switch(target->ID)
+	{
+	case Obj::TOWN:
+	{
+		auto town = dynamic_cast<const CGTownInstance *>(target);
+		auto fortLevel = town->fortLevel();
+		auto neutral = !town->getOwner().isValidPlayer();
+		auto booster = isAnotherAi(town, *ai->cb) ||  neutral ? 1 : 2;
+
+		if(fortLevel < CGTownInstance::CITADEL)
+			return town->hasFort() ? booster * 500 : 0;
+		else
+			return booster * (fortLevel == CGTownInstance::CASTLE ? 5000 : 2000);
+	}
+
+	case Obj::CREATURE_GENERATOR1:
+	case Obj::CREATURE_GENERATOR2:
+	case Obj::CREATURE_GENERATOR3:
+	case Obj::CREATURE_GENERATOR4:
+		return getDwellingArmyGrowth(ai->cb.get(), target, hero->getOwner());
+	case Obj::ARTIFACT:
+		// it is not supported now because hero will not sit in town on 7th day but later parts of legion may be counted as army growth as well.
+		return 0;
+	default:
+		return 0;
+	}
+}
+
 int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const
 int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const
 {
 {
 	if(!target)
 	if(!target)
@@ -338,7 +400,7 @@ float RewardEvaluator::getEnemyHeroStrategicalValue(const CGHeroInstance * enemy
 	  2. The formula quickly approaches 1.0 as hero level increases,
 	  2. The formula quickly approaches 1.0 as hero level increases,
 	  but higher level always means higher value and the minimal value for level 1 hero is 0.5
 	  but higher level always means higher value and the minimal value for level 1 hero is 0.5
 	*/
 	*/
-	return std::min(1.0f, objectValue * 0.9f + (1.0f - (1.0f / (1 + enemy->level))));
+	return std::min(1.5f, objectValue * 0.9f + (1.5f - (1.5f / (1 + enemy->level))));
 }
 }
 
 
 float RewardEvaluator::getResourceRequirementStrength(int resType) const
 float RewardEvaluator::getResourceRequirementStrength(int resType) const
@@ -366,10 +428,26 @@ float RewardEvaluator::getTotalResourceRequirementStrength(int resType) const
 		return 0;
 		return 0;
 
 
 	float ratio = dailyIncome[resType] == 0
 	float ratio = dailyIncome[resType] == 0
-		? (float)requiredResources[resType] / 50.0f
-		: (float)requiredResources[resType] / dailyIncome[resType] / 50.0f;
+		? (float)requiredResources[resType] / 10.0f
+		: (float)requiredResources[resType] / dailyIncome[resType] / 20.0f;
 
 
-	return std::min(ratio, 1.0f);
+	return std::min(ratio, 2.0f);
+}
+
+uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
+{
+	uint64_t result = 0;
+
+	for(auto creatureInfo : town->creatures)
+	{
+		if(creatureInfo.second.empty())
+			continue;
+
+		auto creature = creatureInfo.second.back().toCreature();
+		result += creature->getAIValue() * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth();
+	}
+
+	return result;
 }
 }
 
 
 float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const
 float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const
@@ -407,18 +485,28 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 	case Obj::TOWN:
 	case Obj::TOWN:
 	{
 	{
 		if(ai->buildAnalyzer->getDevelopmentInfo().empty())
 		if(ai->buildAnalyzer->getDevelopmentInfo().empty())
-			return 1;
+			return 10.0f;
 
 
 		auto town = dynamic_cast<const CGTownInstance *>(target);
 		auto town = dynamic_cast<const CGTownInstance *>(target);
+
+		if(town->getOwner() == ai->playerID)
+		{
+			auto armyIncome = townArmyGrowth(town);
+			auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
+
+			return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f);
+		}
+
 		auto fortLevel = town->fortLevel();
 		auto fortLevel = town->fortLevel();
-		auto booster = isAnotherAi(town, *ai->cb) ? 0.3 : 1;
+		auto booster = isAnotherAi(town, *ai->cb) ? 0.4f : 1.0f;
 
 
-		if(town->hasCapitol()) return 1;
+		if(town->hasCapitol())
+			return booster * 1.5;
 
 
 		if(fortLevel < CGTownInstance::CITADEL)
 		if(fortLevel < CGTownInstance::CITADEL)
-			return booster * (town->hasFort() ? 0.6 : 0.4);
+			return booster * (town->hasFort() ? 1.0 : 0.8);
 		else
 		else
-			return booster * (fortLevel == CGTownInstance::CASTLE ? 0.9 : 0.8);
+			return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
 	}
 	}
 
 
 	case Obj::HERO:
 	case Obj::HERO:
@@ -463,15 +551,18 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
 	case Obj::GARDEN_OF_REVELATION:
 	case Obj::GARDEN_OF_REVELATION:
 	case Obj::MARLETTO_TOWER:
 	case Obj::MARLETTO_TOWER:
 	case Obj::MERCENARY_CAMP:
 	case Obj::MERCENARY_CAMP:
-	case Obj::SHRINE_OF_MAGIC_GESTURE:
-	case Obj::SHRINE_OF_MAGIC_INCANTATION:
 	case Obj::TREE_OF_KNOWLEDGE:
 	case Obj::TREE_OF_KNOWLEDGE:
 		return 1;
 		return 1;
 	case Obj::LEARNING_STONE:
 	case Obj::LEARNING_STONE:
 		return 1.0f / std::sqrt(hero->level);
 		return 1.0f / std::sqrt(hero->level);
 	case Obj::ARENA:
 	case Obj::ARENA:
-	case Obj::SHRINE_OF_MAGIC_THOUGHT:
 		return 2;
 		return 2;
+	case Obj::SHRINE_OF_MAGIC_INCANTATION:
+		return 0.2f;
+	case Obj::SHRINE_OF_MAGIC_GESTURE:
+		return 0.3f;
+	case Obj::SHRINE_OF_MAGIC_THOUGHT:
+		return 0.5f;
 	case Obj::LIBRARY_OF_ENLIGHTENMENT:
 	case Obj::LIBRARY_OF_ENLIGHTENMENT:
 		return 8;
 		return 8;
 	case Obj::WITCH_HUT:
 	case Obj::WITCH_HUT:
@@ -513,12 +604,13 @@ int32_t getArmyCost(const CArmedInstance * army)
 	return value;
 	return value;
 }
 }
 
 
-/// Gets aproximated reward in gold. Daily income is multiplied by 5
 int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const
 int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const
 {
 {
 	if(!target)
 	if(!target)
 		return 0;
 		return 0;
 
 
+	auto relations = ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner);
+
 	const int dailyIncomeMultiplier = 5;
 	const int dailyIncomeMultiplier = 5;
 	const float enemyArmyEliminationGoldRewardRatio = 0.2f;
 	const float enemyArmyEliminationGoldRewardRatio = 0.2f;
 	const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2;
 	const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2;
@@ -559,7 +651,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 		//Objectively saves us 2500 to hire hero
 		//Objectively saves us 2500 to hire hero
 		return GameConstants::HERO_GOLD_COST;
 		return GameConstants::HERO_GOLD_COST;
 	case Obj::HERO:
 	case Obj::HERO:
-		return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
+		return relations == PlayerRelations::ENEMIES
 			? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast<const CGHeroInstance *>(target))
 			? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast<const CGHeroInstance *>(target))
 			: 0;
 			: 0;
 	default:
 	default:
@@ -579,7 +671,8 @@ public:
 
 
 		uint64_t armyStrength = heroExchange.getReinforcementArmyStrength();
 		uint64_t armyStrength = heroExchange.getReinforcementArmyStrength();
 
 
-		evaluationContext.strategicalValue += 0.5f * armyStrength / heroExchange.hero.get()->getArmyStrength();
+		evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero.get()->getArmyStrength());
+		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero.get());
 	}
 	}
 };
 };
 
 
@@ -596,7 +689,7 @@ public:
 		uint64_t upgradeValue = armyUpgrade.getUpgradeValue();
 		uint64_t upgradeValue = armyUpgrade.getUpgradeValue();
 
 
 		evaluationContext.armyReward += upgradeValue;
 		evaluationContext.armyReward += upgradeValue;
-		evaluationContext.strategicalValue += upgradeValue / (float)armyUpgrade.hero->getArmyStrength();
+		evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
 	}
 	}
 };
 };
 
 
@@ -621,23 +714,6 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
 
 
 class DefendTownEvaluator : public IEvaluationContextBuilder
 class DefendTownEvaluator : public IEvaluationContextBuilder
 {
 {
-private:
-	uint64_t townArmyIncome(const CGTownInstance * town) const
-	{
-		uint64_t result = 0;
-
-		for(auto creatureInfo : town->creatures)
-		{
-			if(creatureInfo.second.empty())
-				continue;
-
-			auto creature = creatureInfo.second.back().toCreature();
-			result += creature->getAIValue() * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth();
-		}
-
-		return result;
-	}
-
 public:
 public:
 	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
 	{
@@ -648,22 +724,34 @@ public:
 		const CGTownInstance * town = defendTown.town;
 		const CGTownInstance * town = defendTown.town;
 		auto & treat = defendTown.getTreat();
 		auto & treat = defendTown.getTreat();
 
 
-		auto armyIncome = townArmyIncome(town);
-		auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
-
-		auto strategicalValue = std::sqrt(armyIncome / 20000.0f) + dailyIncome / 3000.0f;
-
-		if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1)
-			strategicalValue = 1;
+		auto strategicalValue = evaluationContext.evaluator.getStrategicalValue(town);
 
 
 		float multiplier = 1;
 		float multiplier = 1;
 
 
 		if(treat.turn < defendTown.getTurn())
 		if(treat.turn < defendTown.getTurn())
 			multiplier /= 1 + (defendTown.getTurn() - treat.turn);
 			multiplier /= 1 + (defendTown.getTurn() - treat.turn);
 
 
-		evaluationContext.armyReward += armyIncome * multiplier;
+		multiplier /= 1.0f + treat.turn / 5.0f;
+
+		if(defendTown.getTurn() > 0 && defendTown.isCounterAttack())
+		{
+			auto ourSpeed = defendTown.hero->movementPointsLimit(true);
+			auto enemySpeed = treat.hero->movementPointsLimit(true);
+
+			if(enemySpeed > ourSpeed) multiplier *= 0.7f;
+		}
+
+		auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
+		auto armyGrowth = evaluationContext.evaluator.townArmyGrowth(town);
+
+		evaluationContext.armyGrowth += armyGrowth * multiplier;
 		evaluationContext.goldReward += dailyIncome * 5 * multiplier;
 		evaluationContext.goldReward += dailyIncome * 5 * multiplier;
-		evaluationContext.strategicalValue += strategicalValue * multiplier;
+
+		if(evaluationContext.evaluator.ai->buildAnalyzer->getDevelopmentInfo().size() == 1)
+			vstd::amax(evaluationContext.strategicalValue, 2.5f * multiplier * strategicalValue);
+		else
+			evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
+
 		vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
 		vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
 		addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
 		addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
 	}
 	}
@@ -709,18 +797,22 @@ public:
 		auto army = path.heroArmy;
 		auto army = path.heroArmy;
 
 
 		const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false);
 		const CGObjectInstance * target = ai->cb->getObj((ObjectInstanceID)task->objid, false);
+		auto heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
+
+		if(heroRole == HeroRole::MAIN)
+			evaluationContext.heroRole = heroRole;
 
 
-		if (target && ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner) == PlayerRelations::ENEMIES)
+		if (target)
 		{
 		{
 			evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero);
 			evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero);
 			evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold);
 			evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold);
-			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, evaluationContext.heroRole);
-			evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target);
+			evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
+			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
+			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
 		}
 		}
 
 
 		vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
 		vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
-		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroPtr);
 		addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
 		addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
 		vstd::amax(evaluationContext.turn, path.turn());
 		vstd::amax(evaluationContext.turn, path.turn());
 	}
 	}
@@ -760,7 +852,7 @@ public:
 			evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost;
 			evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost;
 			evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
 			evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
 			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
 			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
-			evaluationContext.strategicalValue += evaluationContext.evaluator.getStrategicalValue(target) / boost;
+			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
 			evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
 			evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
 			evaluationContext.movementCost += objInfo.second.movementCost / boost;
 			evaluationContext.movementCost += objInfo.second.movementCost / boost;
@@ -798,6 +890,31 @@ public:
 	}
 	}
 };
 };
 
 
+class DismissHeroContextBuilder : public IEvaluationContextBuilder
+{
+private:
+	const Nullkiller * ai;
+
+public:
+	DismissHeroContextBuilder(const Nullkiller * ai) : ai(ai) {}
+
+	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	{
+		if(task->goalType != Goals::DISMISS_HERO)
+			return;
+
+		Goals::DismissHero & dismissCommand = dynamic_cast<Goals::DismissHero &>(*task);
+		const CGHeroInstance * dismissedHero = dismissCommand.getHero().get();
+
+		auto role = ai->heroManager->getHeroRole(dismissedHero);
+		auto mpLeft = dismissedHero->movementPointsRemaining();
+			
+		evaluationContext.movementCost += mpLeft;
+		evaluationContext.movementCostByRole[role] += mpLeft;
+		evaluationContext.goldCost += GameConstants::HERO_GOLD_COST + getArmyCost(dismissedHero);
+	}
+};
+
 class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
 class BuildThisEvaluationContextBuilder : public IEvaluationContextBuilder
 {
 {
 public:
 public:
@@ -813,39 +930,47 @@ public:
 		evaluationContext.heroRole = HeroRole::MAIN;
 		evaluationContext.heroRole = HeroRole::MAIN;
 		evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
 		evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
 		evaluationContext.goldCost += bi.buildCostWithPrerequisits[EGameResID::GOLD];
 		evaluationContext.goldCost += bi.buildCostWithPrerequisits[EGameResID::GOLD];
+		evaluationContext.closestWayRatio = 1;
 
 
 		if(bi.creatureID != CreatureID::NONE)
 		if(bi.creatureID != CreatureID::NONE)
 		{
 		{
-			evaluationContext.strategicalValue += buildThis.townInfo.armyStrength / 50000.0;
+			evaluationContext.addNonCriticalStrategicalValue(buildThis.townInfo.armyStrength / 50000.0);
 
 
 			if(bi.baseCreatureID == bi.creatureID)
 			if(bi.baseCreatureID == bi.creatureID)
 			{
 			{
-				evaluationContext.strategicalValue += (0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount;
+				evaluationContext.addNonCriticalStrategicalValue((0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount);
 				evaluationContext.armyReward += bi.armyStrength;
 				evaluationContext.armyReward += bi.armyStrength;
 			}
 			}
 			else
 			else
 			{
 			{
 				auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi);
 				auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi);
 				
 				
-				evaluationContext.strategicalValue += potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount;
+				evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount);
 				evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount;
 				evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount;
 			}
 			}
 		}
 		}
 		else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE)
 		else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE)
 		{
 		{
-			evaluationContext.strategicalValue += buildThis.town->creatures.size() * 0.2f;
+			evaluationContext.addNonCriticalStrategicalValue(buildThis.town->creatures.size() * 0.2f);
 			evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2;
 			evaluationContext.armyReward += buildThis.townInfo.armyStrength / 2;
 		}
 		}
-		else
+		else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
+		{
+			evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
+		}
+		
+		if(evaluationContext.goldReward)
 		{
 		{
 			auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure();
 			auto goldPreasure = evaluationContext.evaluator.ai->buildAnalyzer->getGoldPreasure();
 
 
-			evaluationContext.strategicalValue += evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount;
+			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount);
 		}
 		}
 
 
 		if(bi.notEnoughRes && bi.prerequisitesCount == 1)
 		if(bi.notEnoughRes && bi.prerequisitesCount == 1)
 		{
 		{
-			evaluationContext.strategicalValue /= 2;
+			evaluationContext.strategicalValue /= 3;
+			evaluationContext.movementCostByRole[evaluationContext.heroRole] += 5;
+			evaluationContext.turn += 5;
 		}
 		}
 	}
 	}
 };
 };
@@ -872,6 +997,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai)
 	evaluationContextBuilders.push_back(std::make_shared<ArmyUpgradeEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<ArmyUpgradeEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
 	evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
+	evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai));
 }
 }
 
 
 EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const
 EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const
@@ -909,6 +1035,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 		+ (evaluationContext.armyReward > 0 ? 1 : 0)
 		+ (evaluationContext.armyReward > 0 ? 1 : 0)
 		+ (evaluationContext.skillReward > 0 ? 1 : 0)
 		+ (evaluationContext.skillReward > 0 ? 1 : 0)
 		+ (evaluationContext.strategicalValue > 0 ? 1 : 0);
 		+ (evaluationContext.strategicalValue > 0 ? 1 : 0);
+
+	float goldRewardPerTurn = evaluationContext.goldReward / std::log2f(2 + evaluationContext.movementCost * 10);
 	
 	
 	double result = 0;
 	double result = 0;
 
 
@@ -918,8 +1046,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 		heroRoleVariable->setValue(evaluationContext.heroRole);
 		heroRoleVariable->setValue(evaluationContext.heroRole);
 		mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
 		mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
 		scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
 		scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
-		goldRewardVariable->setValue(evaluationContext.goldReward);
+		goldRewardVariable->setValue(goldRewardPerTurn);
 		armyRewardVariable->setValue(evaluationContext.armyReward);
 		armyRewardVariable->setValue(evaluationContext.armyReward);
+		armyGrowthVariable->setValue(evaluationContext.armyGrowth);
 		skillRewardVariable->setValue(evaluationContext.skillReward);
 		skillRewardVariable->setValue(evaluationContext.skillReward);
 		dangerVariable->setValue(evaluationContext.danger);
 		dangerVariable->setValue(evaluationContext.danger);
 		rewardTypeVariable->setValue(rewardType);
 		rewardTypeVariable->setValue(rewardType);
@@ -940,13 +1069,13 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 	}
 	}
 
 
 #if NKAI_TRACE_LEVEL >= 2
 #if NKAI_TRACE_LEVEL >= 2
-	logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %d, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
+	logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %d, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
 		task->toString(),
 		task->toString(),
 		evaluationContext.armyLossPersentage,
 		evaluationContext.armyLossPersentage,
 		(int)evaluationContext.turn,
 		(int)evaluationContext.turn,
 		evaluationContext.movementCostByRole[HeroRole::MAIN],
 		evaluationContext.movementCostByRole[HeroRole::MAIN],
 		evaluationContext.movementCostByRole[HeroRole::SCOUT],
 		evaluationContext.movementCostByRole[HeroRole::SCOUT],
-		evaluationContext.goldReward,
+		goldRewardPerTurn,
 		evaluationContext.goldCost,
 		evaluationContext.goldCost,
 		evaluationContext.armyReward,
 		evaluationContext.armyReward,
 		evaluationContext.danger,
 		evaluationContext.danger,

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

@@ -33,6 +33,7 @@ public:
 	RewardEvaluator(const Nullkiller * ai) : ai(ai) {}
 	RewardEvaluator(const Nullkiller * ai) : ai(ai) {}
 
 
 	uint64_t getArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army, bool checkGold) const;
 	uint64_t getArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army, bool checkGold) const;
+	uint64_t getArmyGrowth(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const;
 	int getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const;
 	int getGoldCost(const CGObjectInstance * target, const CGHeroInstance * hero, const CCreatureSet * army) const;
 	float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy) const;
 	float getEnemyHeroStrategicalValue(const CGHeroInstance * enemy) const;
 	float getResourceRequirementStrength(int resType) const;
 	float getResourceRequirementStrength(int resType) const;
@@ -43,6 +44,7 @@ public:
 	int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const;
 	int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const;
 	uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
 	uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
 	const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
 	const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
+	uint64_t townArmyGrowth(const CGTownInstance * town) const;
 };
 };
 
 
 struct DLL_EXPORT EvaluationContext
 struct DLL_EXPORT EvaluationContext
@@ -54,6 +56,7 @@ struct DLL_EXPORT EvaluationContext
 	float closestWayRatio;
 	float closestWayRatio;
 	float armyLossPersentage;
 	float armyLossPersentage;
 	float armyReward;
 	float armyReward;
+	uint64_t armyGrowth;
 	int32_t goldReward;
 	int32_t goldReward;
 	int32_t goldCost;
 	int32_t goldCost;
 	float skillReward;
 	float skillReward;
@@ -64,6 +67,8 @@ struct DLL_EXPORT EvaluationContext
 	float enemyHeroDangerRatio;
 	float enemyHeroDangerRatio;
 
 
 	EvaluationContext(const Nullkiller * ai);
 	EvaluationContext(const Nullkiller * ai);
+
+	void addNonCriticalStrategicalValue(float value);
 };
 };
 
 
 class IEvaluationContextBuilder
 class IEvaluationContextBuilder
@@ -95,6 +100,7 @@ private:
 	fl::InputVariable * turnVariable;
 	fl::InputVariable * turnVariable;
 	fl::InputVariable * goldRewardVariable;
 	fl::InputVariable * goldRewardVariable;
 	fl::InputVariable * armyRewardVariable;
 	fl::InputVariable * armyRewardVariable;
+	fl::InputVariable * armyGrowthVariable;
 	fl::InputVariable * dangerVariable;
 	fl::InputVariable * dangerVariable;
 	fl::InputVariable * skillRewardVariable;
 	fl::InputVariable * skillRewardVariable;
 	fl::InputVariable * strategicalValueVariable;
 	fl::InputVariable * strategicalValueVariable;

+ 1 - 1
AI/Nullkiller/Goals/BuyArmy.cpp

@@ -71,7 +71,7 @@ void BuyArmy::accept(AIGateway * ai)
 		throw cannotFulfillGoalException("No creatures to buy.");
 		throw cannotFulfillGoalException("No creatures to buy.");
 	}
 	}
 
 
-	if(town->visitingHero)
+	if(town->visitingHero && !town->garrisonHero)
 	{
 	{
 		ai->moveHeroToTile(town->visitablePos(), town->visitingHero.get());
 		ai->moveHeroToTile(town->visitablePos(), town->visitingHero.get());
 	}
 	}

+ 49 - 9
AI/Nullkiller/Goals/Composition.cpp

@@ -31,9 +31,17 @@ std::string Composition::toString() const
 {
 {
 	std::string result = "Composition";
 	std::string result = "Composition";
 
 
-	for(auto goal : subtasks)
+	for(auto step : subtasks)
 	{
 	{
-		result += " " + goal->toString();
+		result += "[";
+		for(auto goal : step)
+		{
+			if(goal->isElementar())
+				result +=  goal->toString() + " => ";
+			else
+				result += goal->toString() + ", ";
+		}
+		result += "] ";
 	}
 	}
 
 
 	return result;
 	return result;
@@ -41,17 +49,34 @@ std::string Composition::toString() const
 
 
 void Composition::accept(AIGateway * ai)
 void Composition::accept(AIGateway * ai)
 {
 {
-	taskptr(*subtasks.back())->accept(ai);
+	for(auto task : subtasks.back())
+	{
+		if(task->isElementar())
+		{
+			taskptr(*task)->accept(ai);
+		}
+		else
+		{
+			break;
+		}
+	}
 }
 }
 
 
 TGoalVec Composition::decompose() const
 TGoalVec Composition::decompose() const
 {
 {
-	return subtasks;
+	TGoalVec result;
+
+	for(const TGoalVec & step : subtasks)
+		vstd::concatenate(result, step);
+
+	return result;
 }
 }
 
 
-Composition & Composition::addNext(const AbstractGoal & goal)
+Composition & Composition::addNextSequence(const TGoalVec & taskSequence)
 {
 {
-	return addNext(sptr(goal));
+	subtasks.push_back(taskSequence);
+
+	return *this;
 }
 }
 
 
 Composition & Composition::addNext(TSubgoal goal)
 Composition & Composition::addNext(TSubgoal goal)
@@ -64,20 +89,35 @@ Composition & Composition::addNext(TSubgoal goal)
 	}
 	}
 	else
 	else
 	{
 	{
-		subtasks.push_back(goal);
+		subtasks.push_back({goal});
 	}
 	}
 
 
 	return *this;
 	return *this;
 }
 }
 
 
+Composition & Composition::addNext(const AbstractGoal & goal)
+{
+	return addNext(sptr(goal));
+}
+
 bool Composition::isElementar() const
 bool Composition::isElementar() const
 {
 {
-	return subtasks.back()->isElementar();
+	return subtasks.back().front()->isElementar();
 }
 }
 
 
 int Composition::getHeroExchangeCount() const
 int Composition::getHeroExchangeCount() const
 {
 {
-	return isElementar() ? taskptr(*subtasks.back())->getHeroExchangeCount() : 0;
+	auto result = 0;
+
+	for(auto task : subtasks.back())
+	{
+		if(task->isElementar())
+		{
+			result += taskptr(*task)->getHeroExchangeCount();
+		}
+	}
+	
+	return result;
 }
 }
 
 
 }
 }

+ 2 - 6
AI/Nullkiller/Goals/Composition.h

@@ -18,7 +18,7 @@ namespace Goals
 	class DLL_EXPORT Composition : public ElementarGoal<Composition>
 	class DLL_EXPORT Composition : public ElementarGoal<Composition>
 	{
 	{
 	private:
 	private:
-		TGoalVec subtasks;
+		std::vector<TGoalVec> subtasks; // things we want to do now
 
 
 	public:
 	public:
 		Composition()
 		Composition()
@@ -26,16 +26,12 @@ namespace Goals
 		{
 		{
 		}
 		}
 
 
-		Composition(TGoalVec subtasks)
-			: ElementarGoal(Goals::COMPOSITION), subtasks(subtasks)
-		{
-		}
-
 		virtual bool operator==(const Composition & other) const override;
 		virtual bool operator==(const Composition & other) const override;
 		virtual std::string toString() const override;
 		virtual std::string toString() const override;
 		void accept(AIGateway * ai) override;
 		void accept(AIGateway * ai) override;
 		Composition & addNext(const AbstractGoal & goal);
 		Composition & addNext(const AbstractGoal & goal);
 		Composition & addNext(TSubgoal goal);
 		Composition & addNext(TSubgoal goal);
+		Composition & addNextSequence(const TGoalVec & taskSequence);
 		virtual TGoalVec decompose() const override;
 		virtual TGoalVec decompose() const override;
 		virtual bool isElementar() const override;
 		virtual bool isElementar() const override;
 		virtual int getHeroExchangeCount() const override;
 		virtual int getHeroExchangeCount() const override;

+ 14 - 0
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -52,6 +52,20 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 	ai->nullkiller->setActive(chainPath.targetHero, tile);
 	ai->nullkiller->setActive(chainPath.targetHero, tile);
 	ai->nullkiller->setTargetObject(objid);
 	ai->nullkiller->setTargetObject(objid);
 
 
+	auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);
+
+	if(chainPath.turn() == 0 && targetObject && targetObject->ID == Obj::TOWN)
+	{
+		auto relations = ai->myCb->getPlayerRelations(ai->playerID, targetObject->getOwner());
+
+		if(relations == PlayerRelations::ENEMIES)
+		{
+			ai->nullkiller->armyFormation->rearrangeArmyForSiege(
+				dynamic_cast<const CGTownInstance *>(targetObject),
+				chainPath.targetHero);
+		}
+	}
+
 	std::set<int> blockedIndexes;
 	std::set<int> blockedIndexes;
 
 
 	for(int i = chainPath.nodes.size() - 1; i >= 0; i--)
 	for(int i = chainPath.nodes.size() - 1; i >= 0; i--)

+ 12 - 9
AI/Nullkiller/Goals/RecruitHero.cpp

@@ -24,7 +24,10 @@ using namespace Goals;
 
 
 std::string RecruitHero::toString() const
 std::string RecruitHero::toString() const
 {
 {
-	return "Recruit hero at " + town->getNameTranslated();
+	if(heroToBuy)
+		return "Recruit " + heroToBuy->getNameTranslated() + " at " + town->getNameTranslated();
+	else
+		return "Recruit hero at " + town->getNameTranslated();
 }
 }
 
 
 void RecruitHero::accept(AIGateway * ai)
 void RecruitHero::accept(AIGateway * ai)
@@ -45,20 +48,20 @@ void RecruitHero::accept(AIGateway * ai)
 		throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName());
 		throw cannotFulfillGoalException("No available heroes in tavern in " + t->nodeName());
 	}
 	}
 
 
-	auto heroToHire = heroes[0];
+	auto heroToHire = heroToBuy;
 
 
-	for(auto hero : heroes)
+	if(!heroToHire)
 	{
 	{
-		if(objid == hero->id.getNum())
+		for(auto hero : heroes)
 		{
 		{
-			heroToHire = hero;
-			break;
+			if(!heroToHire || hero->getTotalStrength() > heroToHire->getTotalStrength())
+				heroToHire = hero;
 		}
 		}
-
-		if(hero->getTotalStrength() > heroToHire->getTotalStrength())
-			heroToHire = hero;
 	}
 	}
 
 
+	if(!heroToHire)
+		throw cannotFulfillGoalException("No hero to hire!");
+
 	if(t->visitingHero)
 	if(t->visitingHero)
 	{
 	{
 		cb->swapGarrisonHero(t);
 		cb->swapGarrisonHero(t);

+ 7 - 5
AI/Nullkiller/Goals/RecruitHero.h

@@ -22,18 +22,20 @@ namespace Goals
 {
 {
 	class DLL_EXPORT RecruitHero : public ElementarGoal<RecruitHero>
 	class DLL_EXPORT RecruitHero : public ElementarGoal<RecruitHero>
 	{
 	{
+	private:
+		const CGHeroInstance * heroToBuy;
+
 	public:
 	public:
 		RecruitHero(const CGTownInstance * townWithTavern, const CGHeroInstance * heroToBuy)
 		RecruitHero(const CGTownInstance * townWithTavern, const CGHeroInstance * heroToBuy)
-			: RecruitHero(townWithTavern)
+			: ElementarGoal(Goals::RECRUIT_HERO), heroToBuy(heroToBuy)
 		{
 		{
-			objid = heroToBuy->id.getNum();
+			town = townWithTavern;
+			priority = 1;
 		}
 		}
 
 
 		RecruitHero(const CGTownInstance * townWithTavern)
 		RecruitHero(const CGTownInstance * townWithTavern)
-			: ElementarGoal(Goals::RECRUIT_HERO)
+			: RecruitHero(townWithTavern, nullptr)
 		{
 		{
-			priority = 1;
-			town = townWithTavern;
 		}
 		}
 
 
 		virtual bool operator==(const RecruitHero & other) const override
 		virtual bool operator==(const RecruitHero & other) const override

+ 68 - 0
AI/Nullkiller/Helpers/ArmyFormation.cpp

@@ -0,0 +1,68 @@
+/*
+* ArmyFormation.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 "ArmyFormation.h"
+#include "../../../lib/mapObjects/CGTownInstance.h"
+
+namespace NKAI
+{
+
+void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker)
+{
+	auto freeSlots = attacker->getFreeSlotsQueue();
+
+	while(!freeSlots.empty())
+	{
+		auto weakestCreature = vstd::minElementByFun(attacker->Slots(), [](const std::pair<SlotID, CStackInstance *> & slot) -> int
+			{
+				return slot.second->getCount() == 1
+					? std::numeric_limits<int>::max()
+					: slot.second->getCreatureID().toCreature()->getAIValue();
+			});
+
+		if(weakestCreature == attacker->Slots().end() || weakestCreature->second->getCount() == 1)
+		{
+			break;
+		}
+
+		cb->splitStack(attacker, attacker, weakestCreature->first, freeSlots.front(), 1);
+		freeSlots.pop();
+	}
+
+	if(town->fortLevel() > CGTownInstance::FORT)
+	{
+		std::vector<CStackInstance *> stacks;
+
+		for(auto slot : attacker->Slots())
+			stacks.push_back(slot.second);
+
+		boost::sort(
+			stacks,
+			[](CStackInstance * slot1, CStackInstance * slot2) -> bool
+			{
+				auto cre1 = slot1->getCreatureID().toCreature();
+				auto cre2 = slot2->getCreatureID().toCreature();
+				auto flying = cre1->hasBonusOfType(BonusType::FLYING) - cre2->hasBonusOfType(BonusType::FLYING);
+			
+				if(flying != 0) return flying < 0;
+				else return cre1->getAIValue() < cre2->getAIValue();
+			});
+
+		for(int i = 0; i < stacks.size(); i++)
+		{
+			auto pos = vstd::findKey(attacker->Slots(), stacks[i]);
+
+			if(pos.getNum() != i)
+				cb->swapCreatures(attacker, attacker, static_cast<SlotID>(i), pos);
+		}
+	}
+}
+
+}

+ 38 - 0
AI/Nullkiller/Helpers/ArmyFormation.h

@@ -0,0 +1,38 @@
+/*
+* ArmyFormation.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 "../AIUtility.h"
+
+#include "../../../lib/GameConstants.h"
+#include "../../../lib/VCMI_Lib.h"
+#include "../../../lib/CTownHandler.h"
+#include "../../../lib/CBuildingHandler.h"
+
+namespace NKAI
+{
+
+struct HeroPtr;
+class AIGateway;
+class FuzzyHelper;
+class Nullkiller;
+
+class DLL_EXPORT ArmyFormation
+{
+private:
+	std::shared_ptr<CCallback> cb; //this is enough, but we downcast from CCallback
+
+public:
+	ArmyFormation(std::shared_ptr<CCallback> CB, const Nullkiller * ai): cb(CB) {}
+
+	void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker);
+};
+
+}

+ 7 - 0
AI/Nullkiller/Markers/ArmyUpgrade.cpp

@@ -28,6 +28,13 @@ ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * up
 	sethero(upgradePath.targetHero);
 	sethero(upgradePath.targetHero);
 }
 }
 
 
+ArmyUpgrade::ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade)
+	: CGoal(Goals::ARMY_UPGRADE), upgrader(upgrader), upgradeValue(upgrade.upgradeValue),
+	initialValue(targetMain->getArmyStrength()), goldCost(upgrade.upgradeCost[EGameResID::GOLD])
+{
+	sethero(targetMain);
+}
+
 bool ArmyUpgrade::operator==(const ArmyUpgrade & other) const
 bool ArmyUpgrade::operator==(const ArmyUpgrade & other) const
 {
 {
 	return false;
 	return false;

+ 1 - 0
AI/Nullkiller/Markers/ArmyUpgrade.h

@@ -27,6 +27,7 @@ namespace Goals
 
 
 	public:
 	public:
 		ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
 		ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
+		ArmyUpgrade(const CGHeroInstance * targetMain, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade);
 
 
 		virtual bool operator==(const ArmyUpgrade & other) const override;
 		virtual bool operator==(const ArmyUpgrade & other) const override;
 		virtual std::string toString() const override;
 		virtual std::string toString() const override;

+ 2 - 2
AI/Nullkiller/Markers/DefendTown.cpp

@@ -18,8 +18,8 @@ namespace NKAI
 
 
 using namespace Goals;
 using namespace Goals;
 
 
-DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath)
-	: CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn())
+DefendTown::DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack)
+	: CGoal(Goals::DEFEND_TOWN), treat(treat), defenceArmyStrength(defencePath.getHeroStrength()), turn(defencePath.turn()), counterattack(isCounterAttack)
 {
 {
 	settown(town);
 	settown(town);
 	sethero(defencePath.targetHero);
 	sethero(defencePath.targetHero);

+ 4 - 1
AI/Nullkiller/Markers/DefendTown.h

@@ -24,9 +24,10 @@ namespace Goals
 		uint64_t defenceArmyStrength;
 		uint64_t defenceArmyStrength;
 		HitMapInfo treat;
 		HitMapInfo treat;
 		uint8_t turn;
 		uint8_t turn;
+		bool counterattack;
 
 
 	public:
 	public:
-		DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath);
+		DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const AIPath & defencePath, bool isCounterAttack = false);
 		DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender);
 		DefendTown(const CGTownInstance * town, const HitMapInfo & treat, const CGHeroInstance * defender);
 
 
 		virtual bool operator==(const DefendTown & other) const override;
 		virtual bool operator==(const DefendTown & other) const override;
@@ -37,6 +38,8 @@ namespace Goals
 		uint64_t getDefenceStrength() const { return defenceArmyStrength; }
 		uint64_t getDefenceStrength() const { return defenceArmyStrength; }
 
 
 		uint8_t getTurn() const { return turn; }
 		uint8_t getTurn() const { return turn; }
+
+		bool isCounterAttack() { return counterattack; }
 	};
 	};
 }
 }
 
 

+ 1 - 1
AI/Nullkiller/Markers/HeroExchange.cpp

@@ -29,7 +29,7 @@ bool HeroExchange::operator==(const HeroExchange & other) const
 
 
 std::string HeroExchange::toString() const
 std::string HeroExchange::toString() const
 {
 {
-	return "Hero exchange " + exchangePath.toString();
+	return "Hero exchange for " +hero.get()->getObjectName() + " by " + exchangePath.toString();
 }
 }
 
 
 uint64_t HeroExchange::getReinforcementArmyStrength() const
 uint64_t HeroExchange::getReinforcementArmyStrength() const

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

@@ -879,8 +879,12 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
 	for(auto & hero : heroes)
 	for(auto & hero : heroes)
 	{
 	{
 		// do not allow our own heroes in garrison to act on map
 		// do not allow our own heroes in garrison to act on map
-		if(hero.first->getOwner() == ai->playerID && hero.first->inTownGarrison)
+		if(hero.first->getOwner() == ai->playerID
+			&& hero.first->inTownGarrison
+			&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached()))
+		{
 			continue;
 			continue;
+		}
 
 
 		uint64_t mask = FirstActorMask << actors.size();
 		uint64_t mask = FirstActorMask << actors.size();
 		auto actor = std::make_shared<HeroActor>(hero.first, hero.second, mask, ai);
 		auto actor = std::make_shared<HeroActor>(hero.first, hero.second, mask, ai);

+ 3 - 3
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -24,8 +24,8 @@
 
 
 namespace NKAI
 namespace NKAI
 {
 {
-	const int SCOUT_TURN_DISTANCE_LIMIT = 3;
-	const int MAIN_TURN_DISTANCE_LIMIT = 5;
+	const int SCOUT_TURN_DISTANCE_LIMIT = 5;
+	const int MAIN_TURN_DISTANCE_LIMIT = 10;
 
 
 namespace AIPathfinding
 namespace AIPathfinding
 {
 {
@@ -258,7 +258,7 @@ public:
 	{
 	{
 		double ratio = (double)danger / (armyValue * hero->getFightingStrength());
 		double ratio = (double)danger / (armyValue * hero->getFightingStrength());
 
 
-		return (uint64_t)(armyValue * ratio * ratio * ratio);
+		return (uint64_t)(armyValue * ratio * ratio);
 	}
 	}
 
 
 	STRONG_INLINE
 	STRONG_INLINE

+ 5 - 0
AI/Nullkiller/Pathfinding/AIPathfinder.cpp

@@ -61,6 +61,11 @@ void AIPathfinder::updatePaths(std::map<const CGHeroInstance *, HeroRole> heroes
 	storage->setScoutTurnDistanceLimit(pathfinderSettings.scoutTurnDistanceLimit);
 	storage->setScoutTurnDistanceLimit(pathfinderSettings.scoutTurnDistanceLimit);
 	storage->setMainTurnDistanceLimit(pathfinderSettings.mainTurnDistanceLimit);
 	storage->setMainTurnDistanceLimit(pathfinderSettings.mainTurnDistanceLimit);
 
 
+	logAi->trace(
+		"Scout turn distance: %s, main %s",
+		std::to_string(pathfinderSettings.scoutTurnDistanceLimit),
+		std::to_string(pathfinderSettings.mainTurnDistanceLimit));
+
 	if(pathfinderSettings.useHeroChain)
 	if(pathfinderSettings.useHeroChain)
 	{
 	{
 		storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs);
 		storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs);

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

@@ -134,6 +134,7 @@ void ChainActor::setBaseActor(HeroActor * base)
 	armyCost = base->armyCost;
 	armyCost = base->armyCost;
 	actorAction = base->actorAction;
 	actorAction = base->actorAction;
 	tiCache = base->tiCache;
 	tiCache = base->tiCache;
+	actorExchangeCount = base->actorExchangeCount;
 }
 }
 
 
 void HeroActor::setupSpecialActors()
 void HeroActor::setupSpecialActors()

+ 1 - 1
AI/StupidAI/StupidAI.cpp

@@ -242,7 +242,7 @@ void CStupidAI::battleStacksEffectsSet(const SetStackEffect & sse)
 	print("battleStacksEffectsSet called");
 	print("battleStacksEffectsSet called");
 }
 }
 
 
-void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side)
+void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
 {
 {
 	print("battleStart called");
 	print("battleStart called");
 	side = Side;
 	side = Side;

+ 1 - 1
AI/StupidAI/StupidAI.h

@@ -44,7 +44,7 @@ public:
 	void battleSpellCast(const BattleSpellCast *sc) override;
 	void battleSpellCast(const BattleSpellCast *sc) override;
 	void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
 	void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
 	//void battleTriggerEffect(const BattleTriggerEffect & bte) override;
 	//void battleTriggerEffect(const BattleTriggerEffect & bte) override;
-	void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right
+	void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
 	void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
 	void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
 
 
 private:
 private:

+ 12 - 8
AI/VCAI/VCAI.cpp

@@ -819,7 +819,7 @@ void VCAI::makeTurn()
 		for (auto h : cb->getHeroesInfo())
 		for (auto h : cb->getHeroesInfo())
 		{
 		{
 			if (h->movementPointsRemaining())
 			if (h->movementPointsRemaining())
-				logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
+				logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
 		}
 		}
 	}
 	}
 	catch (boost::thread_interrupted & e)
 	catch (boost::thread_interrupted & e)
@@ -1575,14 +1575,14 @@ void VCAI::completeGoal(Goals::TSubgoal goal)
 
 
 }
 }
 
 
-void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side)
+void VCAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
 {
 {
 	NET_EVENT_HANDLER;
 	NET_EVENT_HANDLER;
 	assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE);
 	assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE);
 	status.setBattle(ONGOING_BATTLE);
 	status.setBattle(ONGOING_BATTLE);
 	const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
 	const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
 	battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
 	battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
-	CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side);
+	CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed);
 }
 }
 
 
 void VCAI::battleEnd(const BattleResult * br, QueryID queryID)
 void VCAI::battleEnd(const BattleResult * br, QueryID queryID)
@@ -1593,12 +1593,16 @@ void VCAI::battleEnd(const BattleResult * br, QueryID queryID)
 	bool won = br->winner == myCb->battleGetMySide();
 	bool won = br->winner == myCb->battleGetMySide();
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename);
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename);
 	battlename.clear();
 	battlename.clear();
-	status.addQuery(queryID, "Combat result dialog");
-	const int confirmAction = 0;
-	requestActionASAP([=]()
+
+	if (queryID != -1)
 	{
 	{
-		answerQuery(queryID, confirmAction);
-	});
+		status.addQuery(queryID, "Combat result dialog");
+		const int confirmAction = 0;
+		requestActionASAP([=]()
+		{
+			answerQuery(queryID, confirmAction);
+		});
+	}
 	CAdventureAI::battleEnd(br, queryID);
 	CAdventureAI::battleEnd(br, queryID);
 }
 }
 
 

+ 1 - 1
AI/VCAI/VCAI.h

@@ -201,7 +201,7 @@ public:
 	void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override;
 	void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override;
 	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
 	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
 
 
-	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side) override;
+	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override;
 	void battleEnd(const BattleResult * br, QueryID queryID) override;
 	void battleEnd(const BattleResult * br, QueryID queryID) override;
 
 
 	void makeTurn();
 	void makeTurn();

+ 4 - 0
CMakeLists.txt

@@ -280,6 +280,10 @@ if(MINGW OR MSVC)
 	endif(MSVC)
 	endif(MSVC)
 
 
 	if(MINGW)
 	if(MINGW)
+
+		# Temporary (?) workaround for failing builds on MinGW CI due to bug in TBB
+		set(CMAKE_CXX_EXTENSIONS ON)
+
 		set(SYSTEM_LIBS ${SYSTEM_LIBS} ole32 oleaut32 ws2_32 mswsock dbghelp bcrypt)
 		set(SYSTEM_LIBS ${SYSTEM_LIBS} ole32 oleaut32 ws2_32 mswsock dbghelp bcrypt)
 
 
 		# Check for iconv (may be needed for Boost.Locale)
 		# Check for iconv (may be needed for Boost.Locale)

+ 10 - 1
ChangeLog.md

@@ -1,5 +1,4 @@
 # 1.2.1 -> 1.3.0
 # 1.2.1 -> 1.3.0
-(unreleased)
 
 
 ### GENERAL:
 ### GENERAL:
 * Implemented automatic interface scaling to any resolution supported by monitor
 * Implemented automatic interface scaling to any resolution supported by monitor
@@ -17,6 +16,7 @@
 * Added H3:SOD cheat codes as alternative to vcmi cheats
 * Added H3:SOD cheat codes as alternative to vcmi cheats
 * Fixed several possible crashes caused by autocombat activation
 * Fixed several possible crashes caused by autocombat activation
 * Fixed artifact lock icon in localized versions of the game
 * Fixed artifact lock icon in localized versions of the game
+* Fixed possible crash on changing hardware cursor
 
 
 ### TOUCHSCREEN SUPPORT:
 ### TOUCHSCREEN SUPPORT:
 * VCMI will now properly recognizes touch screen input
 * VCMI will now properly recognizes touch screen input
@@ -47,6 +47,10 @@
 ### AI PLAYER:
 ### AI PLAYER:
 * Fixed potential crash on accessing market (VCAI)
 * Fixed potential crash on accessing market (VCAI)
 * Fixed potentially infinite turns (VCAI)
 * Fixed potentially infinite turns (VCAI)
+* Reworked object prioritizing
+* Improved town defense against enemy heroes
+* Improved town building (mage guild and horde)
+* Various behavior fixes
 
 
 ### GAME MECHANICS
 ### GAME MECHANICS
 * Hero retreating after end of 7th turn will now correctly appear in tavern
 * Hero retreating after end of 7th turn will now correctly appear in tavern
@@ -72,6 +76,7 @@
 * Game will now play correct music track on scenario selection window
 * Game will now play correct music track on scenario selection window
 * Dracon woll now correctly start without spellbook in Dragon Slayer campaign
 * Dracon woll now correctly start without spellbook in Dragon Slayer campaign
 * Fixed frequent crash on moving to next scenario during campaign
 * Fixed frequent crash on moving to next scenario during campaign
+* Fixed inability to dismiss heroes on maps with "capture town" victory condition
 
 
 ### RANDOM MAP GENERATOR:
 ### RANDOM MAP GENERATOR:
 * Improved zone placement, shape and connections
 * Improved zone placement, shape and connections
@@ -86,6 +91,7 @@
 * Support for "wide" connections
 * Support for "wide" connections
 * Support for new "fictive" and "repulsive" connections
 * Support for new "fictive" and "repulsive" connections
 * RMG will now run faster, utilizing many CPU cores
 * RMG will now run faster, utilizing many CPU cores
+* Removed random seed number from random map description
 
 
 ### INTERFACE:
 ### INTERFACE:
 * Adventure map is now scalable and can be used with any resolution without mods
 * Adventure map is now scalable and can be used with any resolution without mods
@@ -105,6 +111,8 @@
 * Last symbol of entered cheat/chat message will no longer trigger hotkey
 * Last symbol of entered cheat/chat message will no longer trigger hotkey
 * Right-clicking map name in scenario selection will now show file name
 * Right-clicking map name in scenario selection will now show file name
 * Right-clicking save game in save/load screen will now show file name and creation date
 * Right-clicking save game in save/load screen will now show file name and creation date
+* Right-clicking in town fort window will now show creature information popup
+* Implemented pasting from clipboard (Ctrl+V) for text input
 
 
 ### BATTLES:
 ### BATTLES:
 * Implemented Tower moat (Land Mines)
 * Implemented Tower moat (Land Mines)
@@ -139,6 +147,7 @@
 * Removed DIRECT_DAMAGE_IMMUNITY bonus - replaced by 100% spell damage resistance
 * Removed DIRECT_DAMAGE_IMMUNITY bonus - replaced by 100% spell damage resistance
 * MAGIC_SCHOOL_SKILL subtype has been changed for consistency with other spell school bonuses
 * MAGIC_SCHOOL_SKILL subtype has been changed for consistency with other spell school bonuses
 * Configurable objects can now be translated
 * Configurable objects can now be translated
+* Fixed loading of custom battlefield identifiers for map objects
 
 
 # 1.2.0 -> 1.2.1
 # 1.2.0 -> 1.2.1
 
 

+ 19 - 1
Mods/vcmi/config/vcmi/polish.json

@@ -30,6 +30,13 @@
 	"vcmi.capitalColors.6" : "Jasnoniebieski",
 	"vcmi.capitalColors.6" : "Jasnoniebieski",
 	"vcmi.capitalColors.7" : "Różowy",
 	"vcmi.capitalColors.7" : "Różowy",
 
 
+	"vcmi.radialWheel.mergeSameUnit" : "Złącz takie same stworzenia",
+	"vcmi.radialWheel.showUnitInformation" : "Pokaż informacje o stworzeniu",
+	"vcmi.radialWheel.splitSingleUnit" : "Wydziel pojedyncze stworzenie",
+	"vcmi.radialWheel.splitUnitEqually" : "Podziel stworzenia równo",
+	"vcmi.radialWheel.moveUnit" : "Przenieś stworzenia do innej armii",
+	"vcmi.radialWheel.splitUnit" : "Podziel jednostkę do wybranego miejsca",
+
 	"vcmi.mainMenu.tutorialNotImplemented" : "Przepraszamy, trening nie został jeszcze zaimplementowany\n",
 	"vcmi.mainMenu.tutorialNotImplemented" : "Przepraszamy, trening nie został jeszcze zaimplementowany\n",
 	"vcmi.mainMenu.highscoresNotImplemented" : "Przepraszamy, najlepsze wyniki nie zostały jeszcze zaimplementowane\n",
 	"vcmi.mainMenu.highscoresNotImplemented" : "Przepraszamy, najlepsze wyniki nie zostały jeszcze zaimplementowane\n",
 	"vcmi.mainMenu.serverConnecting" : "Łączenie...",
 	"vcmi.mainMenu.serverConnecting" : "Łączenie...",
@@ -39,6 +46,9 @@
 	"vcmi.mainMenu.joinTCP" : "Dołącz do gry TCP/IP",
 	"vcmi.mainMenu.joinTCP" : "Dołącz do gry TCP/IP",
 	"vcmi.mainMenu.playerName" : "Gracz",
 	"vcmi.mainMenu.playerName" : "Gracz",
 
 
+	"vcmi.lobby.filename" : "Nazwa pliku",
+	"vcmi.lobby.creationDate" : "Data utworzenia",
+
 	"vcmi.server.errors.existingProcess"     : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej",
 	"vcmi.server.errors.existingProcess"     : "Inny proces 'vcmiserver' został już uruchomiony, zakończ go nim przejdziesz dalej",
 	"vcmi.server.errors.modsIncompatibility" : "Następujące mody są wymagane do wczytania gry:",
 	"vcmi.server.errors.modsIncompatibility" : "Następujące mody są wymagane do wczytania gry:",
 	"vcmi.server.confirmReconnect"           : "Połączyć ponownie z ostatnią sesją?",
 	"vcmi.server.confirmReconnect"           : "Połączyć ponownie z ostatnią sesją?",
@@ -74,6 +84,8 @@
 	"vcmi.systemOptions.longTouchMenu.entry"     : "%d milisekund",
 	"vcmi.systemOptions.longTouchMenu.entry"     : "%d milisekund",
 	"vcmi.systemOptions.framerateButton.hover"  : "Pokaż FPS",
 	"vcmi.systemOptions.framerateButton.hover"  : "Pokaż FPS",
 	"vcmi.systemOptions.framerateButton.help"   : "{Pokaż FPS}\n\n Przełącza widoczność licznika klatek na sekundę (FPS) w rogu okna gry.",
 	"vcmi.systemOptions.framerateButton.help"   : "{Pokaż FPS}\n\n Przełącza widoczność licznika klatek na sekundę (FPS) w rogu okna gry.",
+	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Wibracje urządzenia",
+	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Wibracje urządzenia}\n\nWłącz wibracje na urządzeniu dotykowym",
 
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym",
 	"vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.",
@@ -85,6 +97,10 @@
 	"vcmi.adventureOptions.showGrid.help" : "{Pokaż siatkę}\n\n Włącza siatkę pokazującą brzegi pól mapy przygody.",
 	"vcmi.adventureOptions.showGrid.help" : "{Pokaż siatkę}\n\n Włącza siatkę pokazującą brzegi pól mapy przygody.",
 	"vcmi.adventureOptions.borderScroll.hover" : "Przewijanie na brzegu mapy",
 	"vcmi.adventureOptions.borderScroll.hover" : "Przewijanie na brzegu mapy",
 	"vcmi.adventureOptions.borderScroll.help" : "{Przewijanie na brzegu mapy}\n\nPrzewijanie mapy przygody gdy kursor najeżdża na brzeg okna gry. Może być wyłączone poprzez przytrzymanie klawisza CTRL.",
 	"vcmi.adventureOptions.borderScroll.help" : "{Przewijanie na brzegu mapy}\n\nPrzewijanie mapy przygody gdy kursor najeżdża na brzeg okna gry. Może być wyłączone poprzez przytrzymanie klawisza CTRL.",
+	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Zarządzanie armią w panelu informacyjnym",
+	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Zarządzanie armią w panelu informacyjnym}\n\nPozwala zarządzać jednostkami w panelu informacyjnym, zamiast przełączać między domyślnymi informacjami.",
+	"vcmi.adventureOptions.leftButtonDrag.hover" : "Przeciąganie mapy lewym kliknięciem",
+	"vcmi.adventureOptions.leftButtonDrag.help" : "{Przeciąganie mapy lewym kliknięciem}\n\nGdy włączone, umożliwia przesuwanie mapy przygody poprzez przeciąganie myszy z wciśniętym lewym przyciskiem.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
@@ -111,10 +127,12 @@
 	"vcmi.battleOptions.movementHighlightOnHover.help": "{Pokaż możliwości ruchu po najechaniu}\n\nPodświetla zasięg ruchu jednostki gdy najedziesz na nią myszą.",
 	"vcmi.battleOptions.movementHighlightOnHover.help": "{Pokaż możliwości ruchu po najechaniu}\n\nPodświetla zasięg ruchu jednostki gdy najedziesz na nią myszą.",
 	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Pokaż limit zasięgu dla strzelców",
 	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Pokaż limit zasięgu dla strzelców",
 	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Pokaż limit zasięgu dla strzelców po najechaniu}\n\nPokazuje limity zasięgu jednostki strzeleckiej gdy najedziesz na nią myszą.",
 	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Pokaż limit zasięgu dla strzelców po najechaniu}\n\nPokazuje limity zasięgu jednostki strzeleckiej gdy najedziesz na nią myszą.",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Pokaż trwale statystyki bohaterów",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Pokaż trwale statystyki bohaterów}\n\nWłącza trwałe okna statystyk bohaterów pokazujące umiejętności pierwszorzędne i punkty magii.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Pomiń czekanie startowe",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Pomiń czekanie startowe",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Pomiń czekanie startowe}\n\n Pomija konieczność czekania podczas muzyki startowej, która jest odtwarzana na początku każdej bitwy przed rozpoczęciem akcji.",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Pomiń czekanie startowe}\n\n Pomija konieczność czekania podczas muzyki startowej, która jest odtwarzana na początku każdej bitwy przed rozpoczęciem akcji.",
-	"vcmi.battleWindow.pressKeyToSkipIntro" : "Naciśnij dowolny klawisz by rozpocząć bitwę natychmiastowo",
 
 
+	"vcmi.battleWindow.pressKeyToSkipIntro" : "Naciśnij dowolny klawisz by rozpocząć bitwę natychmiastowo",
 	"vcmi.battleWindow.damageEstimation.melee" : "Atakuj %CREATURE (%DAMAGE).",
 	"vcmi.battleWindow.damageEstimation.melee" : "Atakuj %CREATURE (%DAMAGE).",
 	"vcmi.battleWindow.damageEstimation.meleeKills" : "Atakuj %CREATURE (%DAMAGE, %KILLS).",
 	"vcmi.battleWindow.damageEstimation.meleeKills" : "Atakuj %CREATURE (%DAMAGE, %KILLS).",
 	"vcmi.battleWindow.damageEstimation.ranged" : "Strzelaj do %CREATURE (%SHOTS, %DAMAGE).",
 	"vcmi.battleWindow.damageEstimation.ranged" : "Strzelaj do %CREATURE (%SHOTS, %DAMAGE).",

+ 1 - 2
README.md

@@ -1,7 +1,6 @@
 [![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml)
 [![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml)
 [![Coverity Scan Build Status](https://scan.coverity.com/projects/vcmi/badge.svg)](https://scan.coverity.com/projects/vcmi)
 [![Coverity Scan Build Status](https://scan.coverity.com/projects/vcmi/badge.svg)](https://scan.coverity.com/projects/vcmi)
-[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.2.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.2.1)
-[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.2.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.2.0)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.3.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.3.0)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 # VCMI Project
 # VCMI Project
 VCMI is work-in-progress attempt to recreate engine for Heroes III, giving it new and extended possibilities.
 VCMI is work-in-progress attempt to recreate engine for Heroes III, giving it new and extended possibilities.

+ 16 - 18
client/CPlayerInterface.cpp

@@ -652,26 +652,20 @@ void CPlayerInterface::battleStartBefore(const CCreatureSet *army1, const CCreat
 		waitForAllDialogs();
 		waitForAllDialogs();
 }
 }
 
 
-void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side)
+void CPlayerInterface::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed)
 {
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	bool autoBattleResultRefused = (lastBattleArmies.first == army1 && lastBattleArmies.second == army2);
-	lastBattleArmies.first = army1;
-	lastBattleArmies.second = army2;
-	//quick combat with neutral creatures only
-	auto * army2_object = dynamic_cast<const CGObjectInstance *>(army2);
-	if((!autoBattleResultRefused && !allowBattleReplay && army2_object
-		&& (army2_object->getOwner() == PlayerColor::UNFLAGGABLE || army2_object->getOwner() == PlayerColor::NEUTRAL)
-		&& settings["adventure"]["quickCombat"].Bool())
-		|| settings["adventure"]["alwaysSkipCombat"].Bool())
+
+	bool useQuickCombat = settings["adventure"]["quickCombat"].Bool();
+	bool forceQuickCombat = settings["adventure"]["forceQuickCombat"].Bool();
+
+	if ((replayAllowed && useQuickCombat) || forceQuickCombat)
 	{
 	{
 		autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
 		autofightingAI = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
 		autofightingAI->initBattleInterface(env, cb);
 		autofightingAI->initBattleInterface(env, cb);
-		autofightingAI->battleStart(army1, army2, int3(0,0,0), hero1, hero2, side);
+		autofightingAI->battleStart(army1, army2, tile, hero1, hero2, side, false);
 		isAutoFightOn = true;
 		isAutoFightOn = true;
 		cb->registerBattleInterface(autofightingAI);
 		cb->registerBattleInterface(autofightingAI);
-		// Player shouldn't be able to move on adventure map if quick combat is going
-		allowBattleReplay = true;
 	}
 	}
 
 
 	//Don't wait for dialogs when we are non-active hot-seat player
 	//Don't wait for dialogs when we are non-active hot-seat player
@@ -843,13 +837,17 @@ void CPlayerInterface::battleEnd(const BattleResult *br, QueryID queryID)
 
 
 		if(!battleInt)
 		if(!battleInt)
 		{
 		{
-			bool allowManualReplay = allowBattleReplay && !settings["adventure"]["alwaysSkipCombat"].Bool();
-			allowBattleReplay = false;
+			bool allowManualReplay = queryID != -1;
+
 			auto wnd = std::make_shared<BattleResultWindow>(*br, *this, allowManualReplay);
 			auto wnd = std::make_shared<BattleResultWindow>(*br, *this, allowManualReplay);
-			wnd->resultCallback = [=](ui32 selection)
+
+			if (allowManualReplay)
 			{
 			{
-				cb->selectionMade(selection, queryID);
-			};
+				wnd->resultCallback = [=](ui32 selection)
+				{
+					cb->selectionMade(selection, queryID);
+				};
+			}
 			GH.windows().pushWindow(wnd);
 			GH.windows().pushWindow(wnd);
 			// #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it.
 			// #1490 - during AI turn when quick combat is on, we need to display the message and wait for user to close it.
 			// Otherwise NewTurn causes freeze.
 			// Otherwise NewTurn causes freeze.

+ 1 - 3
client/CPlayerInterface.h

@@ -65,8 +65,6 @@ class CPlayerInterface : public CGameInterface, public IUpdateable
 	int firstCall;
 	int firstCall;
 	int autosaveCount;
 	int autosaveCount;
 
 
-	std::pair<const CCreatureSet *, const CCreatureSet *> lastBattleArmies;
-	bool allowBattleReplay = false;
 	std::list<std::shared_ptr<CInfoWindow>> dialogs; //queue of dialogs awaiting to be shown (not currently shown!)
 	std::list<std::shared_ptr<CInfoWindow>> dialogs; //queue of dialogs awaiting to be shown (not currently shown!)
 	const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr)
 	const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr)
 
 
@@ -169,7 +167,7 @@ protected: // Call-ins from server, should not be called directly, but only via
 	void battleTriggerEffect(const BattleTriggerEffect & bte) override; //various one-shot effect
 	void battleTriggerEffect(const BattleTriggerEffect & bte) override; //various one-shot effect
 	void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
 	void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
 	void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right
 	void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) override; //called by engine just before battle starts; side=0 - left, side=1 - right
-	void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right
+	void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
 	void battleUnitsChanged(const std::vector<UnitChanges> & units) override;
 	void battleUnitsChanged(const std::vector<UnitChanges> & units) override;
 	void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles) override;
 	void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles) override;
 	void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
 	void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack

+ 1 - 1
client/Client.cpp

@@ -580,7 +580,7 @@ void CClient::battleStarted(const BattleInfo * info)
 	auto callBattleStart = [&](PlayerColor color, ui8 side)
 	auto callBattleStart = [&](PlayerColor color, ui8 side)
 	{
 	{
 		if(vstd::contains(battleints, color))
 		if(vstd::contains(battleints, color))
-			battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side);
+			battleints[color]->battleStart(leftSide.armyObject, rightSide.armyObject, info->tile, leftSide.hero, rightSide.hero, side, info->replayAllowed);
 	};
 	};
 	
 	
 	callBattleStart(leftSide.color, 0);
 	callBattleStart(leftSide.color, 0);

+ 2 - 2
client/battle/BattleWindow.cpp

@@ -51,7 +51,7 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 
 
 	REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole);
 	REGISTER_BUILDER("battleConsole", &BattleWindow::buildBattleConsole);
 	
 	
-	const JsonNode config(ResourceID("config/widgets/BattleWindow.json"));
+	const JsonNode config(ResourceID("config/widgets/BattleWindow2.json"));
 	
 	
 	addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this));
 	addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this));
 	addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this));
 	addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this));
@@ -501,7 +501,7 @@ void BattleWindow::bAutofightf()
 
 
 		auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
 		auto ai = CDynLibHandler::getNewBattleAI(settings["server"]["friendlyAI"].String());
 		ai->initBattleInterface(owner.curInt->env, owner.curInt->cb);
 		ai->initBattleInterface(owner.curInt->env, owner.curInt->cb);
-		ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide());
+		ai->battleStart(owner.army1, owner.army2, int3(0,0,0), owner.attackingHeroInstance, owner.defendingHeroInstance, owner.curInt->cb->battleGetMySide(), false);
 		owner.curInt->autofightingAI = ai;
 		owner.curInt->autofightingAI = ai;
 		owner.curInt->cb->registerBattleInterface(ai);
 		owner.curInt->cb->registerBattleInterface(ai);
 
 

+ 1 - 1
client/gui/ShortcutHandler.cpp

@@ -76,7 +76,7 @@ std::vector<EShortcut> ShortcutHandler::translateKeycode(SDL_Keycode key) const
 		{SDLK_r,         EShortcut::GAME_RESTART_GAME         },
 		{SDLK_r,         EShortcut::GAME_RESTART_GAME         },
 		{SDLK_m,         EShortcut::GAME_TO_MAIN_MENU         },
 		{SDLK_m,         EShortcut::GAME_TO_MAIN_MENU         },
 		{SDLK_q,         EShortcut::GAME_QUIT_GAME            },
 		{SDLK_q,         EShortcut::GAME_QUIT_GAME            },
-		{SDLK_t,         EShortcut::GAME_OPEN_MARKETPLACE     },
+		{SDLK_b,         EShortcut::GAME_OPEN_MARKETPLACE     },
 		{SDLK_g,         EShortcut::GAME_OPEN_THIEVES_GUILD   },
 		{SDLK_g,         EShortcut::GAME_OPEN_THIEVES_GUILD   },
 		{SDLK_TAB,       EShortcut::GAME_ACTIVATE_CONSOLE     },
 		{SDLK_TAB,       EShortcut::GAME_ACTIVATE_CONSOLE     },
 		{SDLK_o,         EShortcut::ADVENTURE_GAME_OPTIONS    },
 		{SDLK_o,         EShortcut::ADVENTURE_GAME_OPTIONS    },

+ 13 - 22
client/renderSDL/CursorHardware.cpp

@@ -11,6 +11,7 @@
 #include "StdInc.h"
 #include "StdInc.h"
 #include "CursorHardware.h"
 #include "CursorHardware.h"
 
 
+#include "../gui/CGuiHandler.h"
 #include "../render/Colors.h"
 #include "../render/Colors.h"
 #include "../render/IImage.h"
 #include "../render/IImage.h"
 #include "SDL_Extensions.h"
 #include "SDL_Extensions.h"
@@ -18,10 +19,6 @@
 #include <SDL_render.h>
 #include <SDL_render.h>
 #include <SDL_events.h>
 #include <SDL_events.h>
 
 
-#ifdef VCMI_APPLE
-#include <dispatch/dispatch.h>
-#endif
-
 CursorHardware::CursorHardware():
 CursorHardware::CursorHardware():
 	cursor(nullptr)
 	cursor(nullptr)
 {
 {
@@ -36,16 +33,13 @@ CursorHardware::~CursorHardware()
 
 
 void CursorHardware::setVisible(bool on)
 void CursorHardware::setVisible(bool on)
 {
 {
-#ifdef VCMI_APPLE
-	dispatch_async(dispatch_get_main_queue(), ^{
-#endif
-	if (on)
-		SDL_ShowCursor(SDL_ENABLE);
-	else
-		SDL_ShowCursor(SDL_DISABLE);
-#ifdef VCMI_APPLE
+	GH.dispatchMainThread([on]()
+	{
+		if (on)
+			SDL_ShowCursor(SDL_ENABLE);
+		else
+			SDL_ShowCursor(SDL_DISABLE);
 	});
 	});
-#endif
 }
 }
 
 
 void CursorHardware::setImage(std::shared_ptr<IImage> image, const Point & pivotOffset)
 void CursorHardware::setImage(std::shared_ptr<IImage> image, const Point & pivotOffset)
@@ -63,16 +57,13 @@ void CursorHardware::setImage(std::shared_ptr<IImage> image, const Point & pivot
 		logGlobal->error("Failed to set cursor! SDL says %s", SDL_GetError());
 		logGlobal->error("Failed to set cursor! SDL says %s", SDL_GetError());
 
 
 	SDL_FreeSurface(cursorSurface);
 	SDL_FreeSurface(cursorSurface);
-#ifdef VCMI_APPLE
-	dispatch_async(dispatch_get_main_queue(), ^{
-#endif
-	SDL_SetCursor(cursor);
-
-	if (oldCursor)
-		SDL_FreeCursor(oldCursor);
-#ifdef VCMI_APPLE
+
+	GH.dispatchMainThread([this, oldCursor](){
+		SDL_SetCursor(cursor);
+
+		if (oldCursor)
+			SDL_FreeCursor(oldCursor);
 	});
 	});
-#endif
 }
 }
 
 
 void CursorHardware::setCursorPosition( const Point & newPos )
 void CursorHardware::setCursorPosition( const Point & newPos )

+ 5 - 0
client/widgets/Buttons.cpp

@@ -18,6 +18,7 @@
 #include "../CPlayerInterface.h"
 #include "../CPlayerInterface.h"
 #include "../battle/BattleInterface.h"
 #include "../battle/BattleInterface.h"
 #include "../battle/BattleInterfaceClasses.h"
 #include "../battle/BattleInterfaceClasses.h"
+#include "../eventsSDL/InputHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/MouseButton.h"
 #include "../gui/MouseButton.h"
 #include "../gui/Shortcut.h"
 #include "../gui/Shortcut.h"
@@ -163,7 +164,10 @@ void CButton::clickPressed(const Point & cursorPosition)
 	if (getState() != PRESSED)
 	if (getState() != PRESSED)
 	{
 	{
 		if (!soundDisabled)
 		if (!soundDisabled)
+		{
 			CCS->soundh->playSound(soundBase::button);
 			CCS->soundh->playSound(soundBase::button);
+			GH.input().hapticFeedback();
+		}
 		setState(PRESSED);
 		setState(PRESSED);
 
 
 		if (actOnDown)
 		if (actOnDown)
@@ -390,6 +394,7 @@ void CToggleButton::clickPressed(const Point & cursorPosition)
 	if (canActivate())
 	if (canActivate())
 	{
 	{
 		CCS->soundh->playSound(soundBase::button);
 		CCS->soundh->playSound(soundBase::button);
+		GH.input().hapticFeedback();
 		setState(PRESSED);
 		setState(PRESSED);
 	}
 	}
 }
 }

+ 9 - 0
client/windows/CCastleInterface.cpp

@@ -15,6 +15,7 @@
 #include "InfoWindows.h"
 #include "InfoWindows.h"
 #include "GUIClasses.h"
 #include "GUIClasses.h"
 #include "QuickRecruitmentWindow.h"
 #include "QuickRecruitmentWindow.h"
+#include "CCreatureWindow.h"
 
 
 #include "../CGameInfo.h"
 #include "../CGameInfo.h"
 #include "../CMusicHandler.h"
 #include "../CMusicHandler.h"
@@ -1652,6 +1653,8 @@ CFortScreen::RecruitArea::RecruitArea(int posX, int posY, const CGTownInstance *
 	if(!town->creatures[level].second.empty())
 	if(!town->creatures[level].second.empty())
 		addUsedEvents(LCLICK | HOVER);//Activate only if dwelling is present
 		addUsedEvents(LCLICK | HOVER);//Activate only if dwelling is present
 
 
+	addUsedEvents(SHOW_POPUP);
+
 	icons = std::make_shared<CPicture>("TPCAINFO", 261, 3);
 	icons = std::make_shared<CPicture>("TPCAINFO", 261, 3);
 
 
 	if(getMyBuilding() != nullptr)
 	if(getMyBuilding() != nullptr)
@@ -1739,6 +1742,12 @@ void CFortScreen::RecruitArea::clickPressed(const Point & cursorPosition)
 	LOCPLINT->castleInt->builds->enterDwelling(level);
 	LOCPLINT->castleInt->builds->enterDwelling(level);
 }
 }
 
 
+void CFortScreen::RecruitArea::showPopupWindow(const Point & cursorPosition)
+{
+	if (getMyCreature() != nullptr)
+		GH.windows().createAndPushWindow<CStackWindow>(getMyCreature(), true);
+}
+
 CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner,std::string imagem)
 CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner,std::string imagem)
 	: CStatusbarWindow(BORDERED, imagem)
 	: CStatusbarWindow(BORDERED, imagem)
 {
 {

+ 2 - 0
client/windows/CCastleInterface.h

@@ -347,6 +347,8 @@ class CFortScreen : public CStatusbarWindow
 		void creaturesChangedEventHandler();
 		void creaturesChangedEventHandler();
 		void hover(bool on) override;
 		void hover(bool on) override;
 		void clickPressed(const Point & cursorPosition) override;
 		void clickPressed(const Point & cursorPosition) override;
+		void showPopupWindow(const Point & cursorPosition) override;
+
 	};
 	};
 	std::shared_ptr<CLabel> title;
 	std::shared_ptr<CLabel> title;
 	std::vector<std::shared_ptr<RecruitArea>> recAreas;
 	std::vector<std::shared_ptr<RecruitArea>> recAreas;

+ 155 - 105
config/ai/object-priorities.txt

@@ -5,10 +5,10 @@ InputVariable: mainTurnDistance
   enabled: true
   enabled: true
   range: 0.000 10.000
   range: 0.000 10.000
   lock-range: true
   lock-range: true
-  term: LOWEST Ramp 0.250 0.000
-  term: LOW Discrete 0.000 1.000 0.500 0.800 1.000 0.000
-  term: MEDIUM Discrete 0.000 0.000 0.500 0.200 1.000 1.000 3.000 0.000
-  term: LONG Discrete 1.000 0.000 1.500 0.200 3.000 0.800 10.000 1.000
+  term: LOWEST Ramp 0.400 0.000
+  term: LOW Discrete 0.000 1.000 0.500 0.800 0.800 0.300 2.000 0.000
+  term: MEDIUM Discrete 0.000 0.000 0.500 0.200 0.800 0.700 2.000 1.000 6.000 0.000
+  term: LONG Discrete 2.000 0.000 6.000 1.000 10.000 0.800
 InputVariable: scoutTurnDistance
 InputVariable: scoutTurnDistance
   description: distance to tile in turns
   description: distance to tile in turns
   enabled: true
   enabled: true
@@ -23,11 +23,11 @@ InputVariable: goldReward
   enabled: true
   enabled: true
   range: 0.000 5000.000
   range: 0.000 5000.000
   lock-range: true
   lock-range: true
-  term: LOW Triangle 10.000 500.000 2000.000
-  term: MEDIUM Triangle 500.000 2000.000 5000.000
-  term: HIGH Ramp 2000.000 5000.000
-  term: NONE Ramp 100.000 0.000
-  term: LOWEST Triangle 0.000 100.000 500.000
+  term: LOWEST Triangle 0.000 100.000 200.000
+  term: SMALL Triangle 100.000 200.000 400.000
+  term: MEDIUM Triangle 200.000 400.000 1000.000
+  term: BIG Triangle 400.000 1000.000 5000.000
+  term: HUGE Ramp 1000.000 5000.000
 InputVariable: armyReward
 InputVariable: armyReward
   enabled: true
   enabled: true
   range: 0.000 10000.000
   range: 0.000 10000.000
@@ -43,6 +43,7 @@ InputVariable: armyLoss
   term: LOW Ramp 0.200 0.000
   term: LOW Ramp 0.200 0.000
   term: MEDIUM Triangle 0.000 0.200 0.500
   term: MEDIUM Triangle 0.000 0.200 0.500
   term: HIGH Ramp 0.200 0.500
   term: HIGH Ramp 0.200 0.500
+  term: ALL Ramp 0.700 1.000
 InputVariable: heroRole
 InputVariable: heroRole
   enabled: true
   enabled: true
   range: -0.100 1.100
   range: -0.100 1.100
@@ -82,20 +83,21 @@ InputVariable: closestHeroRatio
 InputVariable: strategicalValue
 InputVariable: strategicalValue
   description: Some abstract long term benefit non gold or army or skill
   description: Some abstract long term benefit non gold or army or skill
   enabled: true
   enabled: true
-  range: 0.000 1.000
+  range: 0.000 3.000
   lock-range: false
   lock-range: false
   term: NONE Ramp 0.200 0.000
   term: NONE Ramp 0.200 0.000
   term: LOWEST Triangle 0.000 0.010 0.250
   term: LOWEST Triangle 0.000 0.010 0.250
-  term: LOW Triangle 0.000 0.250 0.700
-  term: MEDIUM Triangle 0.250 0.700 1.000
-  term: HIGH Ramp 0.700 1.000
+  term: LOW Triangle 0.000 0.250 1.000
+  term: MEDIUM Triangle 0.250 1.000 2.000
+  term: HIGH Triangle 1.000 2.000 3.000
+  term: CRITICAL Ramp 2.000 3.000
 InputVariable: goldPreasure
 InputVariable: goldPreasure
   description: Ratio between weekly army cost and gold income
   description: Ratio between weekly army cost and gold income
   enabled: true
   enabled: true
   range: 0.000 1.000
   range: 0.000 1.000
   lock-range: false
   lock-range: false
   term: LOW Ramp 0.300 0.000
   term: LOW Ramp 0.300 0.000
-  term: HIGH Discrete 0.100 0.000 0.250 0.100 0.300 0.200 0.400 0.700 1.000 1.000
+  term: HIGH Discrete 0.100 0.000 0.250 0.200 0.300 0.300 0.400 0.700 1.000 1.000
 InputVariable: goldCost
 InputVariable: goldCost
   description: Action cost in gold
   description: Action cost in gold
   enabled: true
   enabled: true
@@ -121,106 +123,154 @@ InputVariable: fear
   term: LOW Triangle 0.000 0.500 1.000
   term: LOW Triangle 0.000 0.500 1.000
   term: MEDIUM Triangle 0.500 1.000 1.500
   term: MEDIUM Triangle 0.500 1.000 1.500
   term: HIGH Ramp 1.000 1.800
   term: HIGH Ramp 1.000 1.800
+InputVariable: armyGrowth
+  enabled: true
+  range: 0.000 20000.000
+  lock-range: false
+  term: NONE Ramp 100.000 0.000
+  term: SMALL Triangle 0.000 1000.000 3000.000
+  term: MEDIUM Triangle 1000.000 3000.000 8000.000
+  term: BIG Triangle 3000.000 8000.000 20000.000
+  term: HUGE Ramp 8000.000 20000.000
 OutputVariable: Value
 OutputVariable: Value
   enabled: true
   enabled: true
-  range: -0.500 1.500
+  range: -1.500 2.500
   lock-range: false
   lock-range: false
   aggregation: AlgebraicSum
   aggregation: AlgebraicSum
   defuzzifier: Centroid 100
   defuzzifier: Centroid 100
   default: 0.500
   default: 0.500
   lock-previous: false
   lock-previous: false
-  term: LOWEST Discrete -0.500 0.000 -0.500 1.000 -0.200 1.000 -0.200 0.000 0.200 0.000 0.200 1.000 0.500 1.000 0.500 0.000 0.500
-  term: BITLOW Rectangle -0.010 0.010 0.500
-  term: LOW Discrete -0.150 0.000 -0.150 1.000 -0.050 1.000 -0.050 0.000 0.050 0.000 0.050 1.000 0.150 1.000 0.150 0.000 0.500
-  term: MEDIUM Triangle 0.450 0.500 0.550 0.050
-  term: HIGH Discrete 0.850 0.000 0.850 1.000 0.950 1.000 0.950 0.000 1.050 0.000 1.050 1.000 1.150 1.000 1.150 0.000 0.500
-  term: HIGHEST Discrete 0.500 0.000 0.500 1.000 0.800 1.000 0.800 0.000 1.200 0.000 1.200 1.000 1.500 1.000 1.500 0.000 0.500
-  term: BITHIGH Rectangle 0.990 1.010 0.500
-RuleBlock: gold reward
+  term: WORST Binary -1.000 -inf 0.700
+  term: BAD Rectangle -1.000 -0.700 0.500
+  term: BASE Rectangle -0.200 0.200 0.350
+  term: MEDIUM Rectangle 0.910 1.090 0.500
+  term: SMALL Rectangle 0.960 1.040 0.600
+  term: BITHIGH Rectangle 0.850 1.150 0.400
+  term: HIGH Rectangle 0.750 1.250 0.400
+  term: HIGHEST Rectangle 0.500 1.500 0.350
+  term: CRITICAL Ramp 0.500 2.000 0.500
+RuleBlock: basic
+  enabled: true
+  conjunction: AlgebraicProduct
+  disjunction: AlgebraicSum
+  implication: AlgebraicProduct
+  activation: General
+  rule: if heroRole is MAIN then Value is BASE
+  rule: if heroRole is SCOUT then Value is BASE
+  rule: if heroRole is MAIN and armyGrowth is HUGE and fear is not HIGH then Value is HIGH
+  rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LOW then Value is HIGH
+  rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is MAIN and armyGrowth is BIG and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
+  rule: if armyLoss is ALL then Value is WORST
+  rule: if turn is not NOW then Value is BAD with 0.1
+  rule: if closestHeroRatio is LOWEST and heroRole is SCOUT then Value is WORST
+  rule: if closestHeroRatio is LOW and heroRole is SCOUT then Value is BAD
+  rule: if closestHeroRatio is LOWEST and heroRole is MAIN then Value is BAD
+  rule: if heroRole is SCOUT and turn is NOW and mainTurnDistance is LONG then Value is WORST
+  rule: if heroRole is SCOUT and turn is NOW and mainTurnDistance is MEDIUM then Value is BAD
+  rule: if heroRole is SCOUT and turn is NEXT and mainTurnDistance is LONG then Value is BAD
+  rule: if heroRole is SCOUT and turn is NOW and scoutTurnDistance is LONG then Value is BAD
+  rule: if heroRole is SCOUT and fear is HIGH then Value is BAD with 0.8
+  rule: if heroRole is SCOUT and fear is MEDIUM then Value is BAD with 0.5
+  rule: if heroRole is MAIN and fear is HIGH then Value is BAD with 0.5
+  rule: if heroRole is MAIN and fear is MEDIUM then Value is BAD with 0.2
+RuleBlock: strategicalValue
+  enabled: true
+  conjunction: AlgebraicProduct
+  disjunction: NormalizedSum
+  implication: AlgebraicProduct
+  activation: General
+  rule: if heroRole is MAIN and strategicalValue is HIGH and turn is NOW then Value is HIGHEST
+  rule: if heroRole is MAIN and strategicalValue is HIGH and turn is not NOW and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST
+  rule: if heroRole is MAIN and strategicalValue is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGHEST with 0.5
+  rule: if heroRole is MAIN and strategicalValue is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is HIGH
+  rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LOW then Value is HIGH
+  rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is MAIN and strategicalValue is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is MAIN and strategicalValue is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and turn is NOW then Value is HIGH
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and turn is not NOW and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGH
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH with 0.5
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is not NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is LOW then Value is BITHIGH
+  rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is not NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is SMALL
+  rule: if heroRole is SCOUT and strategicalValue is LOW and danger is not NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is SMALL
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and turn is NOW then Value is HIGHEST
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and turn is not NOW and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGHEST
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGHEST with 0.5
+  rule: if heroRole is SCOUT and strategicalValue is HIGH and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is HIGH
+  rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is LOW then Value is HIGH
+  rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is SCOUT and strategicalValue is MEDIUM and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is SCOUT and strategicalValue is LOW and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL
+  rule: if armyLoss is HIGH and strategicalValue is LOW then Value is BAD
+  rule: if armyLoss is HIGH and strategicalValue is MEDIUM then Value is BAD with 0.7
+  rule: if strategicalValue is CRITICAL and heroRole is MAIN then Value is CRITICAL
+  rule: if strategicalValue is CRITICAL and heroRole is SCOUT then Value is CRITICAL with 0.7
+RuleBlock: armyReward
+  enabled: true
+  conjunction: AlgebraicProduct
+  disjunction: AlgebraicSum
+  implication: AlgebraicProduct
+  activation: General
+  rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST
+  rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH
+  rule: if heroRole is MAIN and armyReward is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LOW then Value is HIGH
+  rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is MAIN and armyReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is MAIN and armyReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL
+  rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is HIGH
+  rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH with 0.7
+  rule: if heroRole is SCOUT and armyReward is HIGH and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is LOW then Value is HIGH with 0.7
+  rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is SCOUT and armyReward is MEDIUM and danger is NONE and scoutTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is SCOUT and armyReward is LOW and danger is NONE and scoutTurnDistance is LOW and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is SCOUT and armyReward is LOW and danger is NONE and scoutTurnDistance is MEDIUM and fear is not HIGH then Value is SMALL
+RuleBlock: gold
+  enabled: true
+  conjunction: AlgebraicProduct
+  disjunction: AlgebraicSum
+  implication: AlgebraicProduct
+  activation: General
+  rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGHEST
+  rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is BITHIGH
+  rule: if goldReward is HUGE and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGHEST
+  rule: if goldReward is HUGE and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is HIGH
+  rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH
+  rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE then Value is MEDIUM
+  rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGH
+  rule: if goldReward is BIG and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is MEDIUM
+  rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is BITHIGH
+  rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE then Value is SMALL
+  rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH
+  rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is MEDIUM
+  rule: if goldReward is SMALL and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is SMALL
+  rule: if goldReward is LOWEST then Value is SMALL with 0.1
+  rule: if goldReward is SMALL then Value is SMALL with 0.2
+  rule: if goldReward is MEDIUM then Value is SMALL with 0.5
+  rule: if goldReward is BIG then Value is SMALL
+  rule: if goldReward is HUGE then Value is BITHIGH
+RuleBlock: skill reward
   enabled: true
   enabled: true
   conjunction: AlgebraicProduct
   conjunction: AlgebraicProduct
   disjunction: AlgebraicSum
   disjunction: AlgebraicSum
   implication: AlgebraicProduct
   implication: AlgebraicProduct
   activation: General
   activation: General
-  rule: if turn is NOW and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOW with 0.5
-  rule: if turn is NOW and mainTurnDistance is LONG and heroRole is SCOUT then Value is LOW with 0.3
-  rule: if turn is NOW and scoutTurnDistance is LONG and heroRole is SCOUT then Value is LOW with 0.3
-  rule: if turn is NOW and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW with 0.3
-  rule: if turn is NEXT and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOW with 0.8
-  rule: if turn is NEXT and scoutTurnDistance is LONG and heroRole is SCOUT then Value is BITLOW
-  rule: if turn is NEXT and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW with 0.3
-  rule: if turn is NEXT and mainTurnDistance is LONG and heroRole is SCOUT then Value is BITLOW with 0.3
-  rule: if turn is FUTURE and scoutTurnDistance is very LONG and heroRole is SCOUT then Value is LOWEST with 0.3
-  rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is SCOUT then Value is LOWEST with 0.5
-  rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is NONE then Value is LOWEST with 0.5
-  rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is LOW then Value is LOWEST with 0.3
-  rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is MEDIUM then Value is LOW with 0.5
-  rule: if turn is FUTURE and mainTurnDistance is very LONG and heroRole is MAIN and strategicalValue is HIGH then Value is BITLOW
-  rule: if turn is FUTURE and scoutTurnDistance is LONG and heroRole is SCOUT then Value is LOW
-  rule: if turn is FUTURE and mainTurnDistance is LONG and heroRole is MAIN then Value is LOW
-  rule: if turn is FUTURE and mainTurnDistance is LONG and heroRole is SCOUT then Value is LOW
-  rule: if scoutTurnDistance is MEDIUM and heroRole is SCOUT then Value is BITLOW
-  rule: if mainTurnDistance is MEDIUM then Value is BITLOW
-  rule: if scoutTurnDistance is LOW and heroRole is SCOUT then Value is MEDIUM
-  rule: if mainTurnDistance is LOW then Value is MEDIUM
-  rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is BITHIGH
-  rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH with 0.7
-  rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST
-  rule: if goldReward is HIGH and goldPreasure is HIGH and heroRole is MAIN and danger is NONE then Value is BITHIGH
-  rule: if goldReward is MEDIUM and goldPreasure is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGH
-  rule: if goldReward is MEDIUM and goldPreasure is HIGH and armyLoss is LOW and heroRole is SCOUT and danger is not NONE then Value is MEDIUM
-  rule: if goldReward is MEDIUM and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is BITLOW
-  rule: if goldReward is MEDIUM and goldPreasure is HIGH and armyLoss is LOW and heroRole is MAIN and danger is not NONE then Value is BITHIGH
-  rule: if goldReward is LOW and goldPreasure is HIGH and heroRole is SCOUT and armyLoss is LOW then Value is BITHIGH
-  rule: if goldReward is LOW and heroRole is MAIN and danger is not NONE and rewardType is SINGLE and armyLoss is LOW then Value is BITLOW
-  rule: if goldReward is LOW and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is LOW
-  rule: if goldReward is LOWEST and heroRole is MAIN and danger is NONE and rewardType is SINGLE then Value is LOWEST
-  rule: if armyReward is HIGH and heroRole is SCOUT and danger is not NONE and armyLoss is LOW then Value is HIGH with 0.5
-  rule: if armyReward is HIGH and heroRole is SCOUT and danger is NONE then Value is HIGHEST
-  rule: if armyReward is HIGH and heroRole is MAIN and rewardType is MIXED and armyLoss is LOW and fear is not HIGH then Value is HIGHEST
-  rule: if armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and mainTurnDistance is LOWEST then Value is HIGHEST
-  rule: if armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and danger is NONE and fear is not HIGH then Value is HIGH
-  rule: if armyReward is HIGH and heroRole is MAIN and rewardType is SINGLE and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST
-  rule: if armyReward is MEDIUM and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST with 0.5
-  rule: if armyReward is MEDIUM and heroRole is MAIN and danger is NONE then Value is BITHIGH
-  rule: if armyReward is MEDIUM and heroRole is MAIN and danger is NONE and mainTurnDistance is LOWEST then Value is HIGH with 0.2
-  rule: if armyReward is MEDIUM and heroRole is SCOUT and danger is NONE then Value is HIGHEST with 0.5
-  rule: if armyReward is LOW and heroRole is SCOUT and danger is NONE then Value is HIGH
-  rule: if armyReward is LOW and heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is HIGH
-  rule: if armyReward is LOW and heroRole is MAIN and danger is NONE then Value is BITLOW with 0.5
-  rule: if armyReward is LOW and heroRole is MAIN and danger is NONE and mainTurnDistance is LOWEST then Value is HIGH
-  rule: if skillReward is LOW and heroRole is MAIN and armyLoss is LOW then Value is BITHIGH
-  rule: if skillReward is MEDIUM and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is BITHIGH
-  rule: if skillReward is MEDIUM and heroRole is MAIN and rewardType is MIXED and armyLoss is LOW and fear is not HIGH then Value is HIGH with 0.5
-  rule: if skillReward is HIGH and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGH
-  rule: if skillReward is MEDIUM and heroRole is SCOUT then Value is LOWEST
-  rule: if skillReward is HIGH and heroRole is SCOUT then Value is LOWEST
-  rule: if strategicalValue is LOW and heroRole is MAIN and armyLoss is LOW then Value is BITHIGH
-  rule: if strategicalValue is LOWEST and heroRole is MAIN and armyLoss is LOW then Value is LOW
-  rule: if strategicalValue is LOW and heroRole is SCOUT and armyLoss is LOW and fear is not HIGH then Value is HIGH with 0.5
-  rule: if strategicalValue is MEDIUM and heroRole is SCOUT and danger is NONE and fear is not HIGH then Value is HIGH
-  rule: if strategicalValue is HIGH and heroRole is SCOUT and danger is NONE and fear is not HIGH then Value is HIGHEST with 0.5
-  rule: if strategicalValue is HIGH and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGHEST
-  rule: if strategicalValue is HIGH and heroRole is MAIN and armyLoss is MEDIUM and fear is not HIGH then Value is HIGH
-  rule: if strategicalValue is MEDIUM and heroRole is MAIN and armyLoss is LOW and fear is not HIGH then Value is HIGH
-  rule: if rewardType is NONE then Value is LOWEST
-  rule: if armyLoss is HIGH and strategicalValue is not HIGH and heroRole is MAIN then Value is LOWEST
-  rule: if armyLoss is HIGH and strategicalValue is HIGH and heroRole is MAIN then Value is LOW
-  rule: if armyLoss is HIGH and heroRole is SCOUT then Value is LOWEST
-  rule: if heroRole is SCOUT and closestHeroRatio is LOW then Value is LOW
-  rule: if heroRole is SCOUT and closestHeroRatio is LOWEST then Value is LOWEST
-  rule: if heroRole is MAIN and danger is NONE and skillReward is NONE and rewardType is SINGLE and closestHeroRatio is LOW then Value is LOW
-  rule: if heroRole is MAIN and danger is NONE and skillReward is NONE and rewardType is SINGLE and closestHeroRatio is LOWEST then Value is LOWEST
-  rule: if heroRole is MAIN and danger is not NONE and armyLoss is LOW then Value is BITHIGH with 0.2
-  rule: if heroRole is SCOUT then Value is BITLOW
-  rule: if goldCost is not NONE and goldReward is NONE and goldPreasure is HIGH then Value is LOWEST
-  rule: if turn is NOW then Value is LOW with 0.3
-  rule: if turn is not NOW then Value is LOW with 0.4
-  rule: if goldPreasure is HIGH and goldReward is HIGH and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST
-  rule: if goldPreasure is HIGH and goldReward is MEDIUM and heroRole is MAIN and danger is not NONE and armyLoss is LOW and fear is not HIGH then Value is HIGH
-  rule: if goldPreasure is HIGH and goldReward is HIGH and heroRole is SCOUT and danger is NONE and armyLoss is LOW and fear is not HIGH then Value is HIGHEST
-  rule: if goldPreasure is HIGH and goldReward is MEDIUM and heroRole is SCOUT and danger is NONE and armyLoss is LOW and fear is not HIGH then Value is HIGH
-  rule: if goldPreasure is HIGH and goldReward is LOW and heroRole is SCOUT and armyLoss is LOW then Value is BITHIGH
-  rule: if goldPreasure is HIGH and goldReward is LOW and heroRole is SCOUT and scoutTurnDistance is LOW and armyLoss is LOW then Value is HIGH with 0.5
-  rule: if fear is MEDIUM then Value is LOW
-  rule: if fear is HIGH then Value is LOWEST
+  rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGH
+  rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGHEST
+  rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LOWEST and fear is not HIGH then Value is HIGHEST
+  rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LOW and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LOW and fear is not HIGH then Value is HIGH
+  rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LOW and fear is not HIGH then Value is HIGHEST
+  rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is MEDIUM and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is MEDIUM and fear is not HIGH then Value is BITHIGH
+  rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is MEDIUM and fear is not HIGH then Value is HIGH
+  rule: if heroRole is MAIN and skillReward is LOW and mainTurnDistance is LONG and fear is not HIGH then Value is SMALL
+  rule: if heroRole is MAIN and skillReward is MEDIUM and mainTurnDistance is LONG and fear is not HIGH then Value is MEDIUM
+  rule: if heroRole is MAIN and skillReward is HIGH and mainTurnDistance is LONG and fear is not HIGH then Value is BITHIGH

+ 1 - 1
config/objects/rewardableOncePerWeek.json

@@ -151,7 +151,7 @@
 						"message" : 170,
 						"message" : 170,
 						"resources" : [
 						"resources" : [
 							{
 							{
-								"list" : [ "ore", "mercury", "gems", "sulfur", "crystal" ],
+								"anyOf" : [ "ore", "mercury", "gems", "sulfur", "crystal" ],
 								"min" : 3,
 								"min" : 3,
 								"max" : 6
 								"max" : 6
 							}
 							}

+ 2 - 2
config/objects/rewardableOnceVisitable.json

@@ -27,7 +27,7 @@
 						"message" : 64,
 						"message" : 64,
 						"resources" : [
 						"resources" : [
 							{
 							{
-								"list" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ],
+								"anyOf" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ],
 								"min" : 1,
 								"min" : 1,
 								"max" : 5
 								"max" : 5
 							}
 							}
@@ -115,7 +115,7 @@
 						"appearChance" : { "min" : 40, "max" : 90 },
 						"appearChance" : { "min" : 40, "max" : 90 },
 						"resources" : [
 						"resources" : [
 							{
 							{
-								"list" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ],
+								"anyOf" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ],
 								"min" : 2,
 								"min" : 2,
 								"max" : 5
 								"max" : 5
 							},
 							},

+ 1 - 1
config/objects/rewardablePickable.json

@@ -30,7 +30,7 @@
 						"removeObject" : true,
 						"removeObject" : true,
 						"resources" : [
 						"resources" : [
 							{
 							{
-								"list" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ],
+								"anyOf" : [ "wood", "ore", "mercury", "gems", "sulfur", "crystal" ],
 								"min" : 4,
 								"min" : 4,
 								"max" : 6
 								"max" : 6
 							},
 							},

+ 2 - 2
config/schemas/settings.json

@@ -223,7 +223,7 @@
 			"type" : "object",
 			"type" : "object",
 			"additionalProperties" : false,
 			"additionalProperties" : false,
 			"default" : {},
 			"default" : {},
-			"required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "alwaysSkipCombat", "borderScroll", "leftButtonDrag" ],
+			"required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag" ],
 			"properties" : {
 			"properties" : {
 				"heroMoveTime" : {
 				"heroMoveTime" : {
 					"type" : "number",
 					"type" : "number",
@@ -253,7 +253,7 @@
 					"type" : "boolean",
 					"type" : "boolean",
 					"default" : true
 					"default" : true
 				},
 				},
-				"alwaysSkipCombat" : {
+				"forceQuickCombat" : {
 					"type" : "boolean",
 					"type" : "boolean",
 					"default" : false
 					"default" : false
 				},
 				},

+ 0 - 0
config/widgets/battleWindow.json → config/widgets/battleWindow2.json


+ 3 - 3
config/widgets/settings/adventureOptionsTab.json

@@ -7,7 +7,7 @@
 			"name": "lineLabelsEnd",
 			"name": "lineLabelsEnd",
 			"type": "texture",
 			"type": "texture",
 			"image": "settingsWindow/lineHorizontal",
 			"image": "settingsWindow/lineHorizontal",
-			"rect": { "x" : 5, "y" : 289, "w": 365, "h": 3}
+			"rect": { "x" : 5, "y" : 229, "w": 365, "h": 3}
 		},
 		},
 /////////////////////////////////////// Left section - Hero Speed and Map Scrolling
 /////////////////////////////////////// Left section - Hero Speed and Map Scrolling
 		{
 		{
@@ -323,7 +323,7 @@
 		{
 		{
 			"type": "verticalLayout",
 			"type": "verticalLayout",
 			"customType": "labelDescription",
 			"customType": "labelDescription",
-			"position": {"x": 45, "y": 295},
+			"position": {"x": 45, "y": 235},
 			"items":
 			"items":
 			[
 			[
 				{
 				{
@@ -353,7 +353,7 @@
 		{
 		{
 			"type": "verticalLayout",
 			"type": "verticalLayout",
 			"customType": "checkbox",
 			"customType": "checkbox",
-			"position": {"x": 10, "y": 293},
+			"position": {"x": 10, "y": 233},
 			"items":
 			"items":
 			[
 			[
 				{
 				{

+ 1 - 1
debian/changelog

@@ -8,7 +8,7 @@ vcmi (1.3.0) jammy; urgency=medium
 
 
   * New upstream release
   * New upstream release
 
 
- -- Ivan Savenko <[email protected]>  Sat, 01 Jul 2023 16:00:00 +0200
+ -- Ivan Savenko <[email protected]>  Fri, 04 Aug 2023 16:00:00 +0200
 
 
 vcmi (1.2.1) jammy; urgency=medium
 vcmi (1.2.1) jammy; urgency=medium
 
 

+ 1 - 1
launcher/eu.vcmi.VCMI.metainfo.xml

@@ -52,7 +52,7 @@
 	</categories>
 	</categories>
 	<releases>
 	<releases>
 		<release version="1.4.0" date="2023-12-22" type="development" />
 		<release version="1.4.0" date="2023-12-22" type="development" />
-		<release version="1.3.0" date="2023-07-01" type="development" />
+		<release version="1.3.0" date="2023-08-04" />
 		<release version="1.2.1" date="2023-04-28" />
 		<release version="1.2.1" date="2023-04-28" />
 		<release version="1.2.0" date="2023-04-14" />
 		<release version="1.2.0" date="2023-04-14" />
 		<release version="1.1.1" date="2023-02-03" />
 		<release version="1.1.1" date="2023-02-03" />

+ 4 - 4
launcher/translation/german.ts

@@ -472,7 +472,7 @@
     <message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="319"/>
         <location filename="../settingsView/csettingsview_moc.ui" line="319"/>
         <source>Autosave limit (0 = off)</source>
         <source>Autosave limit (0 = off)</source>
-        <translation type="unfinished"></translation>
+        <translation>Limit für Autospeicherung (0 = aus)</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="329"/>
         <location filename="../settingsView/csettingsview_moc.ui" line="329"/>
@@ -487,12 +487,12 @@
     <message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="612"/>
         <location filename="../settingsView/csettingsview_moc.ui" line="612"/>
         <source>Autosave prefix</source>
         <source>Autosave prefix</source>
-        <translation type="unfinished"></translation>
+        <translation>Präfix für Autospeicherung</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="629"/>
         <location filename="../settingsView/csettingsview_moc.ui" line="629"/>
         <source>empty = map name prefix</source>
         <source>empty = map name prefix</source>
-        <translation type="unfinished"></translation>
+        <translation>leer = Kartenname als Präfix</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="508"/>
         <location filename="../settingsView/csettingsview_moc.ui" line="508"/>
@@ -911,7 +911,7 @@ Heroes III: HD Edition wird derzeit nicht unterstützt!</translation>
     <message>
     <message>
         <location filename="../languages.cpp" line="39"/>
         <location filename="../languages.cpp" line="39"/>
         <source>Vietnamese</source>
         <source>Vietnamese</source>
-        <translation type="unfinished"></translation>
+        <translation>Vietnamesisch</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../languages.cpp" line="40"/>
         <location filename="../languages.cpp" line="40"/>

+ 8 - 8
launcher/translation/polish.ts

@@ -472,7 +472,7 @@
     <message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="319"/>
         <location filename="../settingsView/csettingsview_moc.ui" line="319"/>
         <source>Autosave limit (0 = off)</source>
         <source>Autosave limit (0 = off)</source>
-        <translation type="unfinished"></translation>
+        <translation>Limit autozapisów (0 = brak)</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="329"/>
         <location filename="../settingsView/csettingsview_moc.ui" line="329"/>
@@ -487,12 +487,12 @@
     <message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="612"/>
         <location filename="../settingsView/csettingsview_moc.ui" line="612"/>
         <source>Autosave prefix</source>
         <source>Autosave prefix</source>
-        <translation type="unfinished"></translation>
+        <translation>Przedrostek autozapisu</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="629"/>
         <location filename="../settingsView/csettingsview_moc.ui" line="629"/>
         <source>empty = map name prefix</source>
         <source>empty = map name prefix</source>
-        <translation type="unfinished"></translation>
+        <translation>puste = przedrostek z nazwy mapy</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="508"/>
         <location filename="../settingsView/csettingsview_moc.ui" line="508"/>
@@ -708,7 +708,7 @@ Heroes III: HD Edition nie jest obecnie wspierane!</translation>
     <message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
         <source>Interface Improvements</source>
         <source>Interface Improvements</source>
-        <translation type="unfinished"></translation>
+        <translation>Ulepszenia interfejsu</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="740"/>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="740"/>
@@ -723,7 +723,7 @@ Heroes III: HD Edition nie jest obecnie wspierane!</translation>
     <message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
         <source>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</source>
         <source>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</source>
-        <translation type="unfinished"></translation>
+        <translation>Zainstaluj modyfikację, która dostarcza różne ulepszenia interfejsu takie jak lepszy ekran ustawień mapy losowej lub wybieralne akcje w bitwach</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="788"/>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="788"/>
@@ -916,17 +916,17 @@ Heroes III: HD Edition nie jest obecnie wspierane!</translation>
     <message>
     <message>
         <location filename="../languages.cpp" line="40"/>
         <location filename="../languages.cpp" line="40"/>
         <source>Other (East European)</source>
         <source>Other (East European)</source>
-        <translation type="unfinished"></translation>
+        <translation>Inne (Wschodnioeuropejski)</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../languages.cpp" line="41"/>
         <location filename="../languages.cpp" line="41"/>
         <source>Other (Cyrillic Script)</source>
         <source>Other (Cyrillic Script)</source>
-        <translation type="unfinished"></translation>
+        <translation>Inne (Cyrylica)</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../languages.cpp" line="42"/>
         <location filename="../languages.cpp" line="42"/>
         <source>Other (West European)</source>
         <source>Other (West European)</source>
-        <translation type="unfinished"></translation>
+        <translation>Inne (Zachodnioeuropejski)</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../languages.cpp" line="64"/>
         <location filename="../languages.cpp" line="64"/>

+ 2 - 2
lib/CGameInterface.cpp

@@ -168,13 +168,13 @@ void CAdventureAI::battleCatapultAttacked(const CatapultAttack & ca)
 }
 }
 
 
 void CAdventureAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile,
 void CAdventureAI::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile,
-							   const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side)
+							   const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
 {
 {
 	assert(!battleAI);
 	assert(!battleAI);
 	assert(cbc);
 	assert(cbc);
 	battleAI = CDynLibHandler::getNewBattleAI(getBattleAIName());
 	battleAI = CDynLibHandler::getNewBattleAI(getBattleAIName());
 	battleAI->initBattleInterface(env, cbc);
 	battleAI->initBattleInterface(env, cbc);
-	battleAI->battleStart(army1, army2, tile, hero1, hero2, side);
+	battleAI->battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed);
 }
 }
 
 
 void CAdventureAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged)
 void CAdventureAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged)

+ 1 - 1
lib/CGameInterface.h

@@ -149,7 +149,7 @@ public:
 	virtual void yourTacticPhase(int distance) override;
 	virtual void yourTacticPhase(int distance) override;
 	virtual void battleNewRound(int round) override;
 	virtual void battleNewRound(int round) override;
 	virtual void battleCatapultAttacked(const CatapultAttack & ca) override;
 	virtual void battleCatapultAttacked(const CatapultAttack & ca) override;
-	virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override;
+	virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override;
 	virtual void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
 	virtual void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override;
 	virtual void actionStarted(const BattleAction &action) override;
 	virtual void actionStarted(const BattleAction &action) override;
 	virtual void battleNewRoundFirst(int round) override;
 	virtual void battleNewRoundFirst(int round) override;

+ 12 - 0
lib/CGeneralTextHandler.cpp

@@ -313,6 +313,18 @@ void CGeneralTextHandler::registerStringOverride(const std::string & modContext,
 	assert(!modContext.empty());
 	assert(!modContext.empty());
 	assert(!language.empty());
 	assert(!language.empty());
 
 
+	std::string baseModLanguage = getModLanguage(modContext);
+
+	if (baseModLanguage != language)
+	{
+		// this is translation - only add text to existing strings, do not register new ones
+		if (stringsLocalizations.count(UID.get()) == 0)
+		{
+			logMod->warn("Unknown string '%s' in mod '%s' for language '%s'. Ignoring", UID.get(), modContext, language);
+			return;
+		}
+	}
+
 	// NOTE: implicitly creates entry, intended - strings added by vcmi (and potential UI mods) are not registered anywhere at the moment
 	// NOTE: implicitly creates entry, intended - strings added by vcmi (and potential UI mods) are not registered anywhere at the moment
 	auto & entry = stringsLocalizations[UID.get()];
 	auto & entry = stringsLocalizations[UID.get()];
 
 

+ 4 - 4
lib/CGeneralTextHandler.h

@@ -141,8 +141,11 @@ class DLL_LINKAGE CGeneralTextHandler
 	std::vector<size_t> scenariosCountPerCampaign;
 	std::vector<size_t> scenariosCountPerCampaign;
 
 
 	std::string getModLanguage(const std::string & modContext);
 	std::string getModLanguage(const std::string & modContext);
-public:
 
 
+	/// add selected string to internal storage as high-priority strings
+	void registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized);
+
+public:
 	/// validates translation of specified language for specified mod
 	/// validates translation of specified language for specified mod
 	/// returns true if localization is valid and complete
 	/// returns true if localization is valid and complete
 	/// any error messages will be written to log file
 	/// any error messages will be written to log file
@@ -155,9 +158,6 @@ public:
 	/// add selected string to internal storage
 	/// add selected string to internal storage
 	void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized);
 	void registerString(const std::string & modContext, const TextIdentifier & UID, const std::string & localized);
 
 
-	/// add selected string to internal storage as high-priority strings
-	void registerStringOverride(const std::string & modContext, const std::string & language, const TextIdentifier & UID, const std::string & localized);
-
 	// returns true if identifier with such name was registered, even if not translated to current language
 	// returns true if identifier with such name was registered, even if not translated to current language
 	// not required right now, can be added if necessary
 	// not required right now, can be added if necessary
 	// bool identifierExists( const std::string identifier) const;
 	// bool identifierExists( const std::string identifier) const;

+ 6 - 2
lib/CTownHandler.cpp

@@ -111,7 +111,11 @@ void CBuilding::addNewBonus(const std::shared_ptr<Bonus> & b, BonusList & bonusL
 
 
 CFaction::~CFaction()
 CFaction::~CFaction()
 {
 {
-	delete town;
+	if (town)
+	{
+		delete town;
+		town = nullptr;
+	}
 }
 }
 
 
 int32_t CFaction::getIndex() const
 int32_t CFaction::getIndex() const
@@ -1030,7 +1034,7 @@ CFaction * CTownHandler::loadFromJson(const std::string & scope, const JsonNode
 	faction->creatureBg120 = source["creatureBackground"]["120px"].String();
 	faction->creatureBg120 = source["creatureBackground"]["120px"].String();
 	faction->creatureBg130 = source["creatureBackground"]["130px"].String();
 	faction->creatureBg130 = source["creatureBackground"]["130px"].String();
 
 
-	faction->boatType = EBoatId::NONE;
+	faction->boatType = EBoatId::CASTLE; //Do not crash
 	if (!source["boat"].isNull())
 	if (!source["boat"].isNull())
 	{
 	{
 		VLC->identifiers()->requestIdentifier("core:boat", source["boat"], [=](int32_t boatTypeID)
 		VLC->identifiers()->requestIdentifier("core:boat", source["boat"], [=](int32_t boatTypeID)

+ 1 - 1
lib/CTownHandler.h

@@ -207,7 +207,7 @@ public:
 
 
 	/// Boat that will be used by town shipyard (if any)
 	/// Boat that will be used by town shipyard (if any)
 	/// and for placing heroes directly on boat (in map editor, water prisons & taverns)
 	/// and for placing heroes directly on boat (in map editor, water prisons & taverns)
-	BoatId boatType;
+	BoatId boatType = BoatId(EBoatId::CASTLE);
 
 
 
 
 	CTown * town = nullptr; //NOTE: can be null
 	CTown * town = nullptr; //NOTE: can be null

+ 1 - 1
lib/IGameEventsReceiver.h

@@ -69,7 +69,7 @@ public:
 	virtual void battleStacksEffectsSet(const SetStackEffect & sse){};//called when a specific effect is set to stacks
 	virtual void battleStacksEffectsSet(const SetStackEffect & sse){};//called when a specific effect is set to stacks
 	virtual void battleTriggerEffect(const BattleTriggerEffect & bte){}; //called for various one-shot effects
 	virtual void battleTriggerEffect(const BattleTriggerEffect & bte){}; //called for various one-shot effects
 	virtual void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start
 	virtual void battleStartBefore(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2) {}; //called just before battle start
-	virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side){}; //called by engine when battle starts; side=0 - left, side=1 - right
+	virtual void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed){}; //called by engine when battle starts; side=0 - left, side=1 - right
 	virtual void battleUnitsChanged(const std::vector<UnitChanges> & units){};
 	virtual void battleUnitsChanged(const std::vector<UnitChanges> & units){};
 	virtual void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles){};
 	virtual void battleObstaclesChanged(const std::vector<ObstacleChanges> & obstacles){};
 	virtual void battleCatapultAttacked(const CatapultAttack & ca){}; //called when catapult makes an attack
 	virtual void battleCatapultAttacked(const CatapultAttack & ca){}; //called when catapult makes an attack

+ 4 - 6
lib/NetPacksLib.cpp

@@ -1381,9 +1381,8 @@ void HeroRecruited::applyGs(CGameState * gs) const
 		auto * boat = dynamic_cast<CGBoat *>(obj);
 		auto * boat = dynamic_cast<CGBoat *>(obj);
 		if (boat)
 		if (boat)
 		{
 		{
-			h->boat = boat;
-			h->attachTo(*boat);
-			boat->hero = h;
+			gs->map->removeBlockVisTiles(boat);
+			h->attachToBoat(boat);
 		}
 		}
 	}
 	}
 
 
@@ -1418,9 +1417,8 @@ void GiveHero::applyGs(CGameState * gs) const
 		auto * boat = dynamic_cast<CGBoat *>(obj);
 		auto * boat = dynamic_cast<CGBoat *>(obj);
 		if (boat)
 		if (boat)
 		{
 		{
-			h->boat = boat;
-			h->attachTo(*boat);
-			boat->hero = h;
+			gs->map->removeBlockVisTiles(boat);
+			h->attachToBoat(boat);
 		}
 		}
 	}
 	}
 
 

+ 1 - 0
lib/battle/BattleInfo.cpp

@@ -207,6 +207,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 	curB->round = -2;
 	curB->round = -2;
 	curB->activeStack = -1;
 	curB->activeStack = -1;
 	curB->creatureBank = creatureBank;
 	curB->creatureBank = creatureBank;
+	curB->replayAllowed = false;
 
 
 	if(town)
 	if(town)
 	{
 	{

+ 5 - 0
lib/battle/BattleInfo.h

@@ -36,6 +36,7 @@ public:
 	const CGTownInstance * town; //used during town siege, nullptr if this is not a siege (note that fortless town IS also a siege)
 	const CGTownInstance * town; //used during town siege, nullptr if this is not a siege (note that fortless town IS also a siege)
 	int3 tile; //for background and bonuses
 	int3 tile; //for background and bonuses
 	bool creatureBank; //auxilary field, do not serialize
 	bool creatureBank; //auxilary field, do not serialize
+	bool replayAllowed;
 	std::vector<CStack*> stacks;
 	std::vector<CStack*> stacks;
 	std::vector<std::shared_ptr<CObstacleInstance> > obstacles;
 	std::vector<std::shared_ptr<CObstacleInstance> > obstacles;
 	SiegeInfo si;
 	SiegeInfo si;
@@ -61,6 +62,10 @@ public:
 		h & tacticsSide;
 		h & tacticsSide;
 		h & tacticDistance;
 		h & tacticDistance;
 		h & static_cast<CBonusSystemNode&>(*this);
 		h & static_cast<CBonusSystemNode&>(*this);
+		if (version > 824)
+			h & replayAllowed;
+		else
+			replayAllowed = false;
 	}
 	}
 
 
 	//////////////////////////////////////////////////////////////////////////
 	//////////////////////////////////////////////////////////////////////////

+ 1 - 2
lib/gameState/CGameState.cpp

@@ -858,8 +858,7 @@ void CGameState::initHeroes()
 			map->objects.emplace_back(boat);
 			map->objects.emplace_back(boat);
 			map->addBlockVisTiles(boat);
 			map->addBlockVisTiles(boat);
 
 
-			boat->hero = hero;
-			hero->boat = boat;
+			hero->attachToBoat(boat);
 		}
 		}
 	}
 	}
 
 

+ 14 - 7
lib/gameState/CGameStateCampaign.cpp

@@ -118,28 +118,35 @@ void CGameStateCampaign::trimCrossoverHeroesParameters(std::vector<CampaignHeroR
 		//trimming artifacts
 		//trimming artifacts
 		for(CGHeroInstance * hero : crossoverHeroes)
 		for(CGHeroInstance * hero : crossoverHeroes)
 		{
 		{
-			size_t totalArts = GameConstants::BACKPACK_START + hero->artifactsInBackpack.size();
-			for (size_t i = 0; i < totalArts; i++ )
+			auto const & checkAndRemoveArtifact = [&](const ArtifactPosition & artifactPosition )
 			{
 			{
-				auto artifactPosition = ArtifactPosition((si32)i);
 				if(artifactPosition == ArtifactPosition::SPELLBOOK)
 				if(artifactPosition == ArtifactPosition::SPELLBOOK)
-					continue; // do not handle spellbook this way
+					return; // do not handle spellbook this way
 
 
 				const ArtSlotInfo *info = hero->getSlot(artifactPosition);
 				const ArtSlotInfo *info = hero->getSlot(artifactPosition);
 				if(!info)
 				if(!info)
-					continue;
+					return;
 
 
 				// TODO: why would there be nullptr artifacts?
 				// TODO: why would there be nullptr artifacts?
 				const CArtifactInstance *art = info->artifact;
 				const CArtifactInstance *art = info->artifact;
 				if(!art)
 				if(!art)
-					continue;
+					return;
 
 
 				bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId());
 				bool takeable = travelOptions.artifactsKeptByHero.count(art->artType->getId());
 
 
 				ArtifactLocation al(hero, artifactPosition);
 				ArtifactLocation al(hero, artifactPosition);
 				if(!takeable  &&  !al.getSlot()->locked)  //don't try removing locked artifacts -> it crashes #1719
 				if(!takeable  &&  !al.getSlot()->locked)  //don't try removing locked artifacts -> it crashes #1719
 					al.removeArtifact();
 					al.removeArtifact();
-			}
+			};
+
+			// process on copy - removal of artifact will invalidate container
+			auto artifactsWorn = hero->artifactsWorn;
+			for (auto const & art : artifactsWorn)
+				checkAndRemoveArtifact(art.first);
+
+			// process in reverse - removal of artifact will shift all artifacts after this one
+			for(int slotNumber = hero->artifactsInBackpack.size() - 1; slotNumber >= 0; slotNumber--)
+				checkAndRemoveArtifact(ArtifactPosition(GameConstants::BACKPACK_START + slotNumber));
 		}
 		}
 	}
 	}
 
 

+ 25 - 10
lib/mapObjects/CGHeroInstance.cpp

@@ -484,9 +484,12 @@ void CGHeroInstance::onHeroVisit(const CGHeroInstance * h) const
 			if (cb->gameState()->map->getTile(boatPos).isWater())
 			if (cb->gameState()->map->getTile(boatPos).isWater())
 			{
 			{
 				smp.val = movementPointsLimit(false);
 				smp.val = movementPointsLimit(false);
-				//Create a new boat for hero
-				cb->createObject(boatPos, Obj::BOAT, getBoatType().getNum());
-				boatId = cb->getTopObj(boatPos)->id;
+				if (!boat)
+				{
+					//Create a new boat for hero
+					cb->createObject(boatPos, Obj::BOAT, getBoatType().getNum());
+					boatId = cb->getTopObj(boatPos)->id;
+				}
 			}
 			}
 			else
 			else
 			{
 			{
@@ -1119,6 +1122,15 @@ int CGHeroInstance::maxSpellLevel() const
 	return std::min(GameConstants::SPELL_LEVELS, valOfBonuses(Selector::type()(BonusType::MAX_LEARNABLE_SPELL_LEVEL)));
 	return std::min(GameConstants::SPELL_LEVELS, valOfBonuses(Selector::type()(BonusType::MAX_LEARNABLE_SPELL_LEVEL)));
 }
 }
 
 
+void CGHeroInstance::attachToBoat(CGBoat* newBoat)
+{
+	assert(newBoat);
+	boat = newBoat;
+	attachTo(const_cast<CGBoat&>(*boat));
+	const_cast<CGBoat*>(boat)->hero = this;
+}
+
+
 void CGHeroInstance::deserializationFix()
 void CGHeroInstance::deserializationFix()
 {
 {
 	artDeserializationFix(this);
 	artDeserializationFix(this);
@@ -1718,22 +1730,25 @@ bool CGHeroInstance::isMissionCritical() const
 {
 {
 	for(const TriggeredEvent & event : IObjectInterface::cb->getMapHeader()->triggeredEvents)
 	for(const TriggeredEvent & event : IObjectInterface::cb->getMapHeader()->triggeredEvents)
 	{
 	{
-		if(event.trigger.test([&](const EventCondition & condition)
+		if (event.effect.type != EventEffect::DEFEAT)
+			continue;
+
+		auto const & testFunctor = [&](const EventCondition & condition)
 		{
 		{
 			if ((condition.condition == EventCondition::CONTROL || condition.condition == EventCondition::HAVE_0) && condition.object)
 			if ((condition.condition == EventCondition::CONTROL || condition.condition == EventCondition::HAVE_0) && condition.object)
 			{
 			{
 				const auto * hero = dynamic_cast<const CGHeroInstance *>(condition.object);
 				const auto * hero = dynamic_cast<const CGHeroInstance *>(condition.object);
 				return (hero != this);
 				return (hero != this);
 			}
 			}
-			else if(condition.condition == EventCondition::IS_HUMAN)
-			{
+
+			if(condition.condition == EventCondition::IS_HUMAN)
 				return true;
 				return true;
-			}
+
 			return false;
 			return false;
-		}))
-		{
+		};
+
+		if(event.trigger.test(testFunctor))
 			return true;
 			return true;
-		}
 	}
 	}
 	return false;
 	return false;
 }
 }

+ 1 - 0
lib/mapObjects/CGHeroInstance.h

@@ -282,6 +282,7 @@ public:
 	void getCastDescription(const spells::Spell * spell, const std::vector<const battle::Unit *> & attacked, MetaString & text) const override;
 	void getCastDescription(const spells::Spell * spell, const std::vector<const battle::Unit *> & attacked, MetaString & text) const override;
 	void spendMana(ServerCallback * server, const int spellCost) const override;
 	void spendMana(ServerCallback * server, const int spellCost) const override;
 
 
+	void attachToBoat(CGBoat* newBoat);
 	void boatDeserializationFix();
 	void boatDeserializationFix();
 	void deserializationFix();
 	void deserializationFix();
 
 

+ 26 - 2
lib/rmg/modificators/ObjectManager.cpp

@@ -216,6 +216,7 @@ int3 ObjectManager::findPlaceForObject(const rmg::Area & searchArea, rmg::Object
 
 
 rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg::Object & obj, si32 min_dist, bool isGuarded, bool onlyStraight, OptimizeType optimizer) const
 rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg::Object & obj, si32 min_dist, bool isGuarded, bool onlyStraight, OptimizeType optimizer) const
 {
 {
+	RecursiveLock lock(externalAccessMutex);
 	return placeAndConnectObject(searchArea, obj, [this, min_dist, &obj](const int3 & tile)
 	return placeAndConnectObject(searchArea, obj, [this, min_dist, &obj](const int3 & tile)
 	{
 	{
 		auto ti = map.getTileInfo(tile);
 		auto ti = map.getTileInfo(tile);
@@ -455,8 +456,31 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD
 				map.setOccupied(i, ETileType::BLOCKED);
 				map.setOccupied(i, ETileType::BLOCKED);
 	}
 	}
 	
 	
-	if(updateDistance)
-		updateDistances(object);
+	if (updateDistance)
+	{
+		//Update distances in every adjacent zone in case of wide connection
+
+		std::set<TRmgTemplateZoneId> adjacentZones;
+		auto objectArea = object.getArea();
+		objectArea.unite(objectArea.getBorderOutside());
+		
+		for (auto tile : objectArea.getTilesVector())
+		{
+			if (map.isOnMap(tile))
+			{
+				adjacentZones.insert(map.getZoneID(tile));
+			}
+		}
+
+		for (auto id : adjacentZones)
+		{
+			auto manager = map.getZones().at(id)->getModificator<ObjectManager>();
+			if (manager)
+			{
+				manager->updateDistances(object);
+			}
+		}
+	}
 	
 	
 	for(auto * instance : object.instances())
 	for(auto * instance : object.instances())
 	{
 	{

+ 1 - 1
lib/serializer/CSerializer.h

@@ -14,7 +14,7 @@
 
 
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
 
 
-const ui32 SERIALIZATION_VERSION = 824;
+const ui32 SERIALIZATION_VERSION = 825;
 const ui32 MINIMAL_SERIALIZATION_VERSION = 824;
 const ui32 MINIMAL_SERIALIZATION_VERSION = 824;
 const std::string SAVEGAME_MAGIC = "VCMISVG";
 const std::string SAVEGAME_MAGIC = "VCMISVG";
 
 

+ 28 - 28
mapeditor/translation/german.ts

@@ -292,22 +292,22 @@
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="252"/>
         <location filename="../mainwindow.cpp" line="252"/>
         <source>Confirmation</source>
         <source>Confirmation</source>
-        <translation type="unfinished"></translation>
+        <translation>Bestätigung</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="252"/>
         <location filename="../mainwindow.cpp" line="252"/>
         <source>Unsaved changes will be lost, are you sure?</source>
         <source>Unsaved changes will be lost, are you sure?</source>
-        <translation type="unfinished"></translation>
+        <translation>Ungespeicherte Änderungen gehen verloren, sind sie sicher?</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="329"/>
         <location filename="../mainwindow.cpp" line="329"/>
         <source>Failed to open map</source>
         <source>Failed to open map</source>
-        <translation type="unfinished"></translation>
+        <translation>Öffnen der Karte fehlgeschlagen</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="329"/>
         <location filename="../mainwindow.cpp" line="329"/>
         <source>Cannot open map from this folder</source>
         <source>Cannot open map from this folder</source>
-        <translation type="unfinished"></translation>
+        <translation>Kann keine Karte aus diesem Ordner öffnen</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="370"/>
         <location filename="../mainwindow.cpp" line="370"/>
@@ -344,22 +344,22 @@
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="1128"/>
         <location filename="../mainwindow.cpp" line="1128"/>
         <source>No objects selected</source>
         <source>No objects selected</source>
-        <translation type="unfinished"></translation>
+        <translation>Keine Objekte selektiert</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="1132"/>
         <location filename="../mainwindow.cpp" line="1132"/>
         <source>This operation is irreversible. Do you want to continue?</source>
         <source>This operation is irreversible. Do you want to continue?</source>
-        <translation type="unfinished"></translation>
+        <translation>Diese Operation ist unumkehrbar. Möchten sie fortsetzen?</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="1191"/>
         <location filename="../mainwindow.cpp" line="1191"/>
         <source>Errors occured. %1 objects were not updated</source>
         <source>Errors occured. %1 objects were not updated</source>
-        <translation type="unfinished"></translation>
+        <translation>Fehler sind aufgetreten. %1 Objekte konnten nicht aktualisiert werden</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="1231"/>
         <location filename="../mainwindow.cpp" line="1231"/>
         <source>Save to image</source>
         <source>Save to image</source>
-        <translation type="unfinished"></translation>
+        <translation>Als Bild speichern</translation>
     </message>
     </message>
 </context>
 </context>
 <context>
 <context>
@@ -387,7 +387,7 @@
     <message>
     <message>
         <location filename="../mapsettings.ui" line="83"/>
         <location filename="../mapsettings.ui" line="83"/>
         <source>Limit maximum heroes level</source>
         <source>Limit maximum heroes level</source>
-        <translation type="unfinished"></translation>
+        <translation>Maximales Level des Helden begrenzen</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="92"/>
         <location filename="../mapsettings.ui" line="92"/>
@@ -397,47 +397,47 @@
     <message>
     <message>
         <location filename="../mapsettings.ui" line="137"/>
         <location filename="../mapsettings.ui" line="137"/>
         <source>Mods</source>
         <source>Mods</source>
-        <translation type="unfinished"></translation>
+        <translation>Mods</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="143"/>
         <location filename="../mapsettings.ui" line="143"/>
         <source>Mandatory mods for playing this map</source>
         <source>Mandatory mods for playing this map</source>
-        <translation type="unfinished"></translation>
+        <translation>Notwendige Mods zum Spielen dieser Karte</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="157"/>
         <location filename="../mapsettings.ui" line="157"/>
         <source>Mod name</source>
         <source>Mod name</source>
-        <translation type="unfinished"></translation>
+        <translation>Mod Name</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="162"/>
         <location filename="../mapsettings.ui" line="162"/>
         <source>Version</source>
         <source>Version</source>
-        <translation type="unfinished"></translation>
+        <translation>Version</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="172"/>
         <location filename="../mapsettings.ui" line="172"/>
         <source>Automatic assignment</source>
         <source>Automatic assignment</source>
-        <translation type="unfinished"></translation>
+        <translation>Automatische Zuweisung</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="179"/>
         <location filename="../mapsettings.ui" line="179"/>
         <source>Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods</source>
         <source>Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods</source>
-        <translation type="unfinished"></translation>
+        <translation>Erforderliche Mods anhand der auf der Karte platzierten Objekte festlegen. Diese Methode kann Probleme verursachen, wenn Sie Belohnungen, Garnisonen usw. von Mods angepasst haben</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="182"/>
         <location filename="../mapsettings.ui" line="182"/>
         <source>Map objects mods</source>
         <source>Map objects mods</source>
-        <translation type="unfinished"></translation>
+        <translation>Mods für Kartenobjekte</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="192"/>
         <location filename="../mapsettings.ui" line="192"/>
         <source>Set all mods having a game content as mandatory</source>
         <source>Set all mods having a game content as mandatory</source>
-        <translation type="unfinished"></translation>
+        <translation>Alle Mods, die einen Spielinhalt haben, als notwendig festlegen</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="195"/>
         <location filename="../mapsettings.ui" line="195"/>
         <source>Full content mods</source>
         <source>Full content mods</source>
-        <translation type="unfinished"></translation>
+        <translation>Vollwertige Mods</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="208"/>
         <location filename="../mapsettings.ui" line="208"/>
@@ -581,7 +581,7 @@
     <message>
     <message>
         <location filename="../mapview.cpp" line="471"/>
         <location filename="../mapview.cpp" line="471"/>
         <source>Can&apos;t place object</source>
         <source>Can&apos;t place object</source>
-        <translation type="unfinished"></translation>
+        <translation>Objekt kann nicht platziert werden</translation>
     </message>
     </message>
 </context>
 </context>
 <context>
 <context>
@@ -621,7 +621,7 @@
     <message>
     <message>
         <location filename="../playerparams.ui" line="179"/>
         <location filename="../playerparams.ui" line="179"/>
         <source>Color</source>
         <source>Color</source>
-        <translation type="unfinished"></translation>
+        <translation>Farbe</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../playerparams.ui" line="85"/>
         <location filename="../playerparams.ui" line="85"/>
@@ -659,7 +659,7 @@
     <message>
     <message>
         <location filename="../playersettings.ui" line="74"/>
         <location filename="../playersettings.ui" line="74"/>
         <source>1</source>
         <source>1</source>
-        <translation type="unfinished">1</translation>
+        <translation>1</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../playersettings.ui" line="117"/>
         <location filename="../playersettings.ui" line="117"/>
@@ -721,7 +721,7 @@
     <message>
     <message>
         <location filename="../validator.cpp" line="70"/>
         <location filename="../validator.cpp" line="70"/>
         <source>No factions allowed for player %1</source>
         <source>No factions allowed for player %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Keine Fraktionen für Spieler %1 erlaubt</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="73"/>
         <location filename="../validator.cpp" line="73"/>
@@ -811,7 +811,7 @@
     <message>
     <message>
         <location filename="../validator.cpp" line="175"/>
         <location filename="../validator.cpp" line="175"/>
         <source>Map contains object from mod &quot;%1&quot;, but doesn&apos;t require it</source>
         <source>Map contains object from mod &quot;%1&quot;, but doesn&apos;t require it</source>
-        <translation type="unfinished"></translation>
+        <translation>Karte enthält Objekt aus Mod &quot;%1&quot;, benötigt es aber nicht</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="181"/>
         <location filename="../validator.cpp" line="181"/>
@@ -907,12 +907,12 @@
     <message>
     <message>
         <location filename="../windownewmap.ui" line="380"/>
         <location filename="../windownewmap.ui" line="380"/>
         <source>Human teams</source>
         <source>Human teams</source>
-        <translation type="unfinished"></translation>
+        <translation>Menschliche Teams</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../windownewmap.ui" line="399"/>
         <location filename="../windownewmap.ui" line="399"/>
         <source>Computer teams</source>
         <source>Computer teams</source>
-        <translation type="unfinished"></translation>
+        <translation>Computer Teams</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../windownewmap.ui" line="416"/>
         <location filename="../windownewmap.ui" line="416"/>
@@ -978,17 +978,17 @@
     <message>
     <message>
         <location filename="../windownewmap.cpp" line="271"/>
         <location filename="../windownewmap.cpp" line="271"/>
         <source>No template</source>
         <source>No template</source>
-        <translation type="unfinished"></translation>
+        <translation>Kein Template</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../windownewmap.cpp" line="271"/>
         <location filename="../windownewmap.cpp" line="271"/>
         <source>No template for parameters scecified. Random map cannot be generated.</source>
         <source>No template for parameters scecified. Random map cannot be generated.</source>
-        <translation type="unfinished"></translation>
+        <translation>Es wurde kein Template für Parameter erstellt. Zufällige Karte kann nicht generiert werden.</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../windownewmap.cpp" line="291"/>
         <location filename="../windownewmap.cpp" line="291"/>
         <source>RMG failure</source>
         <source>RMG failure</source>
-        <translation type="unfinished"></translation>
+        <translation>RMG-Fehler</translation>
     </message>
     </message>
 </context>
 </context>
 <context>
 <context>

+ 61 - 61
mapeditor/translation/polish.ts

@@ -137,7 +137,7 @@
     <message>
     <message>
         <location filename="../mainwindow.ui" line="922"/>
         <location filename="../mainwindow.ui" line="922"/>
         <source>Save as...</source>
         <source>Save as...</source>
-        <translation>Zapisz jako</translation>
+        <translation>Zapisz jako...</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.ui" line="925"/>
         <location filename="../mainwindow.ui" line="925"/>
@@ -287,27 +287,27 @@
     <message>
     <message>
         <location filename="../mainwindow.ui" line="1234"/>
         <location filename="../mainwindow.ui" line="1234"/>
         <source>Export as...</source>
         <source>Export as...</source>
-        <translation type="unfinished"></translation>
+        <translation>Eksportuj jako...</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="252"/>
         <location filename="../mainwindow.cpp" line="252"/>
         <source>Confirmation</source>
         <source>Confirmation</source>
-        <translation type="unfinished"></translation>
+        <translation>Potwierdzenie</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="252"/>
         <location filename="../mainwindow.cpp" line="252"/>
         <source>Unsaved changes will be lost, are you sure?</source>
         <source>Unsaved changes will be lost, are you sure?</source>
-        <translation type="unfinished"></translation>
+        <translation>Niezapisane zmiany zostaną utracone, jesteś pewny?</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="329"/>
         <location filename="../mainwindow.cpp" line="329"/>
         <source>Failed to open map</source>
         <source>Failed to open map</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie udało się otworzyć mapy</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="329"/>
         <location filename="../mainwindow.cpp" line="329"/>
         <source>Cannot open map from this folder</source>
         <source>Cannot open map from this folder</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie można otworzyć mapy z tego folderu</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="370"/>
         <location filename="../mainwindow.cpp" line="370"/>
@@ -344,22 +344,22 @@
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="1128"/>
         <location filename="../mainwindow.cpp" line="1128"/>
         <source>No objects selected</source>
         <source>No objects selected</source>
-        <translation type="unfinished"></translation>
+        <translation>Brak wybranych obiektów</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="1132"/>
         <location filename="../mainwindow.cpp" line="1132"/>
         <source>This operation is irreversible. Do you want to continue?</source>
         <source>This operation is irreversible. Do you want to continue?</source>
-        <translation type="unfinished"></translation>
+        <translation>Ta operacja jest nieodwracalna. Czy chcesz kontynuować?</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="1191"/>
         <location filename="../mainwindow.cpp" line="1191"/>
         <source>Errors occured. %1 objects were not updated</source>
         <source>Errors occured. %1 objects were not updated</source>
-        <translation type="unfinished"></translation>
+        <translation>Wystąpiły błędy. %1 obiektów nie zostało zaktualizowanych</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="1231"/>
         <location filename="../mainwindow.cpp" line="1231"/>
         <source>Save to image</source>
         <source>Save to image</source>
-        <translation type="unfinished"></translation>
+        <translation>Zapisz jako obraz</translation>
     </message>
     </message>
 </context>
 </context>
 <context>
 <context>
@@ -387,7 +387,7 @@
     <message>
     <message>
         <location filename="../mapsettings.ui" line="83"/>
         <location filename="../mapsettings.ui" line="83"/>
         <source>Limit maximum heroes level</source>
         <source>Limit maximum heroes level</source>
-        <translation type="unfinished"></translation>
+        <translation>Ogranicz maksymalny poziom bohaterów</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="92"/>
         <location filename="../mapsettings.ui" line="92"/>
@@ -397,47 +397,47 @@
     <message>
     <message>
         <location filename="../mapsettings.ui" line="137"/>
         <location filename="../mapsettings.ui" line="137"/>
         <source>Mods</source>
         <source>Mods</source>
-        <translation type="unfinished"></translation>
+        <translation>Modyfikacje</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="143"/>
         <location filename="../mapsettings.ui" line="143"/>
         <source>Mandatory mods for playing this map</source>
         <source>Mandatory mods for playing this map</source>
-        <translation type="unfinished"></translation>
+        <translation>Obowiązkowe modyfikacje do uruchomienia tej mapy</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="157"/>
         <location filename="../mapsettings.ui" line="157"/>
         <source>Mod name</source>
         <source>Mod name</source>
-        <translation type="unfinished"></translation>
+        <translation>Nazwa modyfikacji</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="162"/>
         <location filename="../mapsettings.ui" line="162"/>
         <source>Version</source>
         <source>Version</source>
-        <translation type="unfinished"></translation>
+        <translation>Wersja</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="172"/>
         <location filename="../mapsettings.ui" line="172"/>
         <source>Automatic assignment</source>
         <source>Automatic assignment</source>
-        <translation type="unfinished"></translation>
+        <translation>Automatyczne przypisanie</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="179"/>
         <location filename="../mapsettings.ui" line="179"/>
         <source>Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods</source>
         <source>Set required mods based on objects placed on the map. This method may cause problems if you have customized rewards, garrisons, etc from mods</source>
-        <translation type="unfinished"></translation>
+        <translation>Wybierz wymagane modyfikacje bazując na obiektach umeszczonych na mapie. Ta metoda może stworzyć problemy jeśli masz własne nagrody, garnizony itp. z modyfikacji</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="182"/>
         <location filename="../mapsettings.ui" line="182"/>
         <source>Map objects mods</source>
         <source>Map objects mods</source>
-        <translation type="unfinished"></translation>
+        <translation>Mody od nowych obiektów mapy</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="192"/>
         <location filename="../mapsettings.ui" line="192"/>
         <source>Set all mods having a game content as mandatory</source>
         <source>Set all mods having a game content as mandatory</source>
-        <translation type="unfinished"></translation>
+        <translation>Ustaw wszystkie modyfikacje mające nową elementy gry jako obowiązkowe</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="195"/>
         <location filename="../mapsettings.ui" line="195"/>
         <source>Full content mods</source>
         <source>Full content mods</source>
-        <translation type="unfinished"></translation>
+        <translation>Mody od złożonej zawartości</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.ui" line="208"/>
         <location filename="../mapsettings.ui" line="208"/>
@@ -513,7 +513,7 @@
     <message>
     <message>
         <location filename="../mapsettings.cpp" line="174"/>
         <location filename="../mapsettings.cpp" line="174"/>
         <source>No special victory</source>
         <source>No special victory</source>
-        <translation type="unfinished">Bez specjalnych warunków zwycięstwa</translation>
+        <translation>Bez specjalnych warunków zwycięstwa</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.cpp" line="175"/>
         <location filename="../mapsettings.cpp" line="175"/>
@@ -523,57 +523,57 @@
     <message>
     <message>
         <location filename="../mapsettings.cpp" line="176"/>
         <location filename="../mapsettings.cpp" line="176"/>
         <source>Hire creatures</source>
         <source>Hire creatures</source>
-        <translation type="unfinished">Zdobądź stworzenia</translation>
+        <translation>Zdobądź stworzenia</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.cpp" line="177"/>
         <location filename="../mapsettings.cpp" line="177"/>
         <source>Accumulate resources</source>
         <source>Accumulate resources</source>
-        <translation type="unfinished">Zbierz zasoby</translation>
+        <translation>Zbierz zasoby</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.cpp" line="178"/>
         <location filename="../mapsettings.cpp" line="178"/>
         <source>Construct building</source>
         <source>Construct building</source>
-        <translation type="unfinished">Zbuduj budynek</translation>
+        <translation>Zbuduj budynek</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.cpp" line="179"/>
         <location filename="../mapsettings.cpp" line="179"/>
         <source>Capture town</source>
         <source>Capture town</source>
-        <translation type="unfinished">Zdobądź miasto</translation>
+        <translation>Zdobądź miasto</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.cpp" line="180"/>
         <location filename="../mapsettings.cpp" line="180"/>
         <source>Defeat hero</source>
         <source>Defeat hero</source>
-        <translation type="unfinished">Pokonaj bohatera</translation>
+        <translation>Pokonaj bohatera</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.cpp" line="181"/>
         <location filename="../mapsettings.cpp" line="181"/>
         <source>Transport artifact</source>
         <source>Transport artifact</source>
-        <translation type="unfinished">Przenieś artefakt</translation>
+        <translation>Przenieś artefakt</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.cpp" line="184"/>
         <location filename="../mapsettings.cpp" line="184"/>
         <source>No special loss</source>
         <source>No special loss</source>
-        <translation type="unfinished">Bez specjalnych warunków porażki</translation>
+        <translation>Bez specjalnych warunków porażki</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.cpp" line="185"/>
         <location filename="../mapsettings.cpp" line="185"/>
         <source>Lose castle</source>
         <source>Lose castle</source>
-        <translation type="unfinished">Utrata miasta</translation>
+        <translation>Utrata miasta</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.cpp" line="186"/>
         <location filename="../mapsettings.cpp" line="186"/>
         <source>Lose hero</source>
         <source>Lose hero</source>
-        <translation type="unfinished">Utrata bohatera</translation>
+        <translation>Utrata bohatera</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.cpp" line="187"/>
         <location filename="../mapsettings.cpp" line="187"/>
         <source>Time expired</source>
         <source>Time expired</source>
-        <translation type="unfinished">Upłynięcie czasu</translation>
+        <translation>Upłynięcie czasu</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings.cpp" line="188"/>
         <location filename="../mapsettings.cpp" line="188"/>
         <source>Days without town</source>
         <source>Days without town</source>
-        <translation type="unfinished">Dni bez miasta</translation>
+        <translation>Dni bez miasta</translation>
     </message>
     </message>
 </context>
 </context>
 <context>
 <context>
@@ -581,7 +581,7 @@
     <message>
     <message>
         <location filename="../mapview.cpp" line="471"/>
         <location filename="../mapview.cpp" line="471"/>
         <source>Can&apos;t place object</source>
         <source>Can&apos;t place object</source>
-        <translation type="unfinished"></translation>
+        <translation>Nie można umieścić obiektu</translation>
     </message>
     </message>
 </context>
 </context>
 <context>
 <context>
@@ -621,7 +621,7 @@
     <message>
     <message>
         <location filename="../playerparams.ui" line="179"/>
         <location filename="../playerparams.ui" line="179"/>
         <source>Color</source>
         <source>Color</source>
-        <translation type="unfinished"></translation>
+        <translation>Kolor</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../playerparams.ui" line="85"/>
         <location filename="../playerparams.ui" line="85"/>
@@ -659,7 +659,7 @@
     <message>
     <message>
         <location filename="../playersettings.ui" line="74"/>
         <location filename="../playersettings.ui" line="74"/>
         <source>1</source>
         <source>1</source>
-        <translation type="unfinished">1</translation>
+        <translation>1</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../playersettings.ui" line="117"/>
         <location filename="../playersettings.ui" line="117"/>
@@ -716,37 +716,37 @@
     <message>
     <message>
         <location filename="../validator.cpp" line="50"/>
         <location filename="../validator.cpp" line="50"/>
         <source>Map is not loaded</source>
         <source>Map is not loaded</source>
-        <translation type="unfinished"></translation>
+        <translation>Mapa nie została wczytana</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="70"/>
         <location filename="../validator.cpp" line="70"/>
         <source>No factions allowed for player %1</source>
         <source>No factions allowed for player %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Brak dozwolonych frakcji dla gracza %1</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="73"/>
         <location filename="../validator.cpp" line="73"/>
         <source>No players allowed to play this map</source>
         <source>No players allowed to play this map</source>
-        <translation type="unfinished"></translation>
+        <translation>Żaden gracz nie jest dozwolony do rozegrania tej mapy</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="75"/>
         <location filename="../validator.cpp" line="75"/>
         <source>Map is allowed for one player and cannot be started</source>
         <source>Map is allowed for one player and cannot be started</source>
-        <translation type="unfinished"></translation>
+        <translation>Mapa jest dozwolona dla jednego gracza i nie może być rozpoczęta</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="77"/>
         <location filename="../validator.cpp" line="77"/>
         <source>No human players allowed to play this map</source>
         <source>No human players allowed to play this map</source>
-        <translation type="unfinished"></translation>
+        <translation>Żaden gracz ludzki nie został dozwolony by rozegrać tą mapę</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="93"/>
         <location filename="../validator.cpp" line="93"/>
         <source>Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner</source>
         <source>Armored instance %1 is UNFLAGGABLE but must have NEUTRAL or player owner</source>
-        <translation type="unfinished"></translation>
+        <translation>Obiekt z armią %1 jest nie do oflagowania, lecz musi mieć właściciela neutralnego lub gracza</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="99"/>
         <location filename="../validator.cpp" line="99"/>
         <source>Object %1 is assigned to non-playable player %2</source>
         <source>Object %1 is assigned to non-playable player %2</source>
-        <translation type="unfinished"></translation>
+        <translation>Obiekt %1 został przypisany do niegrywalnego gracza %2</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="106"/>
         <location filename="../validator.cpp" line="106"/>
@@ -756,72 +756,72 @@
     <message>
     <message>
         <location filename="../validator.cpp" line="116"/>
         <location filename="../validator.cpp" line="116"/>
         <source>Prison %1 must be a NEUTRAL</source>
         <source>Prison %1 must be a NEUTRAL</source>
-        <translation type="unfinished"></translation>
+        <translation>Więzienie %1 musi być neutralne</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="122"/>
         <location filename="../validator.cpp" line="122"/>
         <source>Hero %1 must have an owner</source>
         <source>Hero %1 must have an owner</source>
-        <translation type="unfinished"></translation>
+        <translation>Bohater %1 musi mieć właściciela</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="127"/>
         <location filename="../validator.cpp" line="127"/>
         <source>Hero %1 is prohibited by map settings</source>
         <source>Hero %1 is prohibited by map settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Bohater %1 jest zabroniony przez ustawienia mapy</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="130"/>
         <location filename="../validator.cpp" line="130"/>
         <source>Hero %1 has duplicate on map</source>
         <source>Hero %1 has duplicate on map</source>
-        <translation type="unfinished"></translation>
+        <translation>Bohater %1 posiada duplikat na mapie</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="133"/>
         <location filename="../validator.cpp" line="133"/>
         <source>Hero %1 has an empty type and must be removed</source>
         <source>Hero %1 has an empty type and must be removed</source>
-        <translation type="unfinished"></translation>
+        <translation>Bohater %1 jest pustego typu i musi zostać usunięty</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="144"/>
         <location filename="../validator.cpp" line="144"/>
         <source>Spell scroll %1 is prohibited by map settings</source>
         <source>Spell scroll %1 is prohibited by map settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Zwój z zaklęciem %1 jest zabroniony przez ustawienia mapy</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="147"/>
         <location filename="../validator.cpp" line="147"/>
         <source>Spell scroll %1 doesn&apos;t have instance assigned and must be removed</source>
         <source>Spell scroll %1 doesn&apos;t have instance assigned and must be removed</source>
-        <translation type="unfinished"></translation>
+        <translation>Zwój z zaklęciem %1 nie ma przypisanej instancji i musi zostać usunięty</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="153"/>
         <location filename="../validator.cpp" line="153"/>
         <source>Artifact %1 is prohibited by map settings</source>
         <source>Artifact %1 is prohibited by map settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Artefakt %1 jest zabroniony przez ustawienia mapy</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="162"/>
         <location filename="../validator.cpp" line="162"/>
         <source>Player %1 doesn&apos;t have any starting town</source>
         <source>Player %1 doesn&apos;t have any starting town</source>
-        <translation type="unfinished"></translation>
+        <translation>Gracz %1 nie ma żadnego startowego miasta</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="166"/>
         <location filename="../validator.cpp" line="166"/>
         <source>Map name is not specified</source>
         <source>Map name is not specified</source>
-        <translation type="unfinished"></translation>
+        <translation>Nazwa mapy nie została ustawiona</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="168"/>
         <location filename="../validator.cpp" line="168"/>
         <source>Map description is not specified</source>
         <source>Map description is not specified</source>
-        <translation type="unfinished"></translation>
+        <translation>Opis mapy nie został ustawiony</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="175"/>
         <location filename="../validator.cpp" line="175"/>
         <source>Map contains object from mod &quot;%1&quot;, but doesn&apos;t require it</source>
         <source>Map contains object from mod &quot;%1&quot;, but doesn&apos;t require it</source>
-        <translation type="unfinished"></translation>
+        <translation>Mapa zawiera obiekt z modyfikacji %1 ale nie wymaga tej modyfikacji</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="181"/>
         <location filename="../validator.cpp" line="181"/>
         <source>Exception occurs during validation: %1</source>
         <source>Exception occurs during validation: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Wystąpił wyjątek podczas walidacji: %1</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="185"/>
         <location filename="../validator.cpp" line="185"/>
         <source>Unknown exception occurs during validation</source>
         <source>Unknown exception occurs during validation</source>
-        <translation type="unfinished"></translation>
+        <translation>Wystąpił nieznane wyjątek podczas walidacji</translation>
     </message>
     </message>
 </context>
 </context>
 <context>
 <context>
@@ -907,12 +907,12 @@
     <message>
     <message>
         <location filename="../windownewmap.ui" line="380"/>
         <location filename="../windownewmap.ui" line="380"/>
         <source>Human teams</source>
         <source>Human teams</source>
-        <translation type="unfinished"></translation>
+        <translation>Sojusze ludzkie</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../windownewmap.ui" line="399"/>
         <location filename="../windownewmap.ui" line="399"/>
         <source>Computer teams</source>
         <source>Computer teams</source>
-        <translation type="unfinished"></translation>
+        <translation>Sojusze komputerowe</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../windownewmap.ui" line="416"/>
         <location filename="../windownewmap.ui" line="416"/>
@@ -978,17 +978,17 @@
     <message>
     <message>
         <location filename="../windownewmap.cpp" line="271"/>
         <location filename="../windownewmap.cpp" line="271"/>
         <source>No template</source>
         <source>No template</source>
-        <translation type="unfinished"></translation>
+        <translation>Brak szablonu</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../windownewmap.cpp" line="271"/>
         <location filename="../windownewmap.cpp" line="271"/>
         <source>No template for parameters scecified. Random map cannot be generated.</source>
         <source>No template for parameters scecified. Random map cannot be generated.</source>
-        <translation type="unfinished"></translation>
+        <translation>Brak szablonu dla wybranych parametrów. Mapa losowa nie może zostać wygenerowana.</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../windownewmap.cpp" line="291"/>
         <location filename="../windownewmap.cpp" line="291"/>
         <source>RMG failure</source>
         <source>RMG failure</source>
-        <translation type="unfinished"></translation>
+        <translation>Niepowodzenie generatora map losowych</translation>
     </message>
     </message>
 </context>
 </context>
 <context>
 <context>

+ 30 - 17
server/CGameHandler.cpp

@@ -607,9 +607,15 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con
 	const int queriedPlayers = battleQuery ? (int)boost::count(queries.allQueries(), battleQuery) : 0;
 	const int queriedPlayers = battleQuery ? (int)boost::count(queries.allQueries(), battleQuery) : 0;
 	finishingBattle = std::make_unique<FinishingBattleHelper>(battleQuery, queriedPlayers);
 	finishingBattle = std::make_unique<FinishingBattleHelper>(battleQuery, queriedPlayers);
 	
 	
-	auto battleDialogQuery = std::make_shared<CBattleDialogQuery>(this, gs->curB);
-	battleResult.data->queryID = battleDialogQuery->queryID;
-	queries.addQuery(battleDialogQuery);
+	// in battles against neutrals, 1st player can ask to replay battle manually
+	if (!gs->curB->sides[1].color.isValidPlayer())
+	{
+		auto battleDialogQuery = std::make_shared<CBattleDialogQuery>(this, gs->curB);
+		battleResult.data->queryID = battleDialogQuery->queryID;
+		queries.addQuery(battleDialogQuery);
+	}
+	else
+		battleResult.data->queryID = -1;
 	
 	
 	//set same battle result for all queries
 	//set same battle result for all queries
 	for(auto q : queries.allQueries())
 	for(auto q : queries.allQueries())
@@ -620,6 +626,9 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance * heroAttacker, con
 	}
 	}
 	
 	
 	sendAndApply(battleResult.data); //after this point casualties objects are destroyed
 	sendAndApply(battleResult.data); //after this point casualties objects are destroyed
+
+	if (battleResult.data->queryID == -1)
+		endBattleConfirm(gs->curB);
 }
 }
 
 
 void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo)
 void CGameHandler::endBattleConfirm(const BattleInfo * battleInfo)
@@ -2118,6 +2127,13 @@ void CGameHandler::setupBattle(int3 tile, const CArmedInstance *armies[2], const
 	//send info about battles
 	//send info about battles
 	BattleStart bs;
 	BattleStart bs;
 	bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town);
 	bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, creatureBank, town);
+
+	engageIntoBattle(bs.info->sides[0].color);
+	engageIntoBattle(bs.info->sides[1].color);
+
+	auto lastBattleQuery = std::dynamic_pointer_cast<CBattleQuery>(queries.topQuery(bs.info->sides[0].color));
+	bs.info->replayAllowed = lastBattleQuery == nullptr && !bs.info->sides[1].color.isValidPlayer();
+
 	sendAndApply(&bs);
 	sendAndApply(&bs);
 }
 }
 
 
@@ -2577,9 +2593,6 @@ void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedI
 	if(gs->curB)
 	if(gs->curB)
 		gs->curB.dellNull();
 		gs->curB.dellNull();
 	
 	
-	engageIntoBattle(army1->tempOwner);
-	engageIntoBattle(army2->tempOwner);
-
 	static const CArmedInstance *armies[2];
 	static const CArmedInstance *armies[2];
 	armies[0] = army1;
 	armies[0] = army1;
 	armies[1] = army2;
 	armies[1] = army2;
@@ -2587,39 +2600,39 @@ void CGameHandler::startBattlePrimary(const CArmedInstance *army1, const CArmedI
 	heroes[0] = hero1;
 	heroes[0] = hero1;
 	heroes[1] = hero2;
 	heroes[1] = hero2;
 
 
-
 	setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces
 	setupBattle(tile, armies, heroes, creatureBank, town); //initializes stacks, places creatures on battlefield, blocks and informs player interfaces
 
 
+	auto lastBattleQuery = std::dynamic_pointer_cast<CBattleQuery>(queries.topQuery(gs->curB->sides[0].color));
+
 	//existing battle query for retying auto-combat
 	//existing battle query for retying auto-combat
-	auto battleQuery = std::dynamic_pointer_cast<CBattleQuery>(queries.topQuery(gs->curB->sides[0].color));
-	if(battleQuery)
+	if(lastBattleQuery)
 	{
 	{
 		for(int i : {0, 1})
 		for(int i : {0, 1})
 		{
 		{
 			if(heroes[i])
 			if(heroes[i])
 			{
 			{
 				SetMana restoreInitialMana;
 				SetMana restoreInitialMana;
-				restoreInitialMana.val = battleQuery->initialHeroMana[i];
+				restoreInitialMana.val = lastBattleQuery->initialHeroMana[i];
 				restoreInitialMana.hid = heroes[i]->id;
 				restoreInitialMana.hid = heroes[i]->id;
 				sendAndApply(&restoreInitialMana);
 				sendAndApply(&restoreInitialMana);
 			}
 			}
 		}
 		}
 		
 		
-		battleQuery->bi = gs->curB;
-		battleQuery->result = std::nullopt;
-		battleQuery->belligerents[0] = gs->curB->sides[0].armyObject;
-		battleQuery->belligerents[1] = gs->curB->sides[1].armyObject;
+		lastBattleQuery->bi = gs->curB;
+		lastBattleQuery->result = std::nullopt;
+		lastBattleQuery->belligerents[0] = gs->curB->sides[0].armyObject;
+		lastBattleQuery->belligerents[1] = gs->curB->sides[1].armyObject;
 	}
 	}
 
 
-	battleQuery = std::make_shared<CBattleQuery>(this, gs->curB);
+	auto nextBattleQuery = std::make_shared<CBattleQuery>(this, gs->curB);
 	for(int i : {0, 1})
 	for(int i : {0, 1})
 	{
 	{
 		if(heroes[i])
 		if(heroes[i])
 		{
 		{
-			battleQuery->initialHeroMana[i] = heroes[i]->mana;
+			nextBattleQuery->initialHeroMana[i] = heroes[i]->mana;
 		}
 		}
 	}
 	}
-	queries.addQuery(battleQuery);
+	queries.addQuery(nextBattleQuery);
 
 
 	this->battleThread = std::make_unique<boost::thread>(boost::thread(&CGameHandler::runBattle, this));
 	this->battleThread = std::make_unique<boost::thread>(boost::thread(&CGameHandler::runBattle, this));
 }
 }

+ 1 - 1
server/CQuery.cpp

@@ -176,7 +176,7 @@ void CObjectVisitQuery::onExposure(QueryPtr topQuery)
 	if(gh->isValidObject(visitedObject))
 	if(gh->isValidObject(visitedObject))
 		topQuery->notifyObjectAboutRemoval(*this);
 		topQuery->notifyObjectAboutRemoval(*this);
 
 
-	owner->popQuery(*this);
+	owner->popIfTop(*this);
 }
 }
 
 
 void Queries::popQuery(PlayerColor player, QueryPtr query)
 void Queries::popQuery(PlayerColor player, QueryPtr query)

+ 5 - 4
server/HeroPoolProcessor.cpp

@@ -237,18 +237,19 @@ bool HeroPoolProcessor::hireHero(const ObjectInstanceID & objectID, const HeroTy
 		gameHandler->complain("Hero is not available for hiring!");
 		gameHandler->complain("Hero is not available for hiring!");
 		return false;
 		return false;
 	}
 	}
+	const auto targetPos = mapObject->visitablePos();
 
 
 	HeroRecruited hr;
 	HeroRecruited hr;
 	hr.tid = mapObject->id;
 	hr.tid = mapObject->id;
 	hr.hid = recruitedHero->subID;
 	hr.hid = recruitedHero->subID;
 	hr.player = player;
 	hr.player = player;
-	hr.tile = recruitedHero->convertFromVisitablePos(mapObject->visitablePos());
-	if(gameHandler->getTile(hr.tile)->isWater())
+	hr.tile = recruitedHero->convertFromVisitablePos(targetPos );
+	if(gameHandler->getTile(hr.tile)->isWater() && !recruitedHero->boat)
 	{
 	{
 		//Create a new boat for hero
 		//Create a new boat for hero
-		gameHandler->createObject(mapObject->visitablePos(), Obj::BOAT, recruitedHero->getBoatType().getNum());
+		gameHandler->createObject(targetPos , Obj::BOAT, recruitedHero->getBoatType().getNum());
 
 
-		hr.boatId = gameHandler->getTopObj(hr.tile)->id;
+		hr.boatId = gameHandler->getTopObj(targetPos)->id;
 	}
 	}
 
 
 	// apply netpack -> this will remove hired hero from pool
 	// apply netpack -> this will remove hired hero from pool