Преглед на файлове

Merge pull request #2465 from vcmi/beta

Merge beta -> master
Ivan Savenko преди 2 години
родител
ревизия
07ab7c2f50
променени са 100 файла, в които са добавени 1988 реда и са изтрити 894 реда
  1. 6 2
      .github/workflows/github.yml
  2. 1 0
      .gitmodules
  3. 19 4
      AI/BattleAI/AttackPossibility.cpp
  4. 187 138
      AI/BattleAI/BattleAI.cpp
  5. 12 6
      AI/BattleAI/BattleAI.h
  6. 14 7
      AI/BattleAI/BattleExchangeVariant.cpp
  7. 2 8
      AI/BattleAI/BattleExchangeVariant.h
  8. 3 3
      AI/BattleAI/PotentialTargets.cpp
  9. 5 5
      AI/BattleAI/StackWithBonuses.cpp
  10. 2 8
      AI/BattleAI/StackWithBonuses.h
  11. 1 1
      AI/BattleAI/ThreatMap.cpp
  12. 11 0
      AI/EmptyAI/CEmptyAI.cpp
  13. 2 0
      AI/EmptyAI/CEmptyAI.h
  14. 1 1
      AI/FuzzyLite
  15. 71 55
      AI/Nullkiller/AIGateway.cpp
  16. 6 6
      AI/Nullkiller/AIGateway.h
  17. 14 16
      AI/Nullkiller/AIUtility.cpp
  18. 5 7
      AI/Nullkiller/AIUtility.h
  19. 87 34
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  20. 34 13
      AI/Nullkiller/Analyzers/ArmyManager.h
  21. 48 23
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  22. 5 2
      AI/Nullkiller/Analyzers/BuildAnalyzer.h
  23. 163 18
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  24. 14 1
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h
  25. 40 12
      AI/Nullkiller/Analyzers/HeroManager.cpp
  26. 4 0
      AI/Nullkiller/Analyzers/HeroManager.h
  27. 7 3
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  28. 5 11
      AI/Nullkiller/Analyzers/ObjectClusterizer.h
  29. 1 3
      AI/Nullkiller/Behaviors/BuildingBehavior.cpp
  30. 0 2
      AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp
  31. 10 6
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp
  32. 236 117
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  33. 5 0
      AI/Nullkiller/Behaviors/DefenceBehavior.h
  34. 105 22
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  35. 22 3
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  36. 2 4
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  37. 2 0
      AI/Nullkiller/CMakeLists.txt
  38. 2 2
      AI/Nullkiller/Engine/AIMemory.cpp
  39. 1 1
      AI/Nullkiller/Engine/DeepDecomposer.h
  40. 10 10
      AI/Nullkiller/Engine/FuzzyEngines.cpp
  41. 9 8
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  42. 1 1
      AI/Nullkiller/Engine/FuzzyHelper.h
  43. 42 13
      AI/Nullkiller/Engine/Nullkiller.cpp
  44. 7 3
      AI/Nullkiller/Engine/Nullkiller.h
  45. 243 109
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  46. 6 0
      AI/Nullkiller/Engine/PriorityEvaluator.h
  47. 1 3
      AI/Nullkiller/Goals/AbstractGoal.cpp
  48. 4 5
      AI/Nullkiller/Goals/AbstractGoal.h
  49. 0 2
      AI/Nullkiller/Goals/AdventureSpellCast.cpp
  50. 7 7
      AI/Nullkiller/Goals/Build.cpp
  51. 4 7
      AI/Nullkiller/Goals/BuildBoat.cpp
  52. 0 2
      AI/Nullkiller/Goals/BuildThis.cpp
  53. 3 3
      AI/Nullkiller/Goals/BuyArmy.cpp
  54. 1 3
      AI/Nullkiller/Goals/CompleteQuest.cpp
  55. 1 1
      AI/Nullkiller/Goals/CompleteQuest.h
  56. 49 11
      AI/Nullkiller/Goals/Composition.cpp
  57. 2 6
      AI/Nullkiller/Goals/Composition.h
  58. 0 2
      AI/Nullkiller/Goals/DismissHero.cpp
  59. 0 2
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp
  60. 21 9
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  61. 2 2
      AI/Nullkiller/Goals/GatherArmy.cpp
  62. 12 11
      AI/Nullkiller/Goals/RecruitHero.cpp
  63. 7 5
      AI/Nullkiller/Goals/RecruitHero.h
  64. 0 2
      AI/Nullkiller/Goals/SaveResources.cpp
  65. 2 2
      AI/Nullkiller/Goals/Trade.h
  66. 68 0
      AI/Nullkiller/Helpers/ArmyFormation.cpp
  67. 38 0
      AI/Nullkiller/Helpers/ArmyFormation.h
  68. 8 1
      AI/Nullkiller/Markers/ArmyUpgrade.cpp
  69. 1 0
      AI/Nullkiller/Markers/ArmyUpgrade.h
  70. 2 2
      AI/Nullkiller/Markers/DefendTown.cpp
  71. 4 1
      AI/Nullkiller/Markers/DefendTown.h
  72. 1 1
      AI/Nullkiller/Markers/HeroExchange.cpp
  73. 67 40
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  74. 14 11
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  75. 5 0
      AI/Nullkiller/Pathfinding/AIPathfinder.cpp
  76. 4 0
      AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
  77. 3 0
      AI/Nullkiller/Pathfinding/AIPathfinderConfig.h
  78. 0 1
      AI/Nullkiller/Pathfinding/Actions/BattleAction.cpp
  79. 0 1
      AI/Nullkiller/Pathfinding/Actions/BattleAction.h
  80. 5 6
      AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp
  81. 0 1
      AI/Nullkiller/Pathfinding/Actions/BoatActions.h
  82. 0 1
      AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp
  83. 0 1
      AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.h
  84. 0 1
      AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp
  85. 1 1
      AI/Nullkiller/Pathfinding/Actions/QuestAction.h
  86. 63 0
      AI/Nullkiller/Pathfinding/Actions/SpecialAction.cpp
  87. 38 1
      AI/Nullkiller/Pathfinding/Actions/SpecialAction.h
  88. 0 1
      AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp
  89. 0 1
      AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h
  90. 11 10
      AI/Nullkiller/Pathfinding/Actors.cpp
  91. 2 5
      AI/Nullkiller/Pathfinding/Actors.h
  92. 4 4
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp
  93. 1 1
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h
  94. 11 6
      AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
  95. 1 1
      AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.h
  96. 1 1
      AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp
  97. 1 1
      AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.h
  98. 4 2
      AI/Nullkiller/Pathfinding/Rules/AIPreviousNodeRule.cpp
  99. 1 1
      AI/Nullkiller/Pathfinding/Rules/AIPreviousNodeRule.h
  100. 40 17
      AI/StupidAI/StupidAI.cpp

+ 6 - 2
.github/workflows/github.yml

@@ -71,11 +71,15 @@ jobs:
           - platform: linux-qt6
             os: ubuntu-22.04
             test: 0
-            preset: linux-clang-release
+            preset: linux-clang-test
           - platform: linux
             os: ubuntu-20.04
             test: 0
-            preset: linux-gcc-release
+            preset: linux-gcc-test
+          - platform: linux
+            os: ubuntu-20.04
+            test: 0
+            preset: linux-gcc-debug
           - platform: mac-intel
             os: macos-12
             test: 0

+ 1 - 0
.gitmodules

@@ -1,6 +1,7 @@
 [submodule "test/googletest"]
 	path = test/googletest
 	url = https://github.com/google/googletest
+	branch = v1.13.x
 [submodule "AI/FuzzyLite"]
 	path = AI/FuzzyLite
 	url = https://github.com/fuzzylite/fuzzylite.git

+ 19 - 4
AI/BattleAI/AttackPossibility.cpp

@@ -50,12 +50,27 @@ int64_t AttackPossibility::calculateDamageReduce(
 	vstd::amin(damageDealt, defender->getAvailableHealth());
 
 	// FIXME: provide distance info for Jousting bonus
-	auto enemyDamageBeforeAttack = cb.battleEstimateDamage(defender, attacker, 0);
-	auto enemiesKilled = damageDealt / defender->MaxHealth() + (damageDealt % defender->MaxHealth() >= defender->getFirstHPleft() ? 1 : 0);
+	auto attackerUnitForMeasurement = attacker;
+
+	if(attackerUnitForMeasurement->isTurret())
+	{
+		auto ourUnits = cb.battleGetUnitsIf([&](const battle::Unit * u) -> bool
+			{
+				return u->unitSide() == attacker->unitSide() && !u->isTurret();
+			});
+
+		if(ourUnits.empty())
+			attackerUnitForMeasurement = defender;
+		else
+			attackerUnitForMeasurement = ourUnits.front();
+	}
+
+	auto enemyDamageBeforeAttack = cb.battleEstimateDamage(defender, attackerUnitForMeasurement, 0);
+	auto enemiesKilled = damageDealt / defender->getMaxHealth() + (damageDealt % defender->getMaxHealth() >= defender->getFirstHPleft() ? 1 : 0);
 	auto enemyDamage = averageDmg(enemyDamageBeforeAttack.damage);
 	auto damagePerEnemy = enemyDamage / (double)defender->getCount();
 
-	return (int64_t)(damagePerEnemy * (enemiesKilled * KILL_BOUNTY + damageDealt * HEALTH_BOUNTY / (double)defender->MaxHealth()));
+	return (int64_t)(damagePerEnemy * (enemiesKilled * KILL_BOUNTY + damageDealt * HEALTH_BOUNTY / (double)defender->getMaxHealth()));
 }
 
 int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state)
@@ -97,7 +112,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
 	auto attacker = attackInfo.attacker;
 	auto defender = attackInfo.defender;
 	const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
-	static const auto selectorBlocksRetaliation = Selector::type()(Bonus::BLOCKS_RETALIATION);
+	static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
 	const auto attackerSide = state.playerToSide(state.battleGetOwner(attacker));
 	const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
 

+ 187 - 138
AI/BattleAI/BattleAI.cpp

@@ -19,6 +19,7 @@
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/spells/ISpellMechanics.h"
 #include "../../lib/battle/BattleStateInfoForRetreat.h"
+#include "../../lib/battle/CObstacleInstance.h"
 #include "../../lib/CStack.h" // TODO: remove
                               // Eventually only IBattleInfoCallback and battle::Unit should be used,
                               // CUnitState should be private and CStack should be removed completely
@@ -87,175 +88,211 @@ void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
 	playerID = *CB->getPlayerID(); //TODO should be sth in callback
 	wasWaitingForRealize = CB->waitTillRealize;
 	wasUnlockingGs = CB->unlockGsWhenWaiting;
-	CB->waitTillRealize = true;
+	CB->waitTillRealize = false;
 	CB->unlockGsWhenWaiting = false;
 	movesSkippedByDefense = 0;
 }
 
-BattleAction CBattleAI::activeStack( const CStack * stack )
+BattleAction CBattleAI::useHealingTent(const CStack *stack)
 {
-	LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
+	auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE);
+	std::map<int, const CStack*> woundHpToStack;
+	for(const auto * stack : healingTargets)
+	{
+		if(auto woundHp = stack->getMaxHealth() - stack->getFirstHPleft())
+			woundHpToStack[woundHp] = stack;
+	}
 
-	BattleAction result = BattleAction::makeDefend(stack);
-	setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too)
+	if(woundHpToStack.empty())
+		return BattleAction::makeDefend(stack);
+	else
+		return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack
+}
 
-	try
+std::optional<PossibleSpellcast> CBattleAI::findBestCreatureSpell(const CStack *stack)
+{
+	//TODO: faerie dragon type spell should be selected by server
+	SpellID creatureSpellToCast = cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED);
+	if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE)
 	{
-		if(stack->type->idNumber == CreatureID::CATAPULT)
-			return useCatapult(stack);
-		if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->hasBonusOfType(Bonus::HEALER))
-		{
-			auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE);
-			std::map<int, const CStack*> woundHpToStack;
-			for(auto stack : healingTargets)
-				if(auto woundHp = stack->MaxHealth() - stack->getFirstHPleft())
-					woundHpToStack[woundHp] = stack;
-			if(woundHpToStack.empty())
-				return BattleAction::makeDefend(stack);
-			else
-				return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack
-		}
+		const CSpell * spell = creatureSpellToCast.toSpell();
 
-		attemptCastingSpell();
-
-		if(cb->battleIsFinished() || !stack->alive())
+		if(spell->canBeCast(getCbc().get(), spells::Mode::CREATURE_ACTIVE, stack))
 		{
-			//spellcast may finish battle or kill active stack
-			//send special preudo-action
-			BattleAction cancel;
-			cancel.actionType = EActionType::CANCEL;
-			return cancel;
-		}
+			std::vector<PossibleSpellcast> possibleCasts;
+			spells::BattleCast temp(getCbc().get(), stack, spells::Mode::CREATURE_ACTIVE, spell);
+			for(auto & target : temp.findPotentialTargets())
+			{
+				PossibleSpellcast ps;
+				ps.dest = target;
+				ps.spell = spell;
+				evaluateCreatureSpellcast(stack, ps);
+				possibleCasts.push_back(ps);
+			}
 
-		if(auto action = considerFleeingOrSurrendering())
-			return *action;
-		//best action is from effective owner point if view, we are effective owner as we received "activeStack"
+			std::sort(possibleCasts.begin(), possibleCasts.end(), [&](const PossibleSpellcast & lhs, const PossibleSpellcast & rhs) { return lhs.value > rhs.value; });
+			if(!possibleCasts.empty() && possibleCasts.front().value > 0)
+			{
+				return possibleCasts.front();
+			}
+		}
+	}
+	return std::nullopt;
+}
 
+BattleAction CBattleAI::selectStackAction(const CStack * stack)
+{
+	//evaluate casting spell for spellcasting stack
+	std::optional<PossibleSpellcast> bestSpellcast = findBestCreatureSpell(stack);
 
-		//evaluate casting spell for spellcasting stack
-		boost::optional<PossibleSpellcast> bestSpellcast(boost::none);
-		//TODO: faerie dragon type spell should be selected by server
-		SpellID creatureSpellToCast = cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED);
-		if(stack->hasBonusOfType(Bonus::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE)
-		{
-			const CSpell * spell = creatureSpellToCast.toSpell();
+	HypotheticBattle hb(env.get(), cb);
 
-			if(spell->canBeCast(getCbc().get(), spells::Mode::CREATURE_ACTIVE, stack))
-			{
-				std::vector<PossibleSpellcast> possibleCasts;
-				spells::BattleCast temp(getCbc().get(), stack, spells::Mode::CREATURE_ACTIVE, spell);
-				for(auto & target : temp.findPotentialTargets())
-				{
-					PossibleSpellcast ps;
-					ps.dest = target;
-					ps.spell = spell;
-					evaluateCreatureSpellcast(stack, ps);
-					possibleCasts.push_back(ps);
-				}
+	PotentialTargets targets(stack, hb);
+	BattleExchangeEvaluator scoreEvaluator(cb, env);
+	auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, targets, hb);
 
-				std::sort(possibleCasts.begin(), possibleCasts.end(), [&](const PossibleSpellcast & lhs, const PossibleSpellcast & rhs) { return lhs.value > rhs.value; });
-				if(!possibleCasts.empty() && possibleCasts.front().value > 0)
-				{
-					bestSpellcast = boost::optional<PossibleSpellcast>(possibleCasts.front());
-				}
-			}
-		}
+	int64_t score = EvaluationResult::INEFFECTIVE_SCORE;
 
-		HypotheticBattle hb(env.get(), cb);
-		
-		PotentialTargets targets(stack, hb);
-		BattleExchangeEvaluator scoreEvaluator(cb, env);
-		auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, targets, hb);
 
-		int64_t score = EvaluationResult::INEFFECTIVE_SCORE;
+	if(targets.possibleAttacks.empty() && bestSpellcast.has_value())
+	{
+		movesSkippedByDefense = 0;
+		return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
+	}
 
-		if(!targets.possibleAttacks.empty())
-		{
+	if(!targets.possibleAttacks.empty())
+	{
 #if BATTLE_TRACE_LEVEL>=1
-			logAi->trace("Evaluating attack for %s", stack->getDescription());
+		logAi->trace("Evaluating attack for %s", stack->getDescription());
 #endif
 
-			auto evaluationResult = scoreEvaluator.findBestTarget(stack, targets, hb);
-			auto & bestAttack = evaluationResult.bestAttack;
+		auto evaluationResult = scoreEvaluator.findBestTarget(stack, targets, hb);
+		auto & bestAttack = evaluationResult.bestAttack;
 
-			//TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc.
-			if(bestSpellcast.is_initialized() && bestSpellcast->value > bestAttack.damageDiff())
-			{
-				// return because spellcast value is damage dealt and score is dps reduce
-				movesSkippedByDefense = 0;
-				return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
-			}
+		//TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc.
+		if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff())
+		{
+			// return because spellcast value is damage dealt and score is dps reduce
+			movesSkippedByDefense = 0;
+			return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
+		}
 
-			if(evaluationResult.score > score)
+		if(evaluationResult.score > score)
+		{
+			score = evaluationResult.score;
+
+			logAi->debug("BattleAI: %s -> %s x %d, from %d curpos %d dist %d speed %d: +%lld -%lld = %lld",
+				bestAttack.attackerState->unitType()->getJsonKey(),
+				bestAttack.affectedUnits[0]->unitType()->getJsonKey(),
+				(int)bestAttack.affectedUnits[0]->getCount(),
+				(int)bestAttack.from,
+				(int)bestAttack.attack.attacker->getPosition().hex,
+				bestAttack.attack.chargeDistance,
+				bestAttack.attack.attacker->speed(0, true),
+				bestAttack.defenderDamageReduce,
+				bestAttack.attackerDamageReduce, bestAttack.attackValue()
+			);
+
+			if (moveTarget.score <= score)
 			{
-				score = evaluationResult.score;
-				std::string action;
-
 				if(evaluationResult.wait)
 				{
-					result = BattleAction::makeWait(stack);
-					action = "wait";
+					return BattleAction::makeWait(stack);
 				}
 				else if(bestAttack.attack.shooting)
 				{
-					result = BattleAction::makeShotAttack(stack, bestAttack.attack.defender);
-					action = "shot";
 					movesSkippedByDefense = 0;
+					return BattleAction::makeShotAttack(stack, bestAttack.attack.defender);
 				}
 				else
 				{
-					result = BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from);
-					action = "melee";
 					movesSkippedByDefense = 0;
+					return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from);
 				}
-
-				logAi->debug("BattleAI: %s -> %s x %d, %s, from %d curpos %d dist %d speed %d: +%lld -%lld = %lld",
-					bestAttack.attackerState->unitType()->getJsonKey(),
-					bestAttack.affectedUnits[0]->unitType()->getJsonKey(),
-					(int)bestAttack.affectedUnits[0]->getCount(), action, (int)bestAttack.from, (int)bestAttack.attack.attacker->getPosition().hex,
-					bestAttack.attack.chargeDistance, bestAttack.attack.attacker->Speed(0, true),
-					bestAttack.defenderDamageReduce, bestAttack.attackerDamageReduce, bestAttack.attackValue()
-				);
 			}
 		}
-		else if(bestSpellcast.is_initialized())
+	}
+
+	//ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code.
+	if(moveTarget.score > score)
+	{
+		score = moveTarget.score;
+
+		if(stack->waited())
+		{
+			return goTowardsNearest(stack, moveTarget.positions);
+		}
+		else
 		{
-			movesSkippedByDefense = 0;
-			return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
+			return BattleAction::makeWait(stack);
 		}
+	}
+
+	if(score <= EvaluationResult::INEFFECTIVE_SCORE
+		&& !stack->hasBonusOfType(BonusType::FLYING)
+		&& stack->unitSide() == BattleSide::ATTACKER
+		&& cb->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
+	{
+		auto brokenWallMoat = getBrokenWallMoatHexes();
 
-			//ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code.
-		if(moveTarget.score > score)
+		if(brokenWallMoat.size())
 		{
-			score = moveTarget.score;
+			movesSkippedByDefense = 0;
 
-			if(stack->waited())
-			{
-				result = goTowardsNearest(stack, moveTarget.positions);
-			}
+			if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition()))
+				return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT));
 			else
-			{
-				result = BattleAction::makeWait(stack);
-			}
+				return goTowardsNearest(stack, brokenWallMoat);
 		}
+	}
+
+	return BattleAction::makeDefend(stack);
+}
+
+void CBattleAI::yourTacticPhase(int distance)
+{
+	cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide()));
+}
+
+void CBattleAI::activeStack( const CStack * stack )
+{
+	LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
+
+	BattleAction result = BattleAction::makeDefend(stack);
+	setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too)
 
-		if(score <= EvaluationResult::INEFFECTIVE_SCORE
-			&& !stack->hasBonusOfType(Bonus::FLYING)
-			&& stack->unitSide() == BattleSide::ATTACKER
-			&& cb->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
+	try
+	{
+		if(stack->creatureId() == CreatureID::CATAPULT)
+		{
+			cb->battleMakeUnitAction(useCatapult(stack));
+			return;
+		}
+		if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER))
 		{
-			auto brokenWallMoat = getBrokenWallMoatHexes();
+			cb->battleMakeUnitAction(useHealingTent(stack));
+			return;
+		}
 
-			if(brokenWallMoat.size())
-			{
-				movesSkippedByDefense = 0;
+		attemptCastingSpell();
 
-				if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition()))
-					result = BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT));
-				else
-					result = goTowardsNearest(stack, brokenWallMoat);
-			}
+		if(cb->battleIsFinished() || !stack->alive())
+		{
+			//spellcast may finish battle or kill active stack
+			//send special preudo-action
+			BattleAction cancel;
+			cancel.actionType = EActionType::CANCEL;
+			cb->battleMakeUnitAction(cancel);
+			return;
 		}
+
+		if(auto action = considerFleeingOrSurrendering())
+		{
+			cb->battleMakeUnitAction(*action);
+			return;
+		}
+
+		result = selectStackAction(stack);
 	}
 	catch(boost::thread_interrupted &)
 	{
@@ -275,13 +312,13 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 		movesSkippedByDefense = 0;
 	}
 
-	return result;
+	cb->battleMakeUnitAction(result);
 }
 
 BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes) const
 {
 	auto reachability = cb->getReachability(stack);
-	auto avHexes = cb->battleGetAvailableHexes(reachability, stack);
+	auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false);
 
 	if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
 	{
@@ -320,27 +357,39 @@ BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vector<Battl
 
 	scoreEvaluator.updateReachabilityMap(hb);
 
-	if(stack->hasBonusOfType(Bonus::FLYING))
+	if(stack->hasBonusOfType(BonusType::FLYING))
 	{
-		std::set<BattleHex> moatHexes;
+		std::set<BattleHex> obstacleHexes;
 
-		if(hb.battleGetSiegeLevel() >= BuildingID::CITADEL)
-		{
-			auto townMoat = hb.getDefendedTown()->town->moatHexes;
+		auto insertAffected = [](const CObstacleInstance & spellObst, std::set<BattleHex> obstacleHexes) {
+			auto affectedHexes = spellObst.getAffectedTiles();
+			obstacleHexes.insert(affectedHexes.cbegin(), affectedHexes.cend());
+		};
+
+		const auto & obstacles = hb.battleGetAllObstacles();
+
+		for (const auto & obst: obstacles) {
 
-			moatHexes = std::set<BattleHex>(townMoat.begin(), townMoat.end());
+			if(obst->triggersEffects())
+			{
+				auto triggerAbility =  VLC->spells()->getById(obst->getTrigger());
+				auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage();
+
+				if(triggerIsNegative)
+					insertAffected(*obst, obstacleHexes);
+			}
 		}
 		// Flying stack doesn't go hex by hex, so we can't backtrack using predecessors.
 		// We just check all available hexes and pick the one closest to the target.
 		auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int
 		{
-			const int MOAT_PENALTY = 100; // avoid landing on moat
+			const int NEGATIVE_OBSTACLE_PENALTY = 100; // avoid landing on negative obstacle (moat, fire wall, etc)
 			const int BLOCKED_STACK_PENALTY = 100; // avoid landing on moat
 
 			auto distance = BattleHex::getDistance(bestNeighbor, hex);
 
-			if(vstd::contains(moatHexes, hex))
-				distance += MOAT_PENALTY;
+			if(vstd::contains(obstacleHexes, hex))
+				distance += NEGATIVE_OBSTACLE_PENALTY;
 
 			return scoreEvaluator.checkPositionBlocksOurStacks(hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance;
 		});
@@ -407,7 +456,7 @@ BattleAction CBattleAI::useCatapult(const CStack * stack)
 	attack.aimToHex(targetHex);
 	attack.actionType = EActionType::CATAPULT;
 	attack.side = side;
-	attack.stackNumber = stack->ID;
+	attack.stackNumber = stack->unitId();
 
 	movesSkippedByDefense = 0;
 
@@ -717,7 +766,7 @@ void CBattleAI::attemptCastingSpell()
 		spellcast.setTarget(castToPerform.dest);
 		spellcast.side = side;
 		spellcast.stackNumber = (!side) ? -1 : -2;
-		cb->battleMakeAction(&spellcast);
+		cb->battleMakeSpellAction(spellcast);
 		movesSkippedByDefense = 0;
 	}
 	else
@@ -777,7 +826,7 @@ void CBattleAI::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcas
 	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);
 	side = Side;
@@ -788,7 +837,7 @@ void CBattleAI::print(const std::string &text) const
 	logAi->trace("%s Battle AI[%p]: %s", playerID.getStr(), this, text);
 }
 
-boost::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
+std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
 {
 	BattleStateInfoForRetreat bs;
 
@@ -802,7 +851,7 @@ boost::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
 	{
 		if(stack->alive())
 		{
-			if(stack->side == bs.ourSide)
+			if(stack->unitSide() == bs.ourSide)
 				bs.ourStacks.push_back(stack);
 			else
 			{
@@ -814,9 +863,9 @@ boost::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
 
 	bs.turnsSkippedByDefense = movesSkippedByDefense / bs.ourStacks.size();
 
-	if(!bs.canFlee || !bs.canSurrender)
+	if(!bs.canFlee && !bs.canSurrender)
 	{
-		return boost::none;
+		return std::nullopt;
 	}
 
 	auto result = cb->makeSurrenderRetreatDecision(bs);

+ 12 - 6
AI/BattleAI/BattleAI.h

@@ -31,7 +31,7 @@ struct CurrentOffensivePotential
 	{
 		for(auto stack : cbc->battleGetStacks())
 		{
-			if(stack->side == side)
+			if(stack->unitSide() == side)
 				ourAttacks[stack] = PotentialTargets(stack);
 			else
 				enemyAttacks[stack] = PotentialTargets(stack);
@@ -59,7 +59,8 @@ class CBattleAI : public CBattleGameInterface
 	std::shared_ptr<Environment> env;
 
 	//Previous setting of cb
-	bool wasWaitingForRealize, wasUnlockingGs;
+	bool wasWaitingForRealize;
+	bool wasUnlockingGs;
 	int movesSkippedByDefense;
 
 public:
@@ -71,18 +72,23 @@ public:
 
 	void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
 
-	BattleAction activeStack(const CStack * stack) override; //called when it's turn of that stack
+	void activeStack(const CStack * stack) override; //called when it's turn of that stack
+	void yourTacticPhase(int distance) override;
 
-	boost::optional<BattleAction> considerFleeingOrSurrendering();
+	std::optional<BattleAction> considerFleeingOrSurrendering();
 
 	void print(const std::string &text) const;
 	BattleAction useCatapult(const CStack *stack);
-	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side) override;
+	BattleAction useHealingTent(const CStack *stack);
+	BattleAction selectStackAction(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, bool replayAllowed) override;
 	//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 battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
 	//void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override; //called when stack receives damage (after battleAttack())
-	//void battleEnd(const BattleResult *br) override;
+	//void battleEnd(const BattleResult *br, QueryID queryID) override;
 	//void battleResultsApplied() override; //called when all effects of last battle are applied
 	//void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied;
 	//void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn

+ 14 - 7
AI/BattleAI/BattleExchangeVariant.cpp

@@ -65,7 +65,7 @@ int64_t BattleExchangeVariant::trackAttack(
 	bool evaluateOnly)
 {
 	const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
-	static const auto selectorBlocksRetaliation = Selector::type()(Bonus::BLOCKS_RETALIATION);
+	static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
 	const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
 
 	DamageEstimation retaliation;
@@ -205,7 +205,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Uni
 	if(targets.unreachableEnemies.empty())
 		return result;
 
-	auto speed = activeStack->Speed();
+	auto speed = activeStack->speed();
 
 	if(speed == 0)
 		return result;
@@ -271,7 +271,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getAdjacentUnits(cons
 		auto hexes = stack->getSurroundingHexes();
 		for(auto hex : hexes)
 		{
-			auto neighbor = cb->battleGetStackByPos(hex);
+			auto neighbor = cb->battleGetUnitByPos(hex);
 
 			if(neighbor && neighbor->unitSide() == stack->unitSide() && !vstd::contains(checkedStacks, neighbor))
 			{
@@ -386,9 +386,13 @@ int64_t BattleExchangeEvaluator::calculateExchange(
 
 	for(auto unit : exchangeUnits)
 	{
+		if(unit->isTurret())
+			continue;
+
 		bool isOur = cb->battleMatchOwner(ap.attack.attacker, unit, true);
 		auto & attackerQueue = isOur ? ourStacks : enemyStacks;
 
+
 		if(!vstd::contains(attackerQueue, unit))
 		{
 			attackerQueue.push_back(unit);
@@ -593,6 +597,9 @@ void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb)
 
 		for(const battle::Unit * unit : turnQueue)
 		{
+			if(unit->isTurret())
+				continue;
+
 			if(turnBattle.battleCanShoot(unit))
 			{
 				for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
@@ -607,7 +614,7 @@ void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb)
 
 			for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
 			{
-				bool reachable = unitReachability.distances[hex] <= unit->Speed(turn);
+				bool reachable = unitReachability.distances[hex] <= unit->speed(turn);
 
 				if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
 				{
@@ -617,7 +624,7 @@ void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb)
 					{
 						for(BattleHex neighbor : hex.neighbouringTiles())
 						{
-							reachable = unitReachability.distances[neighbor] <= unit->Speed(turn);
+							reachable = unitReachability.distances[neighbor] <= unit->speed(turn);
 
 							if(reachable) break;
 						}
@@ -665,7 +672,7 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb
 			for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
 			{
 				bool enemyUnit = false;
-				bool reachable = unitReachability.distances[hex] <= unit->Speed(turn);
+				bool reachable = unitReachability.distances[hex] <= unit->speed(turn);
 
 				if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
 				{
@@ -677,7 +684,7 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb
 
 						for(BattleHex neighbor : hex.neighbouringTiles())
 						{
-							reachable = unitReachability.distances[neighbor] <= unit->Speed(turn);
+							reachable = unitReachability.distances[neighbor] <= unit->speed(turn);
 
 							if(reachable) break;
 						}

+ 2 - 8
AI/BattleAI/BattleExchangeVariant.h

@@ -56,10 +56,7 @@ struct EvaluationResult
 class BattleExchangeVariant
 {
 public:
-	BattleExchangeVariant()
-		:dpsScore(0), attackerValue()
-	{
-	}
+	BattleExchangeVariant(): dpsScore(0) {}
 
 	int64_t trackAttack(const AttackPossibility & ap, HypotheticBattle & state);
 
@@ -92,10 +89,7 @@ private:
 	std::vector<battle::Units> turnOrder;
 
 public:
-	BattleExchangeEvaluator(std::shared_ptr<CBattleInfoCallback> cb, std::shared_ptr<Environment> env)
-		:cb(cb), reachabilityMap(), env(env), turnOrder()
-	{
-	}
+	BattleExchangeEvaluator(std::shared_ptr<CBattleInfoCallback> cb, std::shared_ptr<Environment> env): cb(cb), env(env) {}
 
 	EvaluationResult findBestTarget(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb);
 	int64_t calculateExchange(const AttackPossibility & ap, PotentialTargets & targets, HypotheticBattle & hb);

+ 3 - 3
AI/BattleAI/PotentialTargets.cpp

@@ -15,14 +15,14 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
 {
 	auto attackerInfo = state.battleGetUnitByID(attacker->unitId());
 	auto reachability = state.getReachability(attackerInfo);
-	auto avHexes = state.battleGetAvailableHexes(reachability, attackerInfo);
+	auto avHexes = state.battleGetAvailableHexes(reachability, attackerInfo, false);
 
 	//FIXME: this should part of battleGetAvailableHexes
 	bool forceTarget = false;
 	const battle::Unit * forcedTarget = nullptr;
 	BattleHex forcedHex;
 
-	if(attackerInfo->hasBonusOfType(Bonus::ATTACKS_NEAREST_CREATURE))
+	if(attackerInfo->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE))
 	{
 		forceTarget = true;
 		auto nearest = state.getNearestStack(attackerInfo);
@@ -89,7 +89,7 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
 	{
 		auto & bestAp = possibleAttacks[0];
 
-		logGlobal->info("Battle AI best: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld",
+		logGlobal->debug("Battle AI best: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld",
 			bestAp.attack.attacker->unitType()->getJsonKey(),
 			state.battleGetUnitByPos(bestAp.dest)->unitType()->getJsonKey(),
 			(int)bestAp.dest, (int)bestAp.from, (int)bestAp.affectedUnits.size(),

+ 5 - 5
AI/BattleAI/StackWithBonuses.cpp

@@ -24,7 +24,7 @@ void actualizeEffect(TBonusListPtr target, const Bonus & ef)
 {
 	for(auto & bonus : *target) //TODO: optimize
 	{
-		if(bonus->source == Bonus::SPELL_EFFECT && bonus->type == ef.type && bonus->subtype == ef.subtype)
+		if(bonus->source == BonusSource::SPELL_EFFECT && bonus->type == ef.type && bonus->subtype == ef.subtype)
 		{
 			if(bonus->turnsRemain < ef.turnsRemain)
 			{
@@ -36,9 +36,9 @@ void actualizeEffect(TBonusListPtr target, const Bonus & ef)
 	}
 }
 
-StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const CStack * Stack)
+StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle::CUnitState * Stack)
 	: battle::CUnitState(),
-	origBearer(Stack),
+	origBearer(Stack->getBonusBearer()),
 	owner(Owner),
 	type(Stack->unitType()),
 	baseAmount(Stack->unitBaseAmount()),
@@ -126,7 +126,7 @@ TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, c
 	{
 		if(selector(&bonus) && (!limit || !limit(&bonus)))
 		{
-			if(ret->getFirst(Selector::source(Bonus::SPELL_EFFECT, bonus.sid).And(Selector::typeSubtype(bonus.type, bonus.subtype))))
+			if(ret->getFirst(Selector::source(BonusSource::SPELL_EFFECT, bonus.sid).And(Selector::typeSubtype(bonus.type, bonus.subtype))))
 			{
 				actualizeEffect(ret, bonus);
 			}
@@ -435,7 +435,7 @@ int64_t HypotheticBattle::getActualDamage(const DamageRange & damage, int32_t at
 
 int64_t HypotheticBattle::getTreeVersion() const
 {
-	return getBattleNode()->getTreeVersion() + bonusTreeVersion;
+	return getBonusBearer()->getTreeVersion() + bonusTreeVersion;
 }
 
 #if SCRIPTING_ENABLED

+ 2 - 8
AI/BattleAI/StackWithBonuses.h

@@ -14,16 +14,10 @@
 #include <vcmi/Environment.h>
 #include <vcmi/ServerCallback.h>
 
-#include "../../lib/HeroBonus.h"
+#include "../../lib/bonuses/Bonus.h"
 #include "../../lib/battle/BattleProxy.h"
 #include "../../lib/battle/CUnitState.h"
 
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CStack;
-
-VCMI_LIB_NAMESPACE_END
-
 class HypotheticBattle;
 
 ///Fake random generator, used by AI to evaluate random server behavior
@@ -54,7 +48,7 @@ public:
 	std::vector<Bonus> bonusesToUpdate;
 	std::set<std::shared_ptr<Bonus>> bonusesToRemove;
 
-	StackWithBonuses(const HypotheticBattle * Owner, const CStack * Stack);
+	StackWithBonuses(const HypotheticBattle * Owner, const battle::CUnitState * Stack);
 
 	StackWithBonuses(const HypotheticBattle * Owner, const battle::UnitInfo & info);
 

+ 1 - 1
AI/BattleAI/ThreatMap.cpp

@@ -30,7 +30,7 @@ ThreatMap::ThreatMap(const CStack *Endangered) : endangered(Endangered)
 	for(const CStack *enemy : getCbc()->battleGetStacks())
 	{
 		//Consider only stacks of different owner
-		if(enemy->side == endangered->side)
+		if(enemy->unitSide() == endangered->unitSide())
 			continue;
 
 		//Look-up which tiles can be melee-attacked

+ 11 - 0
AI/EmptyAI/CEmptyAI.cpp

@@ -11,6 +11,7 @@
 #include "CEmptyAI.h"
 
 #include "../../lib/CRandomGenerator.h"
+#include "../../lib/CStack.h"
 
 void CEmptyAI::saveGame(BinarySerializer & h, const int version)
 {
@@ -33,6 +34,16 @@ void CEmptyAI::yourTurn()
 	cb->endTurn();
 }
 
+void CEmptyAI::activeStack(const CStack * stack)
+{
+	cb->battleMakeUnitAction(BattleAction::makeDefend(stack));
+}
+
+void CEmptyAI::yourTacticPhase(int distance)
+{
+	cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide()));
+}
+
 void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID)
 {
 	cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID);

+ 2 - 0
AI/EmptyAI/CEmptyAI.h

@@ -24,6 +24,8 @@ public:
 
 	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
 	void yourTurn() override;
+	void yourTacticPhase(int distance) override;
+	void activeStack(const CStack * stack) override;
 	void heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) override;
 	void commanderGotLevel (const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override;
 	void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel) override;

+ 1 - 1
AI/FuzzyLite

@@ -1 +1 @@
-Subproject commit 9751a751a17c0682ed5d02e583c6a0cda8bc88e5
+Subproject commit 7aee562d6ca17f3cf42588ffb5116e03017c3c50

+ 71 - 55
AI/Nullkiller/AIGateway.cpp

@@ -11,10 +11,11 @@
 
 #include "../../lib/UnlockGuard.h"
 #include "../../lib/mapObjects/MapObjects.h"
+#include "../../lib/mapObjects/ObjectTemplate.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CHeroHandler.h"
 #include "../../lib/GameSettings.h"
-#include "../../lib/CGameState.h"
+#include "../../lib/gameState/CGameState.h"
 #include "../../lib/NetPacks.h"
 #include "../../lib/serializer/CTypeList.h"
 #include "../../lib/serializer/BinarySerializer.h"
@@ -28,7 +29,7 @@ namespace NKAI
 {
 
 // 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 double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
 
@@ -89,9 +90,11 @@ void AIGateway::heroMoved(const TryMoveHero & details, bool verbose)
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
 
-	validateObject(details.id); //enemy hero may have left visible area
 	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 to   = hero ? hero->convertToVisitablePos(details.end)   : (details.end   - int3(0,1,0));
 
@@ -187,7 +190,7 @@ void AIGateway::showShipyardDialog(const IShipyard * obj)
 
 void AIGateway::gameOver(PlayerColor player, const EVictoryLossCheckResult & victoryLossCheckResult)
 {
-	LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf);
+	LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString());
 	NET_EVENT_HANDLER;
 	logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.getStr(), player, player.getStr(), (victoryLossCheckResult.victory() ? "won" : "lost"));
 
@@ -256,7 +259,7 @@ void AIGateway::heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance
 	NET_EVENT_HANDLER;
 }
 
-void AIGateway::tileHidden(const std::unordered_set<int3, ShashInt3> & pos)
+void AIGateway::tileHidden(const std::unordered_set<int3> & pos)
 {
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
@@ -264,7 +267,7 @@ void AIGateway::tileHidden(const std::unordered_set<int3, ShashInt3> & pos)
 	nullkiller->memory->removeInvisibleObjects(myCb.get());
 }
 
-void AIGateway::tileRevealed(const std::unordered_set<int3, ShashInt3> & pos)
+void AIGateway::tileRevealed(const std::unordered_set<int3> & pos)
 {
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
@@ -447,6 +450,12 @@ void AIGateway::battleResultsApplied()
 	status.setBattle(NO_BATTLE);
 }
 
+void AIGateway::beforeObjectPropertyChanged(const SetObjectProperty * sop)
+{
+
+}
+
+
 void AIGateway::objectPropertyChanged(const SetObjectProperty * sop)
 {
 	LOG_TRACE(logAi);
@@ -501,8 +510,7 @@ void AIGateway::showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositio
 	NET_EVENT_HANDLER;
 }
 
-boost::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(
-	const BattleStateInfoForRetreat & battleState)
+std::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState)
 {
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
@@ -516,7 +524,7 @@ boost::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(
 		return BattleAction::makeRetreat(battleState.ourSide);
 	}
 
-	return boost::none;
+	return std::nullopt;
 }
 
 
@@ -699,7 +707,7 @@ void AIGateway::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstan
 	//you can't request action from action-response thread
 	requestActionASAP([=]()
 	{
-		if(removableUnits)
+		if(removableUnits && up->tempOwner == down->tempOwner)
 			pickBestCreatures(down, up);
 
 		answerQuery(queryID, 0);
@@ -771,28 +779,21 @@ void AIGateway::makeTurn()
 	boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
 	setThreadName("AIGateway::makeTurn");
 
+	cb->sendMessage("vcmieagles");
+
+	retrieveVisitableObjs();
+
 	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))
 			{
-				addVisitableObj(obj);
 				nullkiller->memory->markObjectUnvisited(obj);
 			}
 		}
 	}
 
-	cb->sendMessage("vcmieagles");
-
-	if(cb->getDate(Date::DAY) == 1)
-	{
-		retrieveVisitableObjs();
-	}
-
 #if NKAI_TRACE_LEVEL == 0
 	try
 	{
@@ -802,8 +803,8 @@ void AIGateway::makeTurn()
 		//for debug purpose
 		for (auto h : cb->getHeroesInfo())
 		{
-			if (h->movement)
-				logAi->warn("Hero %s has %d MP left", h->getNameTranslated(), h->movement);
+			if (h->movementPointsRemaining())
+				logAi->info("Hero %s has %d MP left", h->getNameTranslated(), h->movementPointsRemaining());
 		}
 #if NKAI_TRACE_LEVEL == 0
 	}
@@ -866,6 +867,19 @@ void AIGateway::pickBestCreatures(const CArmedInstance * destinationArmy, const
 
 	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
 	for(SlotID i = SlotID(0); i.validSlot(); i.advance(1)) //i-th strongest creature type will go to i-th slot
 	{
@@ -1000,7 +1014,7 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 				//FIXME: why are the above possible to be null?
 
 				bool emptySlotFound = false;
-				for(auto slot : artifact->artType->possibleSlots.at(target->bearerType()))
+				for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
 				{
 					ArtifactLocation destLocation(target, slot);
 					if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move
@@ -1013,7 +1027,7 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 				}
 				if(!emptySlotFound) //try to put that atifact in already occupied slot
 				{
-					for(auto slot : artifact->artType->possibleSlots.at(target->bearerType()))
+					for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
 					{
 						auto otherSlot = target->getSlot(slot);
 						if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one
@@ -1053,23 +1067,28 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
 		int count = d->creatures[i].first;
 		CreatureID creID = d->creatures[i].second.back();
 
-		vstd::amin(count, cb->getResourceAmount() / VLC->creh->objects[creID]->cost);
+		if(!recruiter->getSlotFor(creID).validSlot())
+		{
+			continue;
+		}
+
+		vstd::amin(count, cb->getResourceAmount() / creID.toCreature()->getFullRecruitCost());
 		if(count > 0)
 			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;
 	assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_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
 	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)
+void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
 {
 	NET_EVENT_HANDLER;
 	assert(status.getBattle() == ONGOING_BATTLE);
@@ -1077,7 +1096,17 @@ void AIGateway::battleEnd(const BattleResult * br)
 	bool won = br->winner == myCb->battleGetMySide();
 	logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename);
 	battlename.clear();
-	CAdventureAI::battleEnd(br);
+
+	if (queryID != -1)
+	{
+		status.addQuery(queryID, "Combat result dialog");
+		const int confirmAction = 0;
+		requestActionASAP([=]()
+		{
+			answerQuery(queryID, confirmAction);
+		});
+	}
+	CAdventureAI::battleEnd(br, queryID);
 }
 
 void AIGateway::waitTillFree()
@@ -1086,26 +1115,13 @@ void AIGateway::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()
 {
 	foreach_tile_pos([&](const int3 & pos)
 	{
 		for(const CGObjectInstance * obj : myCb->getVisitableObjs(pos, false))
 		{
-			if(obj->tempOwner != playerID)
-				addVisitableObj(obj);
+			addVisitableObj(obj);
 		}
 	});
 }
@@ -1163,7 +1179,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 	if(startHpos == dst)
 	{
 		//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));
 		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
@@ -1190,11 +1206,11 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 			//return cb->getTile(coord,false)->topVisitableObj(ignoreHero);
 		};
 
-		auto isTeleportAction = [&](CGPathNode::ENodeAction action) -> bool
+		auto isTeleportAction = [&](EPathNodeAction action) -> bool
 		{
-			if(action != CGPathNode::TELEPORT_NORMAL && action != CGPathNode::TELEPORT_BLOCKING_VISIT)
+			if(action != EPathNodeAction::TELEPORT_NORMAL && action != EPathNodeAction::TELEPORT_BLOCKING_VISIT)
 			{
-				if(action != CGPathNode::TELEPORT_BATTLE)
+				if(action != EPathNodeAction::TELEPORT_BATTLE)
 				{
 					return false;
 				}
@@ -1305,7 +1321,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 				doChannelProbing();
 		}
 
-		if(path.nodes[0].action == CGPathNode::BLOCKING_VISIT || path.nodes[0].action == CGPathNode::BATTLE)
+		if(path.nodes[0].action == EPathNodeAction::BLOCKING_VISIT || path.nodes[0].action == EPathNodeAction::BATTLE)
 		{
 			// when we take resource we do not reach its position. We even might not move
 			// also guarded town is not get visited automatically after capturing
@@ -1359,7 +1375,7 @@ void AIGateway::tryRealize(Goals::DigAtTile & g)
 
 void AIGateway::tryRealize(Goals::Trade & g) //trade
 {
-	if(cb->getResourceAmount((Res::ERes)g.resID) >= g.value) //goal is already fulfilled. Why we need this check, anyway?
+	if(cb->getResourceAmount(GameResID(g.resID)) >= g.value) //goal is already fulfilled. Why we need this check, anyway?
 		throw goalFulfilledException(sptr(g));
 
 	int accquiredResources = 0;
@@ -1368,10 +1384,10 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
 		if(const IMarket * m = IMarket::castFrom(obj, false))
 		{
 			auto freeRes = cb->getResourceAmount(); //trade only resources which are not reserved
-			for(auto it = Res::ResourceSet::nziterator(freeRes); it.valid(); it++)
+			for(auto it = ResourceSet::nziterator(freeRes); it.valid(); it++)
 			{
 				auto res = it->resType;
-				if(res == g.resID) //sell any other resource
+				if(res.getNum() == g.resID) //sell any other resource
 					continue;
 
 				int toGive, toGet;
@@ -1380,11 +1396,11 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
 				//TODO trade only as much as needed
 				if (toGive) //don't try to sell 0 resources
 				{
-					cb->trade(obj, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive);
+					cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive);
 					accquiredResources = static_cast<int>(toGet * (it->resVal / toGive));
 					logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName());
 				}
-				if (cb->getResourceAmount((Res::ERes)g.resID) >= g.value)
+				if (cb->getResourceAmount(GameResID(g.resID)))
 					throw goalFulfilledException(sptr(g)); //we traded all we needed
 			}
 

+ 6 - 6
AI/Nullkiller/AIGateway.h

@@ -127,7 +127,7 @@ public:
 	void heroMoved(const TryMoveHero & details, bool verbose = true) override;
 	void heroInGarrisonChange(const CGTownInstance * town) override;
 	void centerView(int3 pos, int focusTime) override;
-	void tileHidden(const std::unordered_set<int3, ShashInt3> & pos) override;
+	void tileHidden(const std::unordered_set<int3> & pos) override;
 	void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override;
 	void artifactAssembled(const ArtifactLocation & al) override;
 	void showTavernWindow(const CGObjectInstance * townOrTavern) override;
@@ -142,7 +142,7 @@ public:
 	void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override;
 	void availableArtifactsChanged(const CGBlackMarket * bm = nullptr) override;
 	void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override;
-	void tileRevealed(const std::unordered_set<int3, ShashInt3> & pos) override;
+	void tileRevealed(const std::unordered_set<int3> & pos) override;
 	void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override;
 	void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override;
 	void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override;
@@ -161,15 +161,16 @@ public:
 	void heroManaPointsChanged(const CGHeroInstance * hero) override;
 	void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override;
 	void battleResultsApplied() override;
+	void beforeObjectPropertyChanged(const SetObjectProperty * sop) override;
 	void objectPropertyChanged(const SetObjectProperty * sop) override;
 	void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override;
 	void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override;
 	void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override;
 	void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
-	boost::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 battleEnd(const BattleResult * br) 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 makeTurn();
 
@@ -194,7 +195,6 @@ public:
 
 	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 retrieveVisitableObjs(std::vector<const CGObjectInstance *> & out, bool includeOwned = false) const;
 	void retrieveVisitableObjs();
 	virtual std::vector<const CGObjectInstance *> getFlaggedObjects() const;
 

+ 14 - 16
AI/Nullkiller/AIUtility.cpp

@@ -17,9 +17,11 @@
 #include "../../lib/CHeroHandler.h"
 #include "../../lib/mapObjects/MapObjects.h"
 #include "../../lib/mapping/CMapDefines.h"
-
+#include "../../lib/gameState/QuestInfo.h"
 #include "../../lib/GameSettings.h"
 
+#include <vcmi/CreatureService.h>
+
 namespace NKAI
 {
 
@@ -253,7 +255,7 @@ bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, Pla
 	{
 		auto quest = dynamic_cast<const CGKeys *>(obj);
 
-		if(quest->passableFor(playerColor))
+		if(quest->wasMyColorVisited(playerColor))
 			return true;
 	}
 
@@ -264,7 +266,7 @@ bool isBlockVisitObj(const int3 & pos)
 {
 	if(auto obj = cb->getTopObj(pos))
 	{
-		if(obj->blockVisit) //we can't stand on that object
+		if(obj->isBlockedVisitable()) //we can't stand on that object
 			return true;
 	}
 
@@ -278,8 +280,8 @@ creInfo infoFromDC(const dwellingContent & dc)
 	ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed
 	if (ci.creID != -1)
 	{
-		ci.cre = VLC->creh->objects[ci.creID].get();
-		ci.level = ci.cre->level; //this is cretaure tier, while tryRealize expects dwelling level. Ignore.
+		ci.cre = VLC->creatures()->getById(ci.creID);
+		ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore.
 	}
 	else
 	{
@@ -304,10 +306,10 @@ bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2
 	auto art1 = a1->artType;
 	auto art2 = a2->artType;
 
-	if(art1->price == art2->price)
-		return art1->valOfBonuses(Bonus::PRIMARY_SKILL) > art2->valOfBonuses(Bonus::PRIMARY_SKILL);
+	if(art1->getPrice() == art2->getPrice())
+		return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);
 	else
-		return art1->price > art2->price;
+		return art1->getPrice() > art2->getPrice();
 }
 
 bool isWeeklyRevisitable(const CGObjectInstance * obj)
@@ -317,17 +319,13 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj)
 
 	//TODO: allow polling of remaining creatures in dwelling
 	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj))
-		return rewardable->getResetDuration() == 7;
+		return rewardable->configuration.getResetDuration() == 7;
 
 	if(dynamic_cast<const CGDwelling *>(obj))
 		return true;
-	if(dynamic_cast<const CBank *>(obj)) //banks tend to respawn often in mods
-		return true;
 
 	switch(obj->ID)
 	{
-	case Obj::STABLES:
-	case Obj::MAGIC_WELL:
 	case Obj::HILL_FORT:
 		return true;
 	case Obj::BORDER_GATE:
@@ -398,7 +396,7 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject
 			{
 				if(level.first
 					&& h->getSlotFor(CreatureID(c)) != SlotID()
-					&& ai->cb->getResourceAmount().canAfford(c.toCreature()->cost))
+					&& ai->cb->getResourceAmount().canAfford(c.toCreature()->getFullRecruitCost()))
 				{
 					return true;
 				}
@@ -411,7 +409,7 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject
 	{
 		for(auto slot : h->Slots())
 		{
-			if(slot.second->type->upgrades.size())
+			if(slot.second->type->hasUpgrades())
 				return true; //TODO: check price?
 		}
 		return false;
@@ -438,7 +436,7 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject
 			return false;
 
 		TResources myRes = ai->getFreeResources();
-		if(myRes[Res::GOLD] < 2000 || myRes[Res::GEMS] < 10)
+		if(myRes[EGameResID::GOLD] < 2000 || myRes[EGameResID::GEMS] < 10)
 			return false;
 		break;
 	}

+ 5 - 7
AI/Nullkiller/AIUtility.h

@@ -45,16 +45,14 @@
 #include "../../lib/CTownHandler.h"
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/CStopWatch.h"
-#include "../../lib/mapObjects/CObjectHandler.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
-#include "../../lib/CPathfinder.h"
 #include "../../CCallback.h"
 
 #include <chrono>
 
 using namespace tbb;
 
-typedef std::pair<ui32, std::vector<CreatureID>> dwellingContent;
+using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
 
 namespace NKAI
 {
@@ -161,7 +159,7 @@ struct creInfo
 {
 	int count;
 	CreatureID creID;
-	CCreature * cre;
+	const Creature * cre;
 	int level;
 };
 creInfo infoFromDC(const dwellingContent & dc);
@@ -305,10 +303,10 @@ public:
 public:
 	using ptr_type = std::unique_ptr<T, External_Deleter>;
 
-	SharedPool(std::function<std::unique_ptr<T>()> elementFactory)
-		: elementFactory(elementFactory), pool(), sync(), instance_tracker(new SharedPool<T>*(this))
+	SharedPool(std::function<std::unique_ptr<T>()> elementFactory):
+		elementFactory(elementFactory), pool(), instance_tracker(new SharedPool<T> *(this))
 	{}
-	
+
 	void add(std::unique_ptr<T> t)
 	{
 		boost::lock_guard<boost::mutex> lock(sync);

+ 87 - 34
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -13,6 +13,7 @@
 #include "../Engine/Nullkiller.h"
 #include "../../../CCallback.h"
 #include "../../../lib/mapObjects/MapObjects.h"
+#include "../../../lib/GameConstants.h"
 
 namespace NKAI
 {
@@ -28,11 +29,50 @@ public:
 	StackUpgradeInfo(CreatureID initial, CreatureID upgraded, int count)
 		:initialCreature(initial), upgradedCreature(upgraded), count(count)
 	{
-		cost = (upgradedCreature.toCreature()->cost - initialCreature.toCreature()->cost) * count;
-		upgradeValue = (upgradedCreature.toCreature()->AIValue - initialCreature.toCreature()->AIValue) * count;
+		cost = (upgradedCreature.toCreature()->getFullRecruitCost() - initialCreature.toCreature()->getFullRecruitCost()) * count;
+		upgradeValue = (upgradedCreature.toCreature()->getAIValue() - initialCreature.toCreature()->getAIValue()) * count;
 	}
 };
 
+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
 {
 	return howManyReinforcementsCanGet(hero, hero, source);
@@ -50,9 +90,10 @@ std::vector<SlotInfo> ArmyManager::getSortedSlots(const CCreatureSet * target, c
 	{
 		for(auto & i : armyPtr->Slots())
 		{
-			auto & slotInfp = creToPower[i.second->type];
+			auto cre = dynamic_cast<const CCreature*>(i.second->type);
+			auto & slotInfp = creToPower[cre];
 
-			slotInfp.creature = i.second->type;
+			slotInfp.creature = cre;
 			slotInfp.power += i.second->getPower();
 			slotInfp.count += i.second->count;
 		}
@@ -73,10 +114,10 @@ std::vector<SlotInfo>::iterator ArmyManager::getWeakestCreature(std::vector<Slot
 {
 	auto weakest = boost::min_element(army, [](const SlotInfo & left, const SlotInfo & right) -> bool
 	{
-		if(left.creature->level != right.creature->level)
-			return left.creature->level < right.creature->level;
+		if(left.creature->getLevel() != right.creature->getLevel())
+			return left.creature->getLevel() < right.creature->getLevel();
 		
-		return left.creature->Speed() > right.creature->Speed();
+		return left.creature->speed() > right.creature->speed();
 	});
 
 	return weakest;
@@ -95,24 +136,24 @@ public:
 std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const
 {
 	auto sortedSlots = getSortedSlots(target, source);
-	std::map<TFaction, uint64_t> alignmentMap;
+	std::map<FactionID, uint64_t> alignmentMap;
 
 	for(auto & slot : sortedSlots)
 	{
-		alignmentMap[slot.creature->faction] += slot.power;
+		alignmentMap[slot.creature->getFaction()] += slot.power;
 	}
 
-	std::set<TFaction> allowedFactions;
+	std::set<FactionID> allowedFactions;
 	std::vector<SlotInfo> resultingArmy;
 	uint64_t armyValue = 0;
 
 	TemporaryArmy newArmyInstance;
-	auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(Bonus::MORALE));
+	auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE));
 
 	for(auto bonus : *bonusModifiers)
 	{
 		// army bonuses will change and object bonuses are temporary
-		if(bonus->source != Bonus::ARMY && bonus->source != Bonus::OBJECT)
+		if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT)
 		{
 			newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
 		}
@@ -120,7 +161,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 
 	while(allowedFactions.size() < alignmentMap.size())
 	{
-		auto strongestAlignment = vstd::maxElementByFun(alignmentMap, [&](std::pair<TFaction, uint64_t> pair) -> uint64_t
+		auto strongestAlignment = vstd::maxElementByFun(alignmentMap, [&](std::pair<FactionID, uint64_t> pair) -> uint64_t
 		{
 			return vstd::contains(allowedFactions, pair.first) ? 0 : pair.second;
 		});
@@ -133,13 +174,13 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 
 		for(auto & slot : sortedSlots)
 		{
-			if(vstd::contains(allowedFactions, slot.creature->faction))
+			if(vstd::contains(allowedFactions, slot.creature->getFaction()))
 			{
-				auto slotID = newArmyInstance.getSlotFor(slot.creature);
+				auto slotID = newArmyInstance.getSlotFor(slot.creature->getId());
 
 				if(slotID.validSlot())
 				{
-					newArmyInstance.setCreature(slotID, slot.creature->idNumber, slot.count);
+					newArmyInstance.setCreature(slotID, slot.creature->getId(), slot.count);
 					newArmy.push_back(slot);
 				}
 			}
@@ -149,7 +190,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 
 		for(auto & slot : newArmyInstance.Slots())
 		{
-			auto morale = slot.second->MoraleVal();
+			auto morale = slot.second->moraleVal();
 			auto multiplier = 1.0f;
 
 			const float BadMoraleChance = 0.083f;
@@ -217,7 +258,7 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
 		if(!ci.count || ci.creID == -1)
 			continue;
 
-		vstd::amin(ci.count, availableRes / ci.cre->cost); //max count we can afford
+		vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford
 
 		if(!ci.count)
 			continue;
@@ -228,7 +269,7 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
 			break;
 
 		army->setCreature(dst, ci.creID, ci.count);
-		availableRes -= ci.cre->cost * ci.count;
+		availableRes -= ci.cre->getFullRecruitCost() * ci.count;
 	}
 
 	return army;
@@ -237,14 +278,15 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
 ui64 ArmyManager::howManyReinforcementsCanBuy(
 	const CCreatureSet * targetArmy,
 	const CGDwelling * dwelling,
-	const TResources & availableResources) const
+	const TResources & availableResources,
+	uint8_t turn) const
 {
 	ui64 aivalue = 0;
 	auto army = getArmyAvailableToBuy(targetArmy, dwelling, availableResources);
 
 	for(const creInfo & ci : army)
 	{
-		aivalue += ci.count * ci.cre->AIValue;
+		aivalue += ci.count * ci.cre->getAIValue();
 	}
 
 	return aivalue;
@@ -258,17 +300,29 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(const CCreatureSet * her
 std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 	const CCreatureSet * hero,
 	const CGDwelling * dwelling,
-	TResources availableRes) const
+	TResources availableRes,
+	uint8_t turn) const
 {
 	std::vector<creInfo> creaturesInDwellings;
 	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--)
 	{
 		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);
 		if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
@@ -279,14 +333,13 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 				freeHeroSlots--; //new slot will be occupied
 		}
 
-		vstd::amin(ci.count, availableRes / ci.cre->cost); //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
 		creaturesInDwellings.push_back(ci);
-		availableRes -= ci.cre->cost * ci.count;
+		availableRes -= ci.cre->getFullRecruitCost() * ci.count;
 	}
 
 	return creaturesInDwellings;
@@ -306,9 +359,9 @@ ui64 ArmyManager::howManyReinforcementsCanGet(const IBonusBearer * armyCarrier,
 	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->AIValue * count;
+	return creature->getAIValue() * count;
 }
 
 SlotInfo ArmyManager::getTotalCreaturesAvailable(CreatureID creatureID) const
@@ -378,12 +431,12 @@ std::vector<StackUpgradeInfo> ArmyManager::getHillFortUpgrades(const CCreatureSe
 
 		CreatureID strongestUpgrade = *vstd::minElementByFun(possibleUpgrades, [](CreatureID cre) -> uint64_t
 		{
-			return cre.toCreature()->AIValue;
+			return cre.toCreature()->getAIValue();
 		});
 
 		StackUpgradeInfo upgrade = StackUpgradeInfo(initial, strongestUpgrade, creature.second->count);
 
-		if(initial.toCreature()->level == 1)
+		if(initial.toCreature()->getLevel() == 1)
 			upgrade.cost = TResources();
 
 		upgrades.push_back(upgrade);
@@ -417,7 +470,7 @@ std::vector<StackUpgradeInfo> ArmyManager::getDwellingUpgrades(const CCreatureSe
 
 		CreatureID strongestUpgrade = *vstd::minElementByFun(possibleUpgrades, [](CreatureID cre) -> uint64_t
 		{
-			return cre.toCreature()->AIValue;
+			return cre.toCreature()->getAIValue();
 		});
 
 		StackUpgradeInfo upgrade = StackUpgradeInfo(initial, strongestUpgrade, creature.second->count);
@@ -488,7 +541,7 @@ ArmyUpgradeInfo ArmyManager::calculateCreaturesUpgrade(
 			upgradedArmy.power = evaluateStackPower(upgradedArmy.creature, upgradedArmy.count);
 
 			auto slotToReplace = std::find_if(result.resultingArmy.begin(), result.resultingArmy.end(), [&](const SlotInfo & slot) -> bool {
-				return slot.count == upgradedArmy.count && slot.creature->idNumber == upgrade.initialCreature;
+				return slot.count == upgradedArmy.count && slot.creature->getId() == upgrade.initialCreature;
 			});
 
 			resourcesLeft -= upgrade.cost;

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

@@ -32,13 +32,11 @@ struct SlotInfo
 struct ArmyUpgradeInfo
 {
 	std::vector<SlotInfo> resultingArmy;
-	uint64_t upgradeValue;
+	uint64_t upgradeValue = 0;
 	TResources upgradeCost;
 
-	ArmyUpgradeInfo()
-		: resultingArmy(), upgradeValue(0), upgradeCost()
-	{
-	}
+	void addArmyToBuy(std::vector<SlotInfo> army);
+	void addArmyToGet(std::vector<SlotInfo> army);
 };
 
 class DLL_EXPORT IArmyManager //: public: IAbstractManager
@@ -50,20 +48,33 @@ public:
 	virtual	ui64 howManyReinforcementsCanBuy(
 		const CCreatureSet * targetArmy,
 		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 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>::iterator getWeakestCreature(std::vector<SlotInfo> & army) 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 ArmyUpgradeInfo calculateCreaturesUpgrade(
 		const CCreatureSet * army,
 		const CGObjectInstance * upgrader,
 		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;
 };
 
@@ -79,20 +90,30 @@ private:
 public:
 	ArmyManager(CPlayerSpecificInfoCallback * CB, const Nullkiller * ai): cb(CB), ai(ai) {}
 	void update() override;
+
 	ui64 howManyReinforcementsCanBuy(const CCreatureSet * target, const CGDwelling * source) const override;
 	ui64 howManyReinforcementsCanBuy(
 		const CCreatureSet * targetArmy,
 		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 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> 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,
+		TResources availableRes,
+		uint8_t turn = 0) 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;
 	ArmyUpgradeInfo calculateCreaturesUpgrade(
 		const CCreatureSet * army, 

+ 48 - 23
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -9,7 +9,6 @@
 */
 #include "../StdInc.h"
 #include "../Engine/Nullkiller.h"
-#include "../../../lib/mapping/CMap.h" //for victory conditions
 #include "../Engine/Nullkiller.h"
 
 namespace NKAI
@@ -69,19 +68,22 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
 	logAi->trace("Checking other buildings");
 
 	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)
 	{
 		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 & buildingID : buildingSet)
 		{
-			if(!developmentInfo.town->hasBuilt(buildingID))
+			if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->town->buildings.count(buildingID))
 			{
 				developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID));
 
@@ -93,9 +95,9 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
 
 int32_t convertToGold(const TResources & res)
 {
-	return res[Res::GOLD] 
-		+ 75 * (res[Res::WOOD] + res[Res::ORE]) 
-		+ 125 * (res[Res::GEMS] + res[Res::CRYSTAL] + res[Res::MERCURY] + res[Res::SULFUR]);
+	return res[EGameResID::GOLD] 
+		+ 75 * (res[EGameResID::WOOD] + res[EGameResID::ORE]) 
+		+ 125 * (res[EGameResID::GEMS] + res[EGameResID::CRYSTAL] + res[EGameResID::MERCURY] + res[EGameResID::SULFUR]);
 }
 
 TResources BuildAnalyzer::getResourcesRequiredNow() const
@@ -164,8 +166,8 @@ void BuildAnalyzer::update()
 	}
 	else
 	{
-		goldPreasure = ai->getLockedResources()[Res::GOLD] / 10000.0f
-			+ (float)armyCost[Res::GOLD] / (1 + ai->getFreeGold() + (float)dailyIncome[Res::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);
@@ -191,12 +193,28 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 	const CCreature * creature = nullptr;
 	CreatureID baseCreatureID;
 
+	int creatureLevel = -1;
+	int creatureUpgrade = 0;
+
 	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();
 
 		baseCreatureID = creatures.front();
@@ -280,7 +298,7 @@ void BuildAnalyzer::updateDailyIncome()
 
 		if(mine)
 		{
-			dailyIncome[mine->producedResource] += mine->producedQuantity;
+			dailyIncome[mine->producedResource.getNum()] += mine->producedQuantity;
 		}
 	}
 
@@ -294,7 +312,7 @@ bool BuildAnalyzer::hasAnyBuilding(int32_t alignment, BuildingID bid) const
 {
 	for(auto tdi : developmentInfos)
 	{
-		if(tdi.town->alignment == alignment && tdi.town->hasBuilt(bid))
+		if(tdi.town->subID == alignment && tdi.town->hasBuilt(bid))
 			return true;
 	}
 
@@ -355,10 +373,10 @@ BuildingInfo::BuildingInfo(
 
 	if(creature)
 	{
-		creatureGrows = creature->growth;
-		creatureID = creature->idNumber;
-		creatureCost = creature->cost;
-		creatureLevel = creature->level;
+		creatureGrows = creature->getGrowth();
+		creatureID = creature->getId();
+		creatureCost = creature->getFullRecruitCost();
+		creatureLevel = creature->getLevel();
 		baseCreatureID = baseCreature;
 
 		if(exists)
@@ -367,12 +385,19 @@ BuildingInfo::BuildingInfo(
 		}
 		else
 		{
-			creatureGrows = creature->growth;
+			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);

+ 5 - 2
AI/Nullkiller/Analyzers/BuildAnalyzer.h

@@ -62,8 +62,11 @@ public:
 	HeroRole townRole;
 	bool hasSomethingToBuild;
 
-	TownDevelopmentInfo(const CGTownInstance* town)
-		:town(town), armyStrength(0), toBuild(), townDevelopmentCost(), requiredResources(), townRole(HeroRole::SCOUT), hasSomethingToBuild(false)
+	TownDevelopmentInfo(const CGTownInstance * town):
+		town(town),
+		armyStrength(0),
+		townRole(HeroRole::SCOUT),
+		hasSomethingToBuild(false)
 	{
 	}
 

+ 163 - 18
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -8,7 +8,8 @@
 *
 */
 #include "../StdInc.h"
-#include "lib/mapping/CMap.h" //for victory conditions
+#include "DangerHitMapAnalyzer.h"
+
 #include "../Engine/Nullkiller.h"
 
 namespace NKAI
@@ -16,20 +17,29 @@ namespace NKAI
 
 HitMapInfo HitMapInfo::NoTreat;
 
+double HitMapInfo::value() const
+{
+	return danger / std::sqrt(turn / 3.0f + 1);
+}
+
 void DangerHitMapAnalyzer::updateHitMap()
 {
-	if(upToDate)
+	if(hitMapUpToDate)
 		return;
 
 	logAi->trace("Update danger hitmap");
 
-	upToDate = true;
+	hitMapUpToDate = true;
 	auto start = std::chrono::high_resolution_clock::now();
 
 	auto cb = ai->cb.get();
 	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();
+	townTreats.clear();
 
 	std::map<PlayerColor, std::map<const CGHeroInstance *, HeroRole>> heroes;
 
@@ -66,27 +76,26 @@ void DangerHitMapAnalyzer::updateHitMap()
 				if(path.getFirstBlockedAction())
 					continue;
 
-				auto tileDanger = path.getHeroStrength();
-				auto turn = path.turn();
 				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);
 					
@@ -94,6 +103,26 @@ void DangerHitMapAnalyzer::updateHitMap()
 					{
 						if(cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES)
 							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;
+							}
+						}
 					}
 				}
 			}
@@ -103,6 +132,122 @@ void DangerHitMapAnalyzer::updateHitMap()
 	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
 {
 	int3 tile = path.targetTile();
@@ -143,7 +288,7 @@ const std::set<const CGObjectInstance *> & DangerHitMapAnalyzer::getOneTurnAcces
 
 void DangerHitMapAnalyzer::reset()
 {
-	upToDate = false;
+	hitMapUpToDate = false;
 }
 
 }

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

@@ -14,6 +14,8 @@
 namespace NKAI
 {
 
+struct AIPath;
+
 struct HitMapInfo
 {
 	static HitMapInfo NoTreat;
@@ -33,6 +35,8 @@ struct HitMapInfo
 		turn = 255;
 		hero = HeroPtr();
 	}
+
+	double value() const;
 };
 
 struct HitMapNode
@@ -40,6 +44,8 @@ struct HitMapNode
 	HitMapInfo maximumDanger;
 	HitMapInfo fastestDanger;
 
+	const CGTownInstance * closestTown = nullptr;
+
 	HitMapNode() = default;
 
 	void reset()
@@ -54,18 +60,25 @@ class DangerHitMapAnalyzer
 private:
 	boost::multi_array<HitMapNode, 3> hitMap;
 	std::map<const CGHeroInstance *, std::set<const CGObjectInstance *>> enemyHeroAccessibleObjects;
-	bool upToDate;
+	bool hitMapUpToDate = false;
+	bool tileOwnersUpToDate = false;
 	const Nullkiller * ai;
+	std::map<ObjectInstanceID, std::vector<HitMapInfo>> townTreats;
 
 public:
 	DangerHitMapAnalyzer(const Nullkiller * ai) :ai(ai) {}
 
 	void updateHitMap();
+	void calculateTileOwners();
 	uint64_t enemyCanKillOurHeroesAlongThePath(const AIPath & path) const;
 	const HitMapNode & getObjectTreat(const CGObjectInstance * obj) const;
 	const HitMapNode & getTileTreat(const int3 & tile) const;
 	const std::set<const CGObjectInstance *> & getOneTurnAccessibleObjects(const CGHeroInstance * enemy) const;
 	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;
 };
 
 }

+ 40 - 12
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -13,7 +13,6 @@
 #include "../../../lib/mapObjects/MapObjects.h"
 #include "../../../lib/CHeroHandler.h"
 #include "../../../lib/GameSettings.h"
-#include "../../../lib/CGameState.h"
 
 namespace NKAI
 {
@@ -72,10 +71,10 @@ float HeroManager::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance *
 
 float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
 {
-	auto heroSpecial = Selector::source(Bonus::HERO_SPECIAL, hero->type->getIndex());
-	auto secondarySkillBonus = Selector::targetSourceType()(Bonus::SECONDARY_SKILL);
+	auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, hero->type->getIndex());
+	auto secondarySkillBonus = Selector::targetSourceType()(BonusSource::SECONDARY_SKILL);
 	auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus));
-	auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(Bonus::SECONDARY_SKILL));
+	auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(BonusSource::SECONDARY_SKILL));
 	float specialityScore = 0.0f;
 
 	for(auto bonus : *secondarySkillBonuses)
@@ -126,6 +125,7 @@ void HeroManager::update()
 	}
 
 	std::sort(myHeroes.begin(), myHeroes.end(), scoreSort);
+	heroRoles.clear();
 
 	for(auto hero : myHeroes)
 	{
@@ -181,6 +181,15 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
 	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
 {
 	if(!town)
@@ -189,16 +198,10 @@ bool HeroManager::canRecruitHero(const CGTownInstance * town) const
 	if(!town || !townHasFreeTavern(town))
 		return false;
 
-	if(cb->getResourceAmount(Res::GOLD) < GameConstants::HERO_GOLD_COST)
+	if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST)
 		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;
 
 	if(!cb->getAvailableHeroes(town).size())
@@ -226,6 +229,31 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const
 	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)
 	: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 evaluateHero(const CGHeroInstance * hero) 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 * findWeakHeroToDismiss(uint64_t armyLimit) const = 0;
 };
 
 class DLL_EXPORT ISecondarySkillRule
@@ -71,7 +73,9 @@ public:
 	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
 	float evaluateHero(const CGHeroInstance * hero) const override;
 	bool canRecruitHero(const CGTownInstance * t = nullptr) const override;
+	bool heroCapReached() const override;
 	const CGHeroInstance * findHeroWithGrail() const override;
+	const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override;
 
 private:
 	float evaluateFightingStrength(const CGHeroInstance * hero) const;

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

@@ -12,7 +12,6 @@
 #include "../Goals/ExecuteHeroChain.h"
 #include "../AIGateway.h"
 #include "../Engine/Nullkiller.h"
-#include "lib/mapping/CMap.h" //for victory conditions
 
 namespace NKAI
 {
@@ -114,7 +113,7 @@ const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) cons
 
 			if(blockerObject)
 			{
-				blockers.push_back(blockerObject);
+				blockers.insert(blockers.begin(), blockerObject);
 			}
 		}
 
@@ -228,7 +227,12 @@ void ObjectClusterizer::clusterize()
 			auto obj = objs[i];
 
 			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
 			logAi->trace("Check object %s%s.", obj->getObjectName(), obj->visitablePos().toString());

+ 5 - 11
AI/Nullkiller/Analyzers/ObjectClusterizer.h

@@ -22,7 +22,7 @@ struct ClusterObjectInfo
 	uint8_t turn;
 };
 
-typedef tbb::concurrent_hash_map<const CGObjectInstance *, ClusterObjectInfo> ClusterObjects;
+using ClusterObjects = tbb::concurrent_hash_map<const CGObjectInstance *, ClusterObjectInfo>;
 
 struct ObjectCluster
 {
@@ -36,11 +36,8 @@ public:
 	}
 
 	void addObject(const CGObjectInstance * object, const AIPath & path, float priority);
-	
-	ObjectCluster(const CGObjectInstance * blocker)
-		:objects(), blocker(blocker)
-	{
-	}
+
+	ObjectCluster(const CGObjectInstance * blocker): blocker(blocker) {}
 
 	ObjectCluster() : ObjectCluster(nullptr)
 	{
@@ -50,7 +47,7 @@ public:
 	const CGObjectInstance * calculateCenter() const;
 };
 
-typedef tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>> ClusterMap;
+using ClusterMap = tbb::concurrent_hash_map<const CGObjectInstance *, std::shared_ptr<ObjectCluster>>;
 
 class ObjectClusterizer
 {
@@ -67,10 +64,7 @@ public:
 	std::vector<std::shared_ptr<ObjectCluster>> getLockedClusters() const;
 	const CGObjectInstance * getBlocker(const AIPath & path) const;
 
-	ObjectClusterizer(const Nullkiller * ai)
-		:nearObjects(), farObjects(), blockedObjects(), ai(ai)
-	{
-	}
+	ObjectClusterizer(const Nullkiller * ai): ai(ai) {}
 
 private:
 	bool shouldVisitObject(const CGObjectInstance * obj) const;

+ 1 - 3
AI/Nullkiller/Behaviors/BuildingBehavior.cpp

@@ -15,8 +15,6 @@
 #include "../Goals/Composition.h"
 #include "../Goals/BuildThis.h"
 #include "../Goals/SaveResources.h"
-#include "lib/mapping/CMap.h" //for victory conditions
-#include "lib/CPathfinder.h"
 #include "../Engine/Nullkiller.h"
 
 namespace NKAI
@@ -58,7 +56,7 @@ Goals::TGoalVec BuildingBehavior::decompose() const
 	{
 		for(auto & buildingInfo : developmentInfo.toBuild)
 		{
-			if(goldPreasure < MAX_GOLD_PEASURE || buildingInfo.dailyIncome[Res::GOLD] > 0)
+			if(goldPreasure < MAX_GOLD_PEASURE || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
 			{
 				if(buildingInfo.notEnoughRes)
 				{

+ 0 - 2
AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp

@@ -13,8 +13,6 @@
 #include "../AIUtility.h"
 #include "../Goals/BuyArmy.h"
 #include "../Engine/Nullkiller.h"
-#include "lib/mapping/CMap.h" //for victory conditions
-#include "lib/CPathfinder.h"
 
 namespace NKAI
 {

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

@@ -56,7 +56,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 
 	tasks.reserve(paths.size());
 
-	const AIPath * closestWay = nullptr;
+	std::unordered_map<HeroRole, const AIPath *> closestWaysByRole;
 	std::vector<ExecuteHeroChain *> waysToVisitObj;
 
 	for(auto & path : paths)
@@ -128,8 +128,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(const std::vector<AIPath>
 
 			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;
 			}
@@ -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
 				= closestWay->movementCost() / way->getPath().movementCost();
@@ -209,7 +213,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 	{
 		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());
 	}
 

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

@@ -21,8 +21,6 @@
 #include "../Goals/CaptureObject.h"
 #include "../Markers/DefendTown.h"
 #include "../Goals/ExchangeSwapTownHeroes.h"
-#include "lib/mapping/CMap.h" //for victory conditions
-#include "lib/CPathfinder.h"
 
 namespace NKAI
 {
@@ -51,37 +49,119 @@ Goals::TGoalVec DefenceBehavior::decompose() const
 	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);
 
-	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(
-					"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(
 			"Hero %s in garrison of town %s is suposed to defend the town",
 			town->garrisonHero->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;
 	}
 
@@ -111,103 +191,15 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			std::to_string(treat.turn),
 			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;
-
-		if(!town->visitingHero
-			&& town->hasBuilt(BuildingID::TAVERN)
-			&& cb->getResourceAmount(Res::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->movement
-								|| 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())
 		{
 			logAi->trace("No ways to defend town %s", town->getNameTranslated());
@@ -231,6 +223,22 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				path.movementCost(),
 				path.toString());
 #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 NKAI_TRACE_LEVEL >= 1
@@ -277,9 +285,11 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				tasks.push_back(
 					Goals::sptr(Composition()
 						.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;
 			}
@@ -315,15 +325,58 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 					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
-			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
-			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();
 			if(firstBlockedAction)
@@ -352,4 +405,70 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	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
 {
+
+struct HitMapInfo;
+
 namespace Goals
 {
+
 	class DefenceBehavior : public CGoal<DefenceBehavior>
 	{
 	public:
@@ -35,6 +39,7 @@ namespace Goals
 
 	private:
 		void evaluateDefence(Goals::TGoalVec & tasks, const CGTownInstance * town) const;
+		void evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & treat, const CGTownInstance * town) const;
 	};
 }
 

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

@@ -12,12 +12,13 @@
 #include "../Engine/Nullkiller.h"
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/Composition.h"
+#include "../Goals/RecruitHero.h"
 #include "../Markers/HeroExchange.h"
 #include "../Markers/ArmyUpgrade.h"
 #include "GatherArmyBehavior.h"
+#include "CaptureObjectsBehavior.h"
 #include "../AIUtility.h"
-#include "lib/mapping/CMap.h" //for victory conditions
-#include "lib/CPathfinder.h"
+#include "../Goals/ExchangeSwapTownHeroes.h"
 
 namespace NKAI
 {
@@ -80,20 +81,27 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 	for(const AIPath & path : paths)
 	{
 #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
 		
-		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 NKAI_TRACE_LEVEL >= 1
-			logAi->trace("Skipping garnisoned hero %s, %s", hero->getObjectName(), pos.toString());
+			garrisoned = true;
 #endif
-			continue;
 		}
 
-		if(ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
+		if(path.turn() > 0 && ai->nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
 		{
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
@@ -111,10 +119,11 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 
 		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
-		if(armyValue < 0.1f && armyValue < 20000)
+		if((armyRatio < 0.1f && armyValue < 20000) || armyValue < 500)
 		{
 #if NKAI_TRACE_LEVEL >= 2
 			logAi->trace("Army value is too small.");
@@ -174,7 +183,21 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const CGHeroInstance * her
 			exchangePath.closestWayRatio = 1;
 
 			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();
 
@@ -214,18 +237,42 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 #endif
 	
 	auto paths = ai->nullkiller->pathfinder->getPathInfo(pos);
+	auto goals = CaptureObjectsBehavior::getVisitGoals(paths);
+
 	std::vector<std::shared_ptr<ExecuteHeroChain>> waysToVisitObj;
 
 #if NKAI_TRACE_LEVEL >= 1
 	logAi->trace("Found %d paths", paths.size());
 #endif
 
+	bool hasMainAround = false;
+
 	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
-		logAi->trace("Path found %s", path.toString());
+		logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
 #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
 			logAi->trace("Ignore path. Town has visiting hero.");
@@ -263,18 +310,58 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 
 		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.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();
 
-		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
 			logAi->trace("Ignore path. Army value is too small (%f)", armyValue);
@@ -299,11 +386,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const CGTownInstance * upgrader)
 
 		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)));
 		}
 	}
 

+ 22 - 3
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

@@ -13,8 +13,6 @@
 #include "../AIUtility.h"
 #include "../Goals/RecruitHero.h"
 #include "../Goals/ExecuteHeroChain.h"
-#include "lib/mapping/CMap.h" //for victory conditions
-#include "lib/CPathfinder.h"
 
 namespace NKAI
 {
@@ -68,8 +66,29 @@ 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
-				|| (ai->nullkiller->getFreeResources()[Res::GOLD] > 10000
+				|| (ai->nullkiller->getFreeResources()[EGameResID::GOLD] > 10000
 					&& ai->nullkiller->buildAnalyzer->getGoldPreasure() < MAX_GOLD_PEASURE))
 			{
 				tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3)));

+ 2 - 4
AI/Nullkiller/Behaviors/StartupBehavior.cpp

@@ -15,9 +15,7 @@
 #include "../Goals/RecruitHero.h"
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/ExchangeSwapTownHeroes.h"
-#include "lib/mapping/CMap.h" //for victory conditions
 #include "lib/mapObjects/MapObjects.h" //for victory conditions
-#include "lib/CPathfinder.h"
 #include "../Engine/Nullkiller.h"
 
 namespace NKAI
@@ -76,7 +74,7 @@ bool needToRecruitHero(const CGTownInstance * startupTown)
 
 	for(auto obj : ai->nullkiller->objectClusterizer->getNearbyObjects())
 	{
-		if((obj->ID == Obj::RESOURCE && obj->subID == Res::GOLD)
+		if((obj->ID == Obj::RESOURCE && obj->subID == GameResID(EGameResID::GOLD))
 			|| obj->ID == Obj::TREASURE_CHEST
 			|| obj->ID == Obj::CAMPFIRE
 			|| obj->ID == Obj::WATER_WHEEL)
@@ -208,7 +206,7 @@ Goals::TGoalVec StartupBehavior::decompose() const
 		for(const CGTownInstance * town : towns)
 		{
 			if(town->garrisonHero
-				&& town->garrisonHero->movement
+				&& town->garrisonHero->movementPointsRemaining()
 				&& !town->visitingHero
 				&& ai->nullkiller->getHeroLockedReason(town->garrisonHero) != HeroLockedReason::DEFENCE)
 			{

+ 2 - 0
AI/Nullkiller/CMakeLists.txt

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

+ 2 - 2
AI/Nullkiller/Engine/AIMemory.cpp

@@ -72,10 +72,10 @@ void AIMemory::markObjectVisited(const CGObjectInstance * obj)
 	// TODO: maybe this logic belongs to CaptureObjects::shouldVisit
 	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj))
 	{
-		if (rewardable->getVisitMode() == CRewardableObject::VISIT_HERO) //we may want to visit it with another hero
+		if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_HERO) //we may want to visit it with another hero
 			return;
 
-		if (rewardable->getVisitMode() == CRewardableObject::VISIT_BONUS) //or another time
+		if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_BONUS) //or another time
 			return;
 	}
 

+ 1 - 1
AI/Nullkiller/Engine/DeepDecomposer.h

@@ -22,7 +22,7 @@ struct GoalHash
 	}
 };
 
-typedef std::unordered_map<Goals::TSubgoal, Goals::TGoalVec, GoalHash> TGoalHashSet;
+using TGoalHashSet = std::unordered_map<Goals::TSubgoal, Goals::TGoalVec, GoalHash>;
 
 class DeepDecomposer
 {

+ 10 - 10
AI/Nullkiller/Engine/FuzzyEngines.cpp

@@ -53,25 +53,25 @@ armyStructure evaluateArmyStructure(const CArmedInstance * army)
 	double shootersStrength = 0;
 	ui32 maxSpeed = 0;
 
-	static const CSelector selectorSHOOTER = Selector::type()(Bonus::SHOOTER);
-	static const std::string keySHOOTER = "type_"+std::to_string((int32_t)Bonus::SHOOTER);
+	static const CSelector selectorSHOOTER = Selector::type()(BonusType::SHOOTER);
+	static const std::string keySHOOTER = "type_"+std::to_string((int32_t)BonusType::SHOOTER);
 
-	static const CSelector selectorFLYING = Selector::type()(Bonus::FLYING);
-	static const std::string keyFLYING = "type_"+std::to_string((int32_t)Bonus::FLYING);
+	static const CSelector selectorFLYING = Selector::type()(BonusType::FLYING);
+	static const std::string keyFLYING = "type_"+std::to_string((int32_t)BonusType::FLYING);
 
-	static const CSelector selectorSTACKS_SPEED = Selector::type()(Bonus::STACKS_SPEED);
-	static const std::string keySTACKS_SPEED = "type_"+std::to_string((int32_t)Bonus::STACKS_SPEED);
+	static const CSelector selectorSTACKS_SPEED = Selector::type()(BonusType::STACKS_SPEED);
+	static const std::string keySTACKS_SPEED = "type_"+std::to_string((int32_t)BonusType::STACKS_SPEED);
 
 	for(auto s : army->Slots())
 	{
 		bool walker = true;
-		const CCreature * creature = s.second->type;
-		if(creature->hasBonus(selectorSHOOTER, keySHOOTER))
+		auto bearer = s.second->getType()->getBonusBearer();
+		if(bearer->hasBonus(selectorSHOOTER, keySHOOTER))
 		{
 			shootersStrength += s.second->getPower();
 			walker = false;
 		}
-		if(creature->hasBonus(selectorFLYING, keyFLYING))
+		if(bearer->hasBonus(selectorFLYING, keyFLYING))
 		{
 			flyersStrength += s.second->getPower();
 			walker = false;
@@ -79,7 +79,7 @@ armyStructure evaluateArmyStructure(const CArmedInstance * army)
 		if(walker)
 			walkersStrength += s.second->getPower();
 
-		vstd::amax(maxSpeed, creature->valOfBonuses(selectorSTACKS_SPEED, keySTACKS_SPEED));
+		vstd::amax(maxSpeed, bearer->valOfBonuses(selectorSTACKS_SPEED, keySTACKS_SPEED));
 	}
 	armyStructure as;
 	as.walkers = static_cast<float>(walkersStrength / totalStrength);

+ 9 - 8
AI/Nullkiller/Engine/FuzzyHelper.cpp

@@ -10,10 +10,13 @@
 #include "../StdInc.h"
 #include "FuzzyHelper.h"
 
-#include "../../../lib/mapObjects/CommonConstructors.h"
 #include "../Goals/Goals.h"
 #include "Nullkiller.h"
 
+#include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
+#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
+#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
+
 namespace NKAI
 {
 
@@ -136,7 +139,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 	{
 		if(!vstd::contains(ai->memory->alreadyVisited, obj))
 			return 0;
-		FALLTHROUGH;
+		[[fallthrough]];
 	}
 	case Obj::MONSTER:
 	case Obj::HERO:
@@ -147,17 +150,15 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 	case Obj::MINE:
 	case Obj::ABANDONED_MINE:
 	case Obj::PANDORAS_BOX:
-	{
-		const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
-		return a->getArmyStrength();
-	}
 	case Obj::CRYPT: //crypt
 	case Obj::CREATURE_BANK: //crebank
 	case Obj::DRAGON_UTOPIA:
 	case Obj::SHIPWRECK: //shipwreck
 	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:
 	{
 		if(obj->subID == 0)

+ 1 - 1
AI/Nullkiller/Engine/FuzzyHelper.h

@@ -28,7 +28,7 @@ private:
 	TacticalAdvantageEngine tacticalAdvantageEngine;
 
 public:
-	FuzzyHelper(const Nullkiller * ai) : ai(ai), tacticalAdvantageEngine() {}
+	FuzzyHelper(const Nullkiller * ai): ai(ai) {}
 
 	ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class?
 

+ 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));
 	heroManager.reset(new HeroManager(cb.get(), this));
 	decomposer.reset(new DeepDecomposer());
+	armyFormation.reset(new ArmyFormation(cb, this));
 }
 
 Goals::TTask Nullkiller::choseBestTask(Goals::TTaskVec & tasks) const
@@ -117,7 +118,7 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi
 void Nullkiller::resetAiState()
 {
 	lockedResources = TResources();
-	scanDepth = ScanDepth::FULL;
+	scanDepth = ScanDepth::MAIN_FULL;
 	playerID = ai->playerID;
 	lockedHeroes.clear();
 	dangerHitMap->reset();
@@ -133,10 +134,14 @@ void Nullkiller::updateAiState(int pass, bool fast)
 	activeHero = nullptr;
 	setTargetObject(-1);
 
+	decomposer->reset();
+	buildAnalyzer->update();
+
 	if(!fast)
 	{
 		memory->removeInvisibleObjects(cb.get());
 
+		dangerHitMap->calculateTileOwners();
 		dangerHitMap->updateHitMap();
 
 		boost::this_thread::interruption_point();
@@ -156,11 +161,15 @@ void Nullkiller::updateAiState(int pass, bool fast)
 
 		PathfinderSettings cfg;
 		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();
@@ -173,8 +182,6 @@ void Nullkiller::updateAiState(int pass, bool fast)
 	}
 
 	armyManager->update();
-	buildAnalyzer->update();
-	decomposer->reset();
 
 	logAi->debug("AI state updated in %ld", timeElapsed(start));
 }
@@ -222,7 +229,7 @@ void Nullkiller::makeTurn()
 	boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
 
 	const int MAX_DEPTH = 10;
-	const float FAST_TASK_MINIMAL_PRIORITY = 0.7;
+	const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
 
 	resetAiState();
 
@@ -231,8 +238,8 @@ void Nullkiller::makeTurn()
 		updateAiState(i);
 
 		Goals::TTask bestTask = taskptr(Goals::Invalid());
-		
-		do
+
+		for(;i <= MAXPASS; i++)
 		{
 			Goals::TTaskVec fastTasks = {
 				choseBestTask(sptr(BuyArmyBehavior()), 1),
@@ -246,7 +253,11 @@ void Nullkiller::makeTurn()
 				executeTask(bestTask);
 				updateAiState(i, true);
 			}
-		} while(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY);
+			else
+			{
+				break;
+			}
+		}
 
 		Goals::TTaskVec bestTasks = {
 			bestTask,
@@ -265,7 +276,6 @@ void Nullkiller::makeTurn()
 		bestTask = choseBestTask(bestTasks);
 
 		HeroPtr hero = bestTask->getHero();
-
 		HeroRole heroRole = HeroRole::MAIN;
 
 		if(hero.validAndSet())
@@ -274,20 +284,39 @@ void Nullkiller::makeTurn()
 		if(heroRole != HeroRole::MAIN || bestTask->getHeroExchangeCount() <= 1)
 			useHeroChain = false;
 
+		// TODO: better to check turn distance here instead of priority
 		if((heroRole != HeroRole::MAIN || bestTask->priority < SMALL_SCAN_MIN_PRIORITY)
-			&& scanDepth == ScanDepth::FULL)
+			&& scanDepth == ScanDepth::MAIN_FULL)
 		{
 			useHeroChain = false;
 			scanDepth = ScanDepth::SMALL;
 
 			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->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());
 
 			return;

+ 7 - 3
AI/Nullkiller/Engine/Nullkiller.h

@@ -18,6 +18,7 @@
 #include "../Analyzers/ArmyManager.h"
 #include "../Analyzers/HeroManager.h"
 #include "../Analyzers/ObjectClusterizer.h"
+#include "../Helpers/ArmyFormation.h"
 
 namespace NKAI
 {
@@ -39,9 +40,11 @@ enum class HeroLockedReason
 
 enum class ScanDepth
 {
-	FULL = 0,
+	MAIN_FULL = 0,
 
-	SMALL = 1
+	SMALL = 1,
+
+	ALL_FULL = 2
 };
 
 class Nullkiller
@@ -67,6 +70,7 @@ public:
 	std::unique_ptr<AIMemory> memory;
 	std::unique_ptr<FuzzyHelper> dangerEvaluator;
 	std::unique_ptr<DeepDecomposer> decomposer;
+	std::unique_ptr<ArmyFormation> armyFormation;
 	PlayerColor playerID;
 	std::shared_ptr<CCallback> cb;
 
@@ -85,7 +89,7 @@ public:
 	void unlockHero(const CGHeroInstance * hero) { lockedHeroes.erase(hero); }
 	bool arePathHeroesLocked(const AIPath & path) const;
 	TResources getFreeResources() const;
-	int32_t getFreeGold() const { return getFreeResources()[Res::GOLD]; }
+	int32_t getFreeGold() const { return getFreeResources()[EGameResID::GOLD]; }
 	void lockResources(const TResources & res);
 	const TResources & getLockedResources() const { return lockedResources; }
 	ScanDepth getScanDepth() const { return scanDepth; }

+ 243 - 109
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -11,11 +11,11 @@
 #include <limits>
 
 #include "Nullkiller.h"
+#include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
+#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
+#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
 #include "../../../lib/mapObjects/MapObjects.h"
-#include "../../../lib/mapObjects/CommonConstructors.h"
 #include "../../../lib/CCreatureHandler.h"
-#include "../../../lib/CPathfinder.h"
-#include "../../../lib/CGameStateFwd.h"
 #include "../../../lib/VCMI_Lib.h"
 #include "../../../lib/StartInfo.h"
 #include "../../../CCallback.h"
@@ -23,6 +23,7 @@
 #include "../Goals/ExecuteHeroChain.h"
 #include "../Goals/BuildThis.h"
 #include "../Goals/ExchangeSwapTownHeroes.h"
+#include "../Goals/DismissHero.h"
 #include "../Markers/UnlockCluster.h"
 #include "../Markers/HeroExchange.h"
 #include "../Markers/ArmyUpgrade.h"
@@ -33,6 +34,7 @@ namespace NKAI
 
 #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
+const float MIN_CRITICAL_VALUE = 2.0f;
 
 EvaluationContext::EvaluationContext(const Nullkiller * ai)
 	: movementCost(0.0),
@@ -49,10 +51,16 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
 	turn(0),
 	strategicalValue(0),
 	evaluator(ai),
-	enemyHeroDangerRatio(0)
+	enemyHeroDangerRatio(0),
+	armyGrowth(0)
 {
 }
 
+void EvaluationContext::addNonCriticalStrategicalValue(float value)
+{
+	vstd::amax(strategicalValue, std::min(value, MIN_CRITICAL_VALUE));
+}
+
 PriorityEvaluator::~PriorityEvaluator()
 {
 	delete engine;
@@ -64,6 +72,7 @@ void PriorityEvaluator::initVisitTile()
 	std::string str = std::string((char *)file.first.get(), file.second);
 	engine = fl::FllImporter().fromString(str);
 	armyLossPersentageVariable = engine->getInputVariable("armyLoss");
+	armyGrowthVariable = engine->getInputVariable("armyGrowth");
 	heroRoleVariable = engine->getInputVariable("heroRole");
 	dangerVariable = engine->getInputVariable("danger");
 	turnVariable = engine->getInputVariable("turn");
@@ -99,7 +108,8 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons
 	auto town = cb->getTown(target->id);
 	auto fortLevel = town->fortLevel();
 
-	if(town->hasCapitol()) return booster * 2000;
+	if(town->hasCapitol())
+		return booster * 2000;
 
 	// probably well developed town will have city hall
 	if(fortLevel == CGTownInstance::CASTLE) return booster * 750;
@@ -148,22 +158,23 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero
 	for (auto c : creatures)
 	{
 		//Only if hero has slot for this creature in the army
-		if (hero->getSlotFor(c.data.type).validSlot())
+		auto ccre = dynamic_cast<const CCreature*>(c.data.type);
+		if (hero->getSlotFor(ccre).validSlot())
 		{
-			result += (c.data.type->AIValue * 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
-			result += (c.data.type->AIValue * c.data.count - weakestStackPower) * c.chance;
-		}
+			result += (c.data.type->getAIValue() * c.data.count - weakestStackPower) * c.chance;
+		}*/
 	}
 	result /= 100; //divide by total chance
 
 	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);
 	uint64_t score = 0;
@@ -173,11 +184,32 @@ uint64_t getDwellingScore(CCallback * cb, const CGObjectInstance * target, bool
 		if(creLevel.first && creLevel.second.size())
 		{
 			auto creature = creLevel.second.back().toCreature();
-			auto creaturesAreFree = creature->level == 1;
-			if(!creaturesAreFree && checkGold && !cb->getResourceAmount().canAfford(creature->cost * creLevel.first))
+			auto creaturesAreFree = creature->getLevel() == 1;
+			if(!creaturesAreFree && checkGold && !cb->getResourceAmount().canAfford(creature->getFullRecruitCost() * creLevel.first))
 				continue;
 
-			score += creature->AIValue * creLevel.first;
+			score += creature->getAIValue() * creLevel.first;
+		}
+	}
+
+	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();
 		}
 	}
 
@@ -194,9 +226,9 @@ int getDwellingArmyCost(const CGObjectInstance * target)
 		if(creLevel.first && creLevel.second.size())
 		{
 			auto creature = creLevel.second.back().toCreature();
-			auto creaturesAreFree = creature->level == 1;
+			auto creaturesAreFree = creature->getLevel() == 1;
 			if(!creaturesAreFree)
-				cost += creature->cost[Res::GOLD] * creLevel.first;
+				cost += creature->getRecruitCost(EGameResID::GOLD) * creLevel.first;
 		}
 	}
 
@@ -209,14 +241,14 @@ uint64_t evaluateArtifactArmyValue(CArtifactInstance * art)
 		return 1500;
 
 	auto statsValue =
-		10 * art->valOfBonuses(Bonus::MOVEMENT, 1)
-		+ 1200 * art->valOfBonuses(Bonus::STACKS_SPEED)
-		+ 700 * art->valOfBonuses(Bonus::MORALE)
-		+ 700 * art->getAttack(false)
-		+ 700 * art->getDefense(false)
-		+ 700 * art->valOfBonuses(Bonus::PRIMARY_SKILL, PrimarySkill::KNOWLEDGE)
-		+ 700 * art->valOfBonuses(Bonus::PRIMARY_SKILL, PrimarySkill::SPELL_POWER)
-		+ 500 * art->valOfBonuses(Bonus::LUCK);
+		10 * art->valOfBonuses(BonusType::MOVEMENT, 1)
+		+ 1200 * art->valOfBonuses(BonusType::STACKS_SPEED)
+		+ 700 * art->valOfBonuses(BonusType::MORALE)
+		+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK)
+		+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE)
+		+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::KNOWLEDGE)
+		+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::SPELL_POWER)
+		+ 500 * art->valOfBonuses(BonusType::LUCK);
 
 	auto classValue = 0;
 
@@ -246,23 +278,13 @@ uint64_t RewardEvaluator::getArmyReward(
 {
 	const float enemyArmyEliminationRewardRatio = 0.5f;
 
+	auto relations = ai->cb->getPlayerRelations(target->tempOwner, ai->playerID);
+
 	if(!target)
 		return 0;
 
 	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:
 		return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue;
 	case Obj::CREATURE_BANK:
@@ -271,7 +293,7 @@ uint64_t RewardEvaluator::getArmyReward(
 	case Obj::CREATURE_GENERATOR2:
 	case Obj::CREATURE_GENERATOR3:
 	case Obj::CREATURE_GENERATOR4:
-		return getDwellingScore(ai->cb.get(), target, checkGold);
+		return getDwellingArmyValue(ai->cb.get(), target, checkGold);
 	case Obj::CRYPT:
 	case Obj::SHIPWRECK:
 	case Obj::SHIPWRECK_SURVIVOR:
@@ -282,7 +304,7 @@ uint64_t RewardEvaluator::getArmyReward(
 	case Obj::DRAGON_UTOPIA:
 		return 10000;
 	case Obj::HERO:
-		return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
+		return  relations == PlayerRelations::ENEMIES
 			? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength()
 			: 0;
 	case Obj::PANDORAS_BOX:
@@ -292,20 +314,65 @@ 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
 {
 	if(!target)
 		return 0;
+	
+	if(auto * m = dynamic_cast<const IMarket *>(target))
+	{
+		if(m->allowsTrade(EMarketMode::RESOURCE_SKILL))
+			return 2000;
+	}
 
 	switch(target->ID)
 	{
 	case Obj::HILL_FORT:
-		return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeCost[Res::GOLD];
+		return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeCost[EGameResID::GOLD];
 	case Obj::SCHOOL_OF_MAGIC:
 	case Obj::SCHOOL_OF_WAR:
 		return 1000;
-	case Obj::UNIVERSITY:
-		return 2000;
 	case Obj::CREATURE_GENERATOR1:
 	case Obj::CREATURE_GENERATOR2:
 	case Obj::CREATURE_GENERATOR3:
@@ -333,7 +400,7 @@ float RewardEvaluator::getEnemyHeroStrategicalValue(const CGHeroInstance * enemy
 	  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
 	*/
-	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
@@ -361,10 +428,26 @@ float RewardEvaluator::getTotalResourceRequirementStrength(int resType) const
 		return 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
@@ -375,12 +458,12 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 	switch(target->ID)
 	{
 	case Obj::MINE:
-		return target->subID == Res::GOLD 
+		return target->subID == GameResID(EGameResID::GOLD)
 			? 0.5f 
 			: 0.4f * getTotalResourceRequirementStrength(target->subID) + 0.1f * getResourceRequirementStrength(target->subID);
 
 	case Obj::RESOURCE:
-		return target->subID == Res::GOLD
+		return target->subID == GameResID(EGameResID::GOLD)
 			? 0
 			: 0.2f * getTotalResourceRequirementStrength(target->subID) + 0.4f * getResourceRequirementStrength(target->subID);
 
@@ -391,7 +474,7 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 		for (TResources::nziterator it (resourceReward); it.valid(); it++)
 		{
 			//Evaluate resources used for construction. Gold is evaluated separately.
-			if (it->resType != Res::GOLD)
+			if (it->resType != EGameResID::GOLD)
 			{
 				sum += 0.1f * getResourceRequirementStrength(it->resType);
 			}
@@ -402,18 +485,28 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
 	case Obj::TOWN:
 	{
 		if(ai->buildAnalyzer->getDevelopmentInfo().empty())
-			return 1;
+			return 10.0f;
 
 		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 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)
-			return booster * (town->hasFort() ? 0.6 : 0.4);
+			return booster * (town->hasFort() ? 1.0 : 0.8);
 		else
-			return booster * (fortLevel == CGTownInstance::CASTLE ? 0.9 : 0.8);
+			return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
 	}
 
 	case Obj::HERO:
@@ -458,15 +551,18 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
 	case Obj::GARDEN_OF_REVELATION:
 	case Obj::MARLETTO_TOWER:
 	case Obj::MERCENARY_CAMP:
-	case Obj::SHRINE_OF_MAGIC_GESTURE:
-	case Obj::SHRINE_OF_MAGIC_INCANTATION:
 	case Obj::TREE_OF_KNOWLEDGE:
 		return 1;
 	case Obj::LEARNING_STONE:
 		return 1.0f / std::sqrt(hero->level);
 	case Obj::ARENA:
-	case Obj::SHRINE_OF_MAGIC_THOUGHT:
 		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:
 		return 8;
 	case Obj::WITCH_HUT:
@@ -502,22 +598,23 @@ int32_t getArmyCost(const CArmedInstance * army)
 
 	for(auto stack : army->Slots())
 	{
-		value += stack.second->getCreatureID().toCreature()->cost[Res::GOLD] * stack.second->count;
+		value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count;
 	}
 
 	return value;
 }
 
-/// Gets aproximated reward in gold. Daily income is multiplied by 5
 int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const
 {
 	if(!target)
 		return 0;
 
+	auto relations = ai->cb->getPlayerRelations(target->tempOwner, hero->tempOwner);
+
 	const int dailyIncomeMultiplier = 5;
 	const float enemyArmyEliminationGoldRewardRatio = 0.2f;
 	const int32_t heroEliminationBonus = GameConstants::HERO_GOLD_COST / 2;
-	auto isGold = target->subID == Res::GOLD; // TODO: other resorces could be sold but need to evaluate market power
+	auto isGold = target->subID == GameResID(EGameResID::GOLD); // TODO: other resorces could be sold but need to evaluate market power
 
 	switch(target->ID)
 	{
@@ -540,7 +637,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 	case Obj::WAGON:
 		return 100;
 	case Obj::CREATURE_BANK:
-		return getCreatureBankResources(target, hero)[Res::GOLD];
+		return getCreatureBankResources(target, hero)[EGameResID::GOLD];
 	case Obj::CRYPT:
 	case Obj::DERELICT_SHIP:
 		return 3000;
@@ -554,7 +651,7 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
 		//Objectively saves us 2500 to hire hero
 		return GameConstants::HERO_GOLD_COST;
 	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))
 			: 0;
 	default:
@@ -574,7 +671,8 @@ public:
 
 		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());
 	}
 };
 
@@ -591,7 +689,7 @@ public:
 		uint64_t upgradeValue = armyUpgrade.getUpgradeValue();
 
 		evaluationContext.armyReward += upgradeValue;
-		evaluationContext.strategicalValue += upgradeValue / (float)armyUpgrade.hero->getArmyStrength();
+		evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
 	}
 };
 
@@ -616,23 +714,6 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
 
 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->AIValue * town->getGrowthInfo(creature->getLevel() - 1).totalGrowth();
-		}
-
-		return result;
-	}
-
 public:
 	virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
 	{
@@ -643,22 +724,34 @@ public:
 		const CGTownInstance * town = defendTown.town;
 		auto & treat = defendTown.getTreat();
 
-		auto armyIncome = townArmyIncome(town);
-		auto dailyIncome = town->dailyIncome()[Res::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;
 
 		if(treat.turn < defendTown.getTurn())
 			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.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);
 		addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
 	}
@@ -704,18 +797,22 @@ public:
 		auto army = path.heroArmy;
 
 		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.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);
 		}
 
 		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());
 		vstd::amax(evaluationContext.turn, path.turn());
 	}
@@ -755,7 +852,7 @@ public:
 			evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero) / boost;
 			evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / 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.movementCostByRole[role] += objInfo.second.movementCost / boost;
 			evaluationContext.movementCost += objInfo.second.movementCost / boost;
@@ -784,7 +881,7 @@ public:
 		if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
 		{
 			auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
-			auto mpLeft = garrisonHero->movement / (float)garrisonHero->maxMovePoints(true);
+			auto mpLeft = garrisonHero->movementPointsRemaining() / (float)garrisonHero->movementPointsLimit(true);
 
 			evaluationContext.movementCost += mpLeft;
 			evaluationContext.movementCostByRole[defenderRole] += mpLeft;
@@ -793,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
 {
 public:
@@ -804,50 +926,58 @@ public:
 		Goals::BuildThis & buildThis = dynamic_cast<Goals::BuildThis &>(*task);
 		auto & bi = buildThis.buildingInfo;
 		
-		evaluationContext.goldReward += 7 * bi.dailyIncome[Res::GOLD] / 2; // 7 day income but half we already have
+		evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have
 		evaluationContext.heroRole = HeroRole::MAIN;
 		evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
-		evaluationContext.goldCost += bi.buildCostWithPrerequisits[Res::GOLD];
+		evaluationContext.goldCost += bi.buildCostWithPrerequisits[EGameResID::GOLD];
+		evaluationContext.closestWayRatio = 1;
 
 		if(bi.creatureID != CreatureID::NONE)
 		{
-			evaluationContext.strategicalValue += buildThis.townInfo.armyStrength / 50000.0;
+			evaluationContext.addNonCriticalStrategicalValue(buildThis.townInfo.armyStrength / 50000.0);
 
 			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;
 			}
 			else
 			{
 				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;
 			}
 		}
 		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;
 		}
-		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();
 
-			evaluationContext.strategicalValue += evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount;
+			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.goldReward * goldPreasure / 3500.0f / bi.prerequisitesCount);
 		}
 
 		if(bi.notEnoughRes && bi.prerequisitesCount == 1)
 		{
-			evaluationContext.strategicalValue /= 2;
+			evaluationContext.strategicalValue /= 3;
+			evaluationContext.movementCostByRole[evaluationContext.heroRole] += 5;
+			evaluationContext.turn += 5;
 		}
 	}
 };
 
 uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const
 {
-	if(ai->buildAnalyzer->hasAnyBuilding(town->alignment, bi.id))
+	if(ai->buildAnalyzer->hasAnyBuilding(town->subID, bi.id))
 		return 0;
 
 	auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID);
@@ -867,6 +997,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai)
 	evaluationContextBuilders.push_back(std::make_shared<ArmyUpgradeEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>());
 	evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
+	evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai));
 }
 
 EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const
@@ -904,6 +1035,8 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 		+ (evaluationContext.armyReward > 0 ? 1 : 0)
 		+ (evaluationContext.skillReward > 0 ? 1 : 0)
 		+ (evaluationContext.strategicalValue > 0 ? 1 : 0);
+
+	float goldRewardPerTurn = evaluationContext.goldReward / std::log2f(2 + evaluationContext.movementCost * 10);
 	
 	double result = 0;
 
@@ -913,15 +1046,16 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 		heroRoleVariable->setValue(evaluationContext.heroRole);
 		mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
 		scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
-		goldRewardVariable->setValue(evaluationContext.goldReward);
+		goldRewardVariable->setValue(goldRewardPerTurn);
 		armyRewardVariable->setValue(evaluationContext.armyReward);
+		armyGrowthVariable->setValue(evaluationContext.armyGrowth);
 		skillRewardVariable->setValue(evaluationContext.skillReward);
 		dangerVariable->setValue(evaluationContext.danger);
 		rewardTypeVariable->setValue(rewardType);
 		closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
 		strategicalValueVariable->setValue(evaluationContext.strategicalValue);
 		goldPreasureVariable->setValue(ai->buildAnalyzer->getGoldPreasure());
-		goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[Res::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[Res::GOLD] + 1.0f));
+		goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
 		turnVariable->setValue(evaluationContext.turn);
 		fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
 
@@ -935,13 +1069,13 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 	}
 
 #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(),
 		evaluationContext.armyLossPersentage,
 		(int)evaluationContext.turn,
 		evaluationContext.movementCostByRole[HeroRole::MAIN],
 		evaluationContext.movementCostByRole[HeroRole::SCOUT],
-		evaluationContext.goldReward,
+		goldRewardPerTurn,
 		evaluationContext.goldCost,
 		evaluationContext.armyReward,
 		evaluationContext.danger,

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

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

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

@@ -10,8 +10,6 @@
 #include "StdInc.h"
 #include "AbstractGoal.h"
 #include "../AIGateway.h"
-#include "../../../lib/mapping/CMap.h" //for victory conditions
-#include "../../../lib/CPathfinder.h"
 #include "../../../lib/StringConstants.h"
 
 namespace NKAI
@@ -60,7 +58,7 @@ std::string AbstractGoal::toString() const //TODO: virtualize
 		desc = "GATHER TROOPS";
 		break;
 	case GET_ART_TYPE:
-		desc = "GET ARTIFACT OF TYPE " + VLC->arth->objects[aid]->getNameTranslated();
+		desc = "GET ARTIFACT OF TYPE " + VLC->artifacts()->getByIndex(aid)->getNameTranslated();
 		break;
 	case DIG_AT_TILE:
 		desc = "DIG AT TILE " + tile.toString();

+ 4 - 5
AI/Nullkiller/Goals/AbstractGoal.h

@@ -81,9 +81,9 @@ namespace Goals
 		bool operator<(const TSubgoal & rhs) const;
 	};
 
-	typedef std::shared_ptr<ITask> TTask;
-	typedef std::vector<TTask> TTaskVec;
-	typedef std::vector<TSubgoal> TGoalVec;
+	using TTask = std::shared_ptr<ITask>;
+	using TTaskVec = std::vector<TTask>;
+	using TGoalVec = std::vector<TSubgoal>;
 
 	//method chaining + clone pattern
 #define SETTER(type, field) AbstractGoal & set ## field(const type &rhs) {field = rhs; return *this;};
@@ -107,8 +107,7 @@ namespace Goals
 		const CGTownInstance *town; SETTER(CGTownInstance *, town)
 		int bid; SETTER(int, bid)
 
-		AbstractGoal(EGoals goal = EGoals::INVALID)
-			: goalType(goal), hero()
+		AbstractGoal(EGoals goal = EGoals::INVALID): goalType(goal)
 		{
 			isAbstract = false;
 			value = 0;

+ 0 - 2
AI/Nullkiller/Goals/AdventureSpellCast.cpp

@@ -10,8 +10,6 @@
 #include "StdInc.h"
 #include "AdventureSpellCast.h"
 #include "../AIGateway.h"
-#include "../../../lib/mapping/CMap.h" //for victory conditions
-#include "../../../lib/CPathfinder.h"
 
 namespace NKAI
 {

+ 7 - 7
AI/Nullkiller/Goals/Build.cpp

@@ -42,10 +42,10 @@ TGoalVec Build::getAllPossibleSubgoals()
 		auto expensiveBuilding = ai->ah->expensiveBuilding();
 
 		//handling for early town development to save money and focus on income
-		if(!t->hasBuilt(ai->ah->getMaxPossibleGoldBuilding(t)) && expensiveBuilding.is_initialized())
+		if(!t->hasBuilt(ai->ah->getMaxPossibleGoldBuilding(t)) && expensiveBuilding.has_value())
 		{
-			auto potentialBuilding = expensiveBuilding.get();
-			switch(expensiveBuilding.get().bid)
+			auto potentialBuilding = expensiveBuilding.value();
+			switch(expensiveBuilding.value().bid)
 			{
 			case BuildingID::TOWN_HALL:
 			case BuildingID::CITY_HALL:
@@ -61,15 +61,15 @@ TGoalVec Build::getAllPossibleSubgoals()
 			}
 		}
 
-		if(immediateBuilding.is_initialized())
+		if(immediateBuilding.has_value())
 		{
-			ret.push_back(sptr(BuildThis(immediateBuilding.get().bid, t).setpriority(2))); //prioritize buildings we can build quick
+			ret.push_back(sptr(BuildThis(immediateBuilding.value().bid, t).setpriority(2))); //prioritize buildings we can build quick
 		}
 		else //try build later
 		{
-			if(expensiveBuilding.is_initialized())
+			if(expensiveBuilding.has_value())
 			{
-				auto potentialBuilding = expensiveBuilding.get(); //gather resources for any we can't afford
+				auto potentialBuilding = expensiveBuilding.value(); //gather resources for any we can't afford
 				auto goal = ai->ah->whatToDo(potentialBuilding.price, sptr(BuildThis(potentialBuilding.bid, t).setpriority(0.5)));
 				ret.push_back(goal);
 			}

+ 4 - 7
AI/Nullkiller/Goals/BuildBoat.cpp

@@ -10,8 +10,6 @@
 #include "StdInc.h"
 #include "BuildBoat.h"
 #include "../AIGateway.h"
-#include "../../../lib/mapping/CMap.h" //for victory conditions
-#include "../../../lib/CPathfinder.h"
 #include "../Behaviors/CaptureObjectsBehavior.h"
 
 namespace NKAI
@@ -24,7 +22,7 @@ using namespace Goals;
 
 bool BuildBoat::operator==(const BuildBoat & other) const
 {
-	return shipyard->o->id == other.shipyard->o->id;
+	return shipyard == other.shipyard;
 }
 //
 //TSubgoal BuildBoat::decomposeSingle() const
@@ -55,7 +53,7 @@ void BuildBoat::accept(AIGateway * ai)
 		throw cannotFulfillGoalException("Can not afford boat");
 	}
 
-	if(cb->getPlayerRelations(ai->playerID, shipyard->o->tempOwner) == PlayerRelations::ENEMIES)
+	if(cb->getPlayerRelations(ai->playerID, shipyard->getObject()->getOwner()) == PlayerRelations::ENEMIES)
 	{
 		throw cannotFulfillGoalException("Can not build boat in enemy shipyard");
 	}
@@ -66,9 +64,8 @@ void BuildBoat::accept(AIGateway * ai)
 	}
 
 	logAi->trace(
-		"Building boat at shipyard %s located at %s, estimated boat position %s", 
-		shipyard->o->getObjectName(),
-		shipyard->o->visitablePos().toString(),
+		"Building boat at shipyard located at %s, estimated boat position %s",
+		shipyard->getObject()->visitablePos().toString(),
 		shipyard->bestLocation().toString());
 
 	cb->buildBoat(shipyard);

+ 0 - 2
AI/Nullkiller/Goals/BuildThis.cpp

@@ -11,8 +11,6 @@
 #include "BuildThis.h"
 #include "../AIGateway.h"
 #include "../AIUtility.h"
-#include "../../../lib/mapping/CMap.h" //for victory conditions
-#include "../../../lib/CPathfinder.h"
 #include "../../../lib/StringConstants.h"
 
 

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

@@ -57,12 +57,12 @@ void BuyArmy::accept(AIGateway * ai)
 		if(objid != -1 && ci.creID != objid)
 			continue;
 
-		vstd::amin(ci.count, res / ci.cre->cost);
+		vstd::amin(ci.count, res / ci.cre->getFullRecruitCost());
 
 		if(ci.count)
 		{
 			cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level);
-			valueBought += ci.count * ci.cre->AIValue;
+			valueBought += ci.count * ci.cre->getAIValue();
 		}
 	}
 
@@ -71,7 +71,7 @@ void BuyArmy::accept(AIGateway * ai)
 		throw cannotFulfillGoalException("No creatures to buy.");
 	}
 
-	if(town->visitingHero)
+	if(town->visitingHero && !town->garrisonHero)
 	{
 		ai->moveHeroToTile(town->visitablePos(), town->visitingHero.get());
 	}

+ 1 - 3
AI/Nullkiller/Goals/CompleteQuest.cpp

@@ -11,8 +11,6 @@
 #include "CompleteQuest.h"
 #include "../Behaviors/CaptureObjectsBehavior.h"
 #include "../AIGateway.h"
-#include "../../../lib/mapping/CMap.h" //for victory conditions
-#include "../../../lib/CPathfinder.h"
 #include "../../../lib/VCMI_Lib.h"
 #include "../../../lib/CGeneralTextHandler.h"
 
@@ -214,7 +212,7 @@ TGoalVec CompleteQuest::missionResources() const
 			for(int i = 0; i < q.quest->m7resources.size(); ++i)
 			{
 				if(q.quest->m7resources[i])
-					solutions.push_back(sptr(CollectRes(i, q.quest->m7resources[i])));
+					solutions.push_back(sptr(CollectRes(static_cast<EGameResID>(i), q.quest->m7resources[i])));
 			}
 		}
 	}

+ 1 - 1
AI/Nullkiller/Goals/CompleteQuest.h

@@ -12,7 +12,7 @@
 #include "../AIUtility.h"
 #include "../../../CCallback.h"
 #include "../Goals/CGoal.h"
-#include "../../../lib/CGameState.h"
+#include "../../../lib/gameState/QuestInfo.h"
 
 namespace NKAI
 {

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

@@ -11,8 +11,6 @@
 #include "Composition.h"
 #include "../AIGateway.h"
 #include "../AIUtility.h"
-#include "../../../lib/mapping/CMap.h" //for victory conditions
-#include "../../../lib/CPathfinder.h"
 #include "../../../lib/StringConstants.h"
 
 
@@ -33,9 +31,17 @@ std::string Composition::toString() const
 {
 	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;
@@ -43,17 +49,34 @@ std::string Composition::toString() const
 
 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
 {
-	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)
@@ -66,20 +89,35 @@ Composition & Composition::addNext(TSubgoal goal)
 	}
 	else
 	{
-		subtasks.push_back(goal);
+		subtasks.push_back({goal});
 	}
 
 	return *this;
 }
 
+Composition & Composition::addNext(const AbstractGoal & goal)
+{
+	return addNext(sptr(goal));
+}
+
 bool Composition::isElementar() const
 {
-	return subtasks.back()->isElementar();
+	return subtasks.back().front()->isElementar();
 }
 
 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>
 	{
 	private:
-		TGoalVec subtasks;
+		std::vector<TGoalVec> subtasks; // things we want to do now
 
 	public:
 		Composition()
@@ -26,16 +26,12 @@ namespace Goals
 		{
 		}
 
-		Composition(TGoalVec subtasks)
-			: ElementarGoal(Goals::COMPOSITION), subtasks(subtasks)
-		{
-		}
-
 		virtual bool operator==(const Composition & other) const override;
 		virtual std::string toString() const override;
 		void accept(AIGateway * ai) override;
 		Composition & addNext(const AbstractGoal & goal);
 		Composition & addNext(TSubgoal goal);
+		Composition & addNextSequence(const TGoalVec & taskSequence);
 		virtual TGoalVec decompose() const override;
 		virtual bool isElementar() const override;
 		virtual int getHeroExchangeCount() const override;

+ 0 - 2
AI/Nullkiller/Goals/DismissHero.cpp

@@ -10,8 +10,6 @@
 #include "StdInc.h"
 #include "DismissHero.h"
 #include "../AIGateway.h"
-#include "../../../lib/mapping/CMap.h" //for victory conditions
-#include "../../../lib/CPathfinder.h"
 
 namespace NKAI
 {

+ 0 - 2
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp

@@ -11,8 +11,6 @@
 #include "ExchangeSwapTownHeroes.h"
 #include "ExecuteHeroChain.h"
 #include "../AIGateway.h"
-#include "../../../lib/mapping/CMap.h" //for victory conditions
-#include "../../../lib/CPathfinder.h"
 #include "../Engine/Nullkiller.h"
 
 namespace NKAI

+ 21 - 9
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

@@ -10,8 +10,6 @@
 #include "StdInc.h"
 #include "ExecuteHeroChain.h"
 #include "../AIGateway.h"
-#include "../../../lib/mapping/CMap.h" //for victory conditions
-#include "../../../lib/CPathfinder.h"
 #include "../Engine/Nullkiller.h"
 
 namespace NKAI
@@ -54,6 +52,20 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 	ai->nullkiller->setActive(chainPath.targetHero, tile);
 	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;
 
 	for(int i = chainPath.nodes.size() - 1; i >= 0; i--)
@@ -80,7 +92,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 
 		try
 		{
-			if(hero->movement)
+			if(hero->movementPointsRemaining() > 0)
 			{
 				ai->nullkiller->setActive(hero, node.coord);
 
@@ -105,9 +117,9 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 				{
 					auto targetNode = cb->getPathsInfo(hero)->getPathInfo(node.coord);
 
-					if(targetNode->accessible == CGPathNode::EAccessibility::NOT_SET
-						|| targetNode->accessible == CGPathNode::EAccessibility::BLOCKED
-						|| targetNode->accessible == CGPathNode::EAccessibility::FLYABLE
+					if(targetNode->accessible == EPathAccessibility::NOT_SET
+						|| targetNode->accessible == EPathAccessibility::BLOCKED
+						|| targetNode->accessible == EPathAccessibility::FLYABLE
 						|| targetNode->turns != 0)
 					{
 						logAi->error(
@@ -119,7 +131,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 					}
 				}
 
-				if(hero->movement)
+				if(hero->movementPointsRemaining())
 				{
 					try
 					{
@@ -137,14 +149,14 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 							return;
 						}
 
-						if(hero->movement > 0)
+						if(hero->movementPointsRemaining() > 0)
 						{
 							CGPath path;
 							bool isOk = cb->getPathsInfo(hero)->getPath(path, node.coord);
 
 							if(isOk && path.nodes.back().turns > 0)
 							{
-								logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->getNameTranslated(), hero->movement, node.coord.toString());
+								logAi->warn("Hero %s has %d mp which is not enough to continue his way towards %s.", hero->getNameTranslated(), hero->movementPointsRemaining(), node.coord.toString());
 
 								ai->nullkiller->lockHero(hero, HeroLockedReason::HERO_CHAIN);
 								return;

+ 2 - 2
AI/Nullkiller/Goals/GatherArmy.cpp

@@ -97,7 +97,7 @@ TGoalVec GatherArmy::getAllPossibleSubgoals()
 
 			//Do not use below code for now, rely on generic Build. Code below needs to know a lot of town/resource context to do more good than harm
 			/*auto bid = ai->ah->canBuildAnyStructure(t, std::vector<BuildingID>(unitsSource, unitsSource + ARRAY_COUNT(unitsSource)), 1);
-			if (bid.is_initialized())
+			if (bid.has_value())
 			{
 				auto goal = sptr(BuildThis(bid.get(), t).setpriority(priority));
 				if (!ai->ah->containsObjective(goal)) //avoid loops caused by reserving same objective twice
@@ -162,7 +162,7 @@ TGoalVec GatherArmy::getAllPossibleSubgoals()
 							for(auto & creatureID : creLevel.second)
 							{
 								auto creature = VLC->creh->creatures[creatureID];
-								if(ai->ah->freeResources().canAfford(creature->cost))
+								if(ai->ah->freeResources().canAfford(creature->getFullRecruitCost()))
 									objs.push_back(obj); //TODO: reserve resources?
 							}
 						}

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

@@ -11,8 +11,6 @@
 #include "Goals.h"
 #include "../AIGateway.h"
 #include "../AIUtility.h"
-#include "../../../lib/mapping/CMap.h" //for victory conditions
-#include "../../../lib/CPathfinder.h"
 #include "../../../lib/StringConstants.h"
 
 
@@ -26,7 +24,10 @@ using namespace Goals;
 
 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)
@@ -47,20 +48,20 @@ void RecruitHero::accept(AIGateway * ai)
 		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)
 	{
 		cb->swapGarrisonHero(t);

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

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

+ 0 - 2
AI/Nullkiller/Goals/SaveResources.cpp

@@ -10,8 +10,6 @@
 #include "StdInc.h"
 #include "SaveResources.h"
 #include "../AIGateway.h"
-#include "../../../lib/mapping/CMap.h" //for victory conditions
-#include "../../../lib/CPathfinder.h"
 #include "../Behaviors/CaptureObjectsBehavior.h"
 
 namespace NKAI

+ 2 - 2
AI/Nullkiller/Goals/Trade.h

@@ -27,10 +27,10 @@ namespace Goals
 			: CGoal(Goals::TRADE)
 		{
 		}
-		Trade(int rid, int val, int Objid)
+		Trade(GameResID rid, int val, int Objid)
 			: CGoal(Goals::TRADE)
 		{
-			resID = rid;
+			resID = rid.getNum();
 			value = val;
 			objid = Objid;
 		}

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

+ 8 - 1
AI/Nullkiller/Markers/ArmyUpgrade.cpp

@@ -23,11 +23,18 @@ using namespace Goals;
 
 ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade)
 	: CGoal(Goals::ARMY_UPGRADE), upgrader(upgrader), upgradeValue(upgrade.upgradeValue),
-	initialValue(upgradePath.heroArmy->getArmyStrength()), goldCost(upgrade.upgradeCost[Res::GOLD])
+	initialValue(upgradePath.heroArmy->getArmyStrength()), goldCost(upgrade.upgradeCost[EGameResID::GOLD])
 {
 	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
 {
 	return false;

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

@@ -27,6 +27,7 @@ namespace Goals
 
 	public:
 		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 std::string toString() const override;

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

@@ -18,8 +18,8 @@ namespace NKAI
 
 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);
 	sethero(defencePath.targetHero);

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

@@ -24,9 +24,10 @@ namespace Goals
 		uint64_t defenceArmyStrength;
 		HitMapInfo treat;
 		uint8_t turn;
+		bool counterattack;
 
 	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);
 
 		virtual bool operator==(const DefendTown & other) const override;
@@ -37,6 +38,8 @@ namespace Goals
 		uint64_t getDefenceStrength() const { return defenceArmyStrength; }
 
 		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
 {
-	return "Hero exchange " + exchangePath.toString();
+	return "Hero exchange for " +hero.get()->getObjectName() + " by " + exchangePath.toString();
 }
 
 uint64_t HeroExchange::getReinforcementArmyStrength() const

+ 67 - 40
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -16,7 +16,9 @@
 #include "../../../CCallback.h"
 #include "../../../lib/mapping/CMap.h"
 #include "../../../lib/mapObjects/MapObjects.h"
-#include "../../../lib/PathfinderUtil.h"
+#include "../../../lib/pathfinder/CPathfinder.h"
+#include "../../../lib/pathfinder/PathfinderUtil.h"
+#include "../../../lib/pathfinder/PathfinderOptions.h"
 #include "../../../lib/CPlayerState.h"
 
 namespace NKAI
@@ -52,6 +54,27 @@ AISharedStorage::~AISharedStorage()
 	}
 }
 
+void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
+{
+	if(!specialAction)
+	{
+		specialAction = action;
+	}
+	else
+	{
+		auto parts = specialAction->getParts();
+
+		if(parts.empty())
+		{
+			parts.push_back(specialAction);
+		}
+
+		parts.push_back(action);
+
+		specialAction = std::make_shared<CompositeAction>(parts);
+	}
+}
+
 AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
 	: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
 {
@@ -120,7 +143,7 @@ void AINodeStorage::clear()
 	turnDistanceLimit[HeroRole::SCOUT] = 255;
 }
 
-boost::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
+std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 	const int3 & pos, 
 	const EPathfindingLayer layer, 
 	const ChainActor * actor)
@@ -131,7 +154,7 @@ boost::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 
 	if(chains[0].blocked())
 	{
-		return boost::none;
+		return std::nullopt;
 	}
 
 	for(auto i = AIPathfinding::BUCKET_SIZE - 1; i >= 0; i--)
@@ -151,7 +174,7 @@ boost::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 		}
 	}
 
-	return boost::none;
+	return std::nullopt;
 }
 
 std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
@@ -175,7 +198,7 @@ std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
 		if(!allocated)
 			continue;
 
-		AIPathNode * initialNode = allocated.get();
+		AIPathNode * initialNode = allocated.value();
 
 		initialNode->inPQ = false;
 		initialNode->pq = nullptr;
@@ -183,7 +206,7 @@ std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
 		initialNode->moveRemains = actor->initialMovement;
 		initialNode->danger = 0;
 		initialNode->setCost(actor->initialTurn);
-		initialNode->action = CGPathNode::ENodeAction::NORMAL;
+		initialNode->action = EPathNodeAction::NORMAL;
 
 		if(actor->isMovable)
 		{
@@ -201,7 +224,7 @@ std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
 	return initialNodes;
 }
 
-void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, CGPathNode::EAccessibility accessibility)
+void AINodeStorage::resetTile(const int3 & coord, EPathfindingLayer layer, EPathAccessibility accessibility)
 {
 	for(AIPathNode & heroNode : nodes.get(coord, layer))
 {
@@ -239,7 +262,7 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf
 void AINodeStorage::commit(
 	AIPathNode * destination, 
 	const AIPathNode * source, 
-	CGPathNode::ENodeAction action, 
+	EPathNodeAction action, 
 	int turn, 
 	int movementLeft, 
 	float cost) const
@@ -289,10 +312,10 @@ std::vector<CGPathNode *> AINodeStorage::calculateNeighbours(
 		{
 			auto nextNode = getOrCreateNode(neighbour, i, srcNode->actor);
 
-			if(!nextNode || nextNode.get()->accessible == CGPathNode::NOT_SET)
+			if(!nextNode || nextNode.value()->accessible == EPathAccessibility::NOT_SET)
 				continue;
 
-			neighbours.push_back(nextNode.get());
+			neighbours.push_back(nextNode.value());
 		}
 	}
 	
@@ -319,7 +342,7 @@ bool AINodeStorage::increaseHeroChainTurnLimit()
 			{
 				for(AIPathNode & node : chains)
 				{
-					if(node.turns <= heroChainTurn && node.action != CGPathNode::ENodeAction::UNKNOWN)
+					if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
 					{
 						commitedTiles.insert(pos);
 						break;
@@ -349,7 +372,7 @@ bool AINodeStorage::calculateHeroChainFinal()
 				{
 					if(node.turns > heroChainTurn
 						&& !node.locked
-						&& node.action != CGPathNode::ENodeAction::UNKNOWN
+						&& node.action != EPathNodeAction::UNKNOWN
 						&& node.actor->actorExchangeCount > 1
 						&& !hasBetterChain(&node, &node, chains))
 					{
@@ -421,7 +444,7 @@ public:
 
 				for(AIPathNode & node : chains)
 				{
-					if(node.turns <= heroChainTurn && node.action != CGPathNode::ENodeAction::UNKNOWN)
+					if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
 						existingChains.push_back(&node);
 				}
 
@@ -621,16 +644,16 @@ void HeroChainCalculationTask::calculateHeroChain(
 		if(node->actor->actorExchangeCount + srcNode->actor->actorExchangeCount > CHAIN_MAX_DEPTH)
 			continue;
 
-		if(node->action == CGPathNode::ENodeAction::BATTLE
-			|| node->action == CGPathNode::ENodeAction::TELEPORT_BATTLE
-			|| node->action == CGPathNode::ENodeAction::TELEPORT_NORMAL
-			|| node->action == CGPathNode::ENodeAction::TELEPORT_BLOCKING_VISIT)
+		if(node->action == EPathNodeAction::BATTLE
+			|| node->action == EPathNodeAction::TELEPORT_BATTLE
+			|| node->action == EPathNodeAction::TELEPORT_NORMAL
+			|| node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT)
 		{
 			continue;
 		}
 
 		if(node->turns > heroChainTurn 
-			|| (node->action == CGPathNode::ENodeAction::UNKNOWN && node->actor->hero)
+			|| (node->action == EPathNodeAction::UNKNOWN && node->actor->hero)
 			|| (node->actor->chainMask & srcNode->actor->chainMask) != 0)
 		{
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
@@ -643,7 +666,7 @@ void HeroChainCalculationTask::calculateHeroChain(
 				srcNode->coord.toString(),
 				(node->turns > heroChainTurn 
 					? "turn limit" 
-					: (node->action == CGPathNode::ENodeAction::UNKNOWN && node->actor->hero)
+					: (node->action == EPathNodeAction::UNKNOWN && node->actor->hero)
 						? "action unknown"
 						: "chain mask"));
 #endif
@@ -670,8 +693,8 @@ void HeroChainCalculationTask::calculateHeroChain(
 	std::vector<ExchangeCandidate> & result)
 {	
 	if(carrier->armyLoss < carrier->actor->armyValue
-		&& (carrier->action != CGPathNode::BATTLE || (carrier->actor->allowBattle && carrier->specialAction))
-		&& carrier->action != CGPathNode::BLOCKING_VISIT
+		&& (carrier->action != EPathNodeAction::BATTLE || (carrier->actor->allowBattle && carrier->specialAction))
+		&& carrier->action != EPathNodeAction::BLOCKING_VISIT
 		&& (other->armyLoss == 0 || other->armyLoss < other->actor->armyValue))
 	{
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
@@ -722,9 +745,9 @@ void HeroChainCalculationTask::addHeroChain(const std::vector<ExchangeCandidate>
 			continue;
 		}
 
-		auto exchangeNode = chainNodeOptional.get();
+		auto exchangeNode = chainNodeOptional.value();
 
-		if(exchangeNode->action != CGPathNode::ENodeAction::UNKNOWN)
+		if(exchangeNode->action != EPathNodeAction::UNKNOWN)
 		{
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
 			logAi->trace(
@@ -765,7 +788,7 @@ void HeroChainCalculationTask::addHeroChain(const std::vector<ExchangeCandidate>
 		if(exchangeNode->actor->actorAction)
 		{
 			exchangeNode->theNodeBefore = carrier;
-			exchangeNode->specialAction = exchangeNode->actor->actorAction;
+			exchangeNode->addSpecialAction(exchangeNode->actor->actorAction);
 		}
 
 		exchangeNode->chainOther = other;
@@ -856,16 +879,20 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
 	for(auto & hero : heroes)
 	{
 		// 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;
+		}
 
 		uint64_t mask = FirstActorMask << actors.size();
 		auto actor = std::make_shared<HeroActor>(hero.first, hero.second, mask, ai);
 
 		if(actor->hero->tempOwner != ai->playerID)
 		{
-			bool onLand = !actor->hero->boat;
-			actor->initialMovement = actor->hero->maxMovePoints(onLand);
+			bool onLand = !actor->hero->boat || actor->hero->boat->layer != EPathfindingLayer::SAIL;
+			actor->initialMovement = actor->hero->movementPointsLimit(onLand);
 		}
 
 		playerID = actor->hero->tempOwner;
@@ -946,7 +973,7 @@ std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
 			if(!node)
 				continue;
 
-			neighbours.push_back(node.get());
+			neighbours.push_back(node.value());
 		}
 	}
 
@@ -1017,35 +1044,35 @@ struct TowmPortalFinder
 		return nullptr;
 	}
 
-	boost::optional<AIPathNode *> createTownPortalNode(const CGTownInstance * targetTown)
+	std::optional<AIPathNode *> createTownPortalNode(const CGTownInstance * targetTown)
 	{
 		auto bestNode = getBestInitialNodeForTownPortal(targetTown);
 
 		if(!bestNode)
-			return boost::none;
+			return std::nullopt;
 
 		auto nodeOptional = nodeStorage->getOrCreateNode(targetTown->visitablePos(), EPathfindingLayer::LAND, actor->castActor);
 
 		if(!nodeOptional)
-			return boost::none;
+			return std::nullopt;
 
-		AIPathNode * node = nodeOptional.get();
-		float movementCost = (float)movementNeeded / (float)hero->maxMovePoints(EPathfindingLayer::LAND);
+		AIPathNode * node = nodeOptional.value();
+		float movementCost = (float)movementNeeded / (float)hero->movementPointsLimit(EPathfindingLayer::LAND);
 
 		movementCost += bestNode->getCost();
 
-		if(node->action == CGPathNode::UNKNOWN || node->getCost() > movementCost)
+		if(node->action == EPathNodeAction::UNKNOWN || node->getCost() > movementCost)
 		{
 			nodeStorage->commit(
 				node,
 				nodeStorage->getAINode(bestNode),
-				CGPathNode::TELEPORT_NORMAL,
+				EPathNodeAction::TELEPORT_NORMAL,
 				bestNode->turns,
 				bestNode->moveRemains - movementNeeded,
 				movementCost);
 
 			node->theNodeBefore = bestNode;
-			node->specialAction.reset(new AIPathfinding::TownPortalAction(targetTown));
+			node->addSpecialAction(std::make_shared<AIPathfinding::TownPortalAction>(targetTown));
 		}
 
 		return nodeOptional;
@@ -1095,7 +1122,7 @@ void AINodeStorage::calculateTownPortal(
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 1
 				logAi->trace("Adding town portal node at %s", targetTown->getObjectName());
 #endif
-				output.push_back(nodeOptional.get());
+				output.push_back(nodeOptional.value());
 			}
 		}
 	}
@@ -1167,7 +1194,7 @@ bool AINodeStorage::hasBetterChain(
 	{
 		auto sameNode = node.actor == candidateNode->actor;
 
-		if(sameNode	|| node.action == CGPathNode::ENodeAction::UNKNOWN || !node.actor || !node.actor->hero)
+		if(sameNode	|| node.action == EPathNodeAction::UNKNOWN || !node.actor || !node.actor->hero)
 		{
 			continue;
 		}
@@ -1250,7 +1277,7 @@ bool AINodeStorage::isTileAccessible(const HeroPtr & hero, const int3 & pos, con
 
 	for(const AIPathNode & node : chains)
 	{
-		if(node.action != CGPathNode::ENodeAction::UNKNOWN 
+		if(node.action != EPathNodeAction::UNKNOWN 
 			&& node.actor && node.actor->hero == hero.h)
 		{
 			return true;
@@ -1270,7 +1297,7 @@ std::vector<AIPath> AINodeStorage::getChainInfo(const int3 & pos, bool isOnLand)
 
 	for(const AIPathNode & node : chains)
 	{
-		if(node.action == CGPathNode::ENodeAction::UNKNOWN || !node.actor || !node.actor->hero)
+		if(node.action == EPathNodeAction::UNKNOWN || !node.actor || !node.actor->hero)
 		{
 			continue;
 		}

+ 14 - 11
AI/Nullkiller/Pathfinding/AINodeStorage.h

@@ -13,7 +13,8 @@
 #define NKAI_PATHFINDER_TRACE_LEVEL 0
 #define NKAI_TRACE_LEVEL 0
 
-#include "../../../lib/CPathfinder.h"
+#include "../../../lib/pathfinder/CGPathNode.h"
+#include "../../../lib/pathfinder/INodeStorage.h"
 #include "../../../lib/mapObjects/CGHeroInstance.h"
 #include "../AIUtility.h"
 #include "../Engine/FuzzyHelper.h"
@@ -23,8 +24,8 @@
 
 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
 {
@@ -52,9 +53,11 @@ struct AIPathNode : public CGPathNode
 	STRONG_INLINE
 	bool blocked() const
 	{
-		return accessible == CGPathNode::EAccessibility::NOT_SET
-			|| accessible == CGPathNode::EAccessibility::BLOCKED;
+		return accessible == EPathAccessibility::NOT_SET
+			|| accessible == EPathAccessibility::BLOCKED;
 	}
+
+	void addSpecialAction(std::shared_ptr<const SpecialAction> action);
 };
 
 struct AIPathNodeInfo
@@ -193,7 +196,7 @@ public:
 	void commit(
 		AIPathNode * destination,
 		const AIPathNode * source,
-		CGPathNode::ENodeAction action,
+		EPathNodeAction action,
 		int turn,
 		int movementLeft,
 		float cost) const;
@@ -205,14 +208,14 @@ public:
 
 	inline void updateAINode(CGPathNode * node, std::function<void (AIPathNode *)> updater)
 	{
-		auto aiNode = static_cast<AIPathNode *>(node);
+		auto * aiNode = static_cast<AIPathNode *>(node);
 
 		updater(aiNode);
 	}
 
 	inline const CGHeroInstance * getHero(const CGPathNode * node) const
 	{
-		auto aiNode = getAINode(node);
+		const auto * aiNode = getAINode(node);
 
 		return aiNode->actor->hero;
 	}
@@ -232,7 +235,7 @@ public:
 		const AIPathNode * destinationNode,
 		const NodeRange & chains) const;
 
-	boost::optional<AIPathNode *> getOrCreateNode(const int3 & coord, const EPathfindingLayer layer, const ChainActor * actor);
+	std::optional<AIPathNode *> getOrCreateNode(const int3 & coord, const EPathfindingLayer layer, const ChainActor * actor);
 	std::vector<AIPath> getChainInfo(const int3 & pos, bool isOnLand) const;
 	bool isTileAccessible(const HeroPtr & hero, const int3 & pos, const EPathfindingLayer layer) const;
 	void setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes);
@@ -255,11 +258,11 @@ public:
 	{
 		double ratio = (double)danger / (armyValue * hero->getFightingStrength());
 
-		return (uint64_t)(armyValue * ratio * ratio * ratio);
+		return (uint64_t)(armyValue * ratio * ratio);
 	}
 
 	STRONG_INLINE
-	void resetTile(const int3 & tile, EPathfindingLayer layer, CGPathNode::EAccessibility accessibility);
+	void resetTile(const int3 & tile, EPathfindingLayer layer, EPathAccessibility accessibility);
 
 	STRONG_INLINE int getBucket(const ChainActor * actor) const
 	{

+ 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->setMainTurnDistanceLimit(pathfinderSettings.mainTurnDistanceLimit);
 
+	logAi->trace(
+		"Scout turn distance: %s, main %s",
+		std::to_string(pathfinderSettings.scoutTurnDistanceLimit),
+		std::to_string(pathfinderSettings.mainTurnDistanceLimit));
+
 	if(pathfinderSettings.useHeroChain)
 	{
 		storage->setTownsAndDwellings(cb->getTownsInfo(), ai->memory->visitableObjs);

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

@@ -15,6 +15,8 @@
 #include "Rules/AIPreviousNodeRule.h"
 #include "../Engine//Nullkiller.h"
 
+#include "../../../lib/pathfinder/CPathfinder.h"
+
 namespace NKAI
 {
 namespace AIPathfinding
@@ -44,6 +46,8 @@ namespace AIPathfinding
 	{
 	}
 
+	AIPathfinderConfig::~AIPathfinderConfig() = default;
+
 	CPathfinderHelper * AIPathfinderConfig::getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs)
 	{
 		auto hero = aiNodeStorage->getHero(source.node);

+ 3 - 0
AI/Nullkiller/Pathfinding/AIPathfinderConfig.h

@@ -11,6 +11,7 @@
 #pragma once
 
 #include "AINodeStorage.h"
+#include "../../../lib/pathfinder/PathfinderOptions.h"
 
 namespace NKAI
 {
@@ -31,6 +32,8 @@ namespace AIPathfinding
 			Nullkiller * ai,
 			std::shared_ptr<AINodeStorage> nodeStorage);
 
+		~AIPathfinderConfig();
+
 		virtual CPathfinderHelper * getOrCreatePathfinderHelper(const PathNodeInfo & source, CGameState * gs) override;
 	};
 }

+ 0 - 1
AI/Nullkiller/Pathfinding/Actions/BattleAction.cpp

@@ -12,7 +12,6 @@
 #include "BattleAction.h"
 #include "../../AIGateway.h"
 #include "../../Goals/CompleteQuest.h"
-#include "../../../../lib/mapping/CMap.h" //for victory conditions
 
 namespace NKAI
 {

+ 0 - 1
AI/Nullkiller/Pathfinding/Actions/BattleAction.h

@@ -11,7 +11,6 @@
 #pragma once
 
 #include "SpecialAction.h"
-#include "../../../../lib/CGameState.h"
 
 namespace NKAI
 {

+ 5 - 6
AI/Nullkiller/Pathfinding/Actions/BoatActions.cpp

@@ -14,7 +14,6 @@
 #include "../../Goals/CaptureObject.h"
 #include "../../Goals/Invalid.h"
 #include "../../Goals/BuildBoat.h"
-#include "../../../../lib/mapping/CMap.h"
 #include "../../../../lib/mapObjects/MapObjects.h"
 #include "BoatActions.h"
 
@@ -33,9 +32,9 @@ namespace AIPathfinding
 
 	Goals::TSubgoal BuildBoatAction::decompose(const CGHeroInstance * hero) const
 	{
-		if(cb->getPlayerRelations(ai->playerID, shipyard->o->tempOwner) == PlayerRelations::ENEMIES)
+		if(cb->getPlayerRelations(ai->playerID, shipyard->getObject()->getOwner()) == PlayerRelations::ENEMIES)
 		{
-			return Goals::sptr(Goals::CaptureObject(shipyard->o));
+			return Goals::sptr(Goals::CaptureObject(targetObject()));
 		}
 		
 		return Goals::sptr(Goals::Invalid());
@@ -45,7 +44,7 @@ namespace AIPathfinding
 	{
 		auto hero = source->actor->hero;
 
-		if(cb->getPlayerRelations(hero->tempOwner, shipyard->o->tempOwner) == PlayerRelations::ENEMIES)
+		if(cb->getPlayerRelations(hero->tempOwner, shipyard->getObject()->getOwner()) == PlayerRelations::ENEMIES)
 		{
 #if NKAI_TRACE_LEVEL > 1
 			logAi->trace("Can not build a boat. Shipyard is enemy.");
@@ -71,7 +70,7 @@ namespace AIPathfinding
 
 	const CGObjectInstance * BuildBoatAction::targetObject() const
 	{
-		return shipyard->o;
+		return dynamic_cast<const CGObjectInstance*>(shipyard);
 	}
 
 	const ChainActor * BuildBoatAction::getActor(const ChainActor * sourceActor) const
@@ -102,7 +101,7 @@ namespace AIPathfinding
 
 	std::string BuildBoatAction::toString() const
 	{
-		return "Build Boat at " + shipyard->o->getObjectName();
+		return "Build Boat at " + shipyard->getObject()->visitablePos().toString();
 	}
 
 	bool SummonBoatAction::canAct(const AIPathNode * source) const

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

@@ -11,7 +11,6 @@
 #pragma once
 
 #include "SpecialAction.h"
-#include "../../../../lib/mapping/CMap.h"
 #include "../../../../lib/mapObjects/MapObjects.h"
 
 namespace NKAI

+ 0 - 1
AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.cpp

@@ -12,7 +12,6 @@
 #include "BuyArmyAction.h"
 #include "../../AIGateway.h"
 #include "../../Goals/CompleteQuest.h"
-#include "../../../../lib/mapping/CMap.h" //for victory conditions
 
 namespace NKAI
 {

+ 0 - 1
AI/Nullkiller/Pathfinding/Actions/BuyArmyAction.h

@@ -11,7 +11,6 @@
 #pragma once
 
 #include "SpecialAction.h"
-#include "../../../../lib/CGameState.h"
 
 namespace NKAI
 {

+ 0 - 1
AI/Nullkiller/Pathfinding/Actions/QuestAction.cpp

@@ -12,7 +12,6 @@
 #include "QuestAction.h"
 #include "../../AIGateway.h"
 #include "../../Goals/CompleteQuest.h"
-#include "../../../../lib/mapping/CMap.h" //for victory conditions
 
 namespace NKAI
 {

+ 1 - 1
AI/Nullkiller/Pathfinding/Actions/QuestAction.h

@@ -11,7 +11,7 @@
 #pragma once
 
 #include "SpecialAction.h"
-#include "../../../../lib/CGameState.h"
+#include "../../../../lib/gameState/QuestInfo.h"
 
 namespace NKAI
 {

+ 63 - 0
AI/Nullkiller/Pathfinding/Actions/SpecialAction.cpp

@@ -27,4 +27,67 @@ void SpecialAction::execute(const CGHeroInstance * hero) const
 	throw cannotFulfillGoalException("Can not execute " + toString());
 }
 
+bool CompositeAction::canAct(const AIPathNode * source) const
+{
+	for(auto part : parts)
+	{
+		if(!part->canAct(source)) return false;
+	}
+
+	return true;
+}
+
+Goals::TSubgoal CompositeAction::decompose(const CGHeroInstance * hero) const
+{
+	for(auto part : parts)
+	{
+		auto goal = part->decompose(hero);
+
+		if(!goal->invalid()) return goal;
+	}
+
+	return SpecialAction::decompose(hero);
+}
+
+void CompositeAction::execute(const CGHeroInstance * hero) const
+{
+	for(auto part : parts)
+	{
+		part->execute(hero);
+	}
+}
+
+void CompositeAction::applyOnDestination(
+	const CGHeroInstance * hero,
+	CDestinationNodeInfo & destination,
+	const PathNodeInfo & source,
+	AIPathNode * dstNode,
+	const AIPathNode * srcNode) const
+{
+	for(auto part : parts)
+	{
+		part->applyOnDestination(hero, destination, source, dstNode, srcNode);
+	}
+}
+
+std::string CompositeAction::toString() const
+{
+	std::string result = "";
+
+	for(auto part : parts)
+	{
+		result += ", " + part->toString();
+	}
+
+	return result;
+}
+
+const CGObjectInstance * CompositeAction::targetObject() const
+{
+	if(parts.empty())
+		return nullptr;
+
+	return parts.front()->targetObject();
+}
+
 }

+ 38 - 1
AI/Nullkiller/Pathfinding/Actions/SpecialAction.h

@@ -13,6 +13,11 @@
 #include "../../AIUtility.h"
 #include "../../Goals/AbstractGoal.h"
 
+VCMI_LIB_NAMESPACE_BEGIN
+struct PathNodeInfo;
+struct CDestinationNodeInfo;
+VCMI_LIB_NAMESPACE_END
+
 namespace NKAI
 {
 
@@ -36,7 +41,7 @@ public:
 		const CGHeroInstance * hero,
 		CDestinationNodeInfo & destination,
 		const PathNodeInfo & source,
-		AIPathNode * dstMode,
+		AIPathNode * dstNode,
 		const AIPathNode * srcNode) const
 	{
 	}
@@ -44,6 +49,38 @@ public:
 	virtual std::string toString() const = 0;
 
 	virtual const CGObjectInstance * targetObject() const { return nullptr; }
+
+	virtual std::vector<std::shared_ptr<const SpecialAction>> getParts() const
+	{
+		return {};
+	}
+};
+
+class CompositeAction : public SpecialAction
+{
+private:
+	std::vector<std::shared_ptr<const SpecialAction>> parts;
+
+public:
+	CompositeAction(std::vector<std::shared_ptr<const SpecialAction>> parts) : parts(parts) {}
+
+	bool canAct(const AIPathNode * source) const override;
+	void execute(const CGHeroInstance * hero) const override;
+	std::string toString() const override;
+	const CGObjectInstance * targetObject() const override;
+	Goals::TSubgoal decompose(const CGHeroInstance * hero) const override;
+
+	std::vector<std::shared_ptr<const SpecialAction>> getParts() const override
+	{
+		return parts;
+	}
+
+	void applyOnDestination(
+		const CGHeroInstance * hero,
+		CDestinationNodeInfo & destination,
+		const PathNodeInfo & source,
+		AIPathNode * dstNode,
+		const AIPathNode * srcNode) const override;
 };
 
 }

+ 0 - 1
AI/Nullkiller/Pathfinding/Actions/TownPortalAction.cpp

@@ -10,7 +10,6 @@
 
 #include "StdInc.h"
 #include "../../Goals/AdventureSpellCast.h"
-#include "../../../../lib/mapping/CMap.h"
 #include "../../../../lib/mapObjects/MapObjects.h"
 #include "TownPortalAction.h"
 

+ 0 - 1
AI/Nullkiller/Pathfinding/Actions/TownPortalAction.h

@@ -11,7 +11,6 @@
 #pragma once
 
 #include "SpecialAction.h"
-#include "../../../../lib/mapping/CMap.h"
 #include "../../../../lib/mapObjects/MapObjects.h"
 #include "../../Goals/AdventureSpellCast.h"
 

+ 11 - 10
AI/Nullkiller/Pathfinding/Actors.cpp

@@ -12,8 +12,8 @@
 #include "../AIGateway.h"
 #include "../Engine/Nullkiller.h"
 #include "../../../CCallback.h"
-#include "../../../lib/mapping/CMap.h"
 #include "../../../lib/mapObjects/MapObjects.h"
+#include "../../../lib/pathfinder/TurnInfo.h"
 #include "Actions/BuyArmyAction.h"
 
 using namespace NKAI;
@@ -42,8 +42,8 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t
 	baseActor(this), carrierParent(nullptr), otherParent(nullptr), actorExchangeCount(1), armyCost(), actorAction()
 {
 	initialPosition = hero->visitablePos();
-	layer = hero->boat ? EPathfindingLayer::SAIL : EPathfindingLayer::LAND;
-	initialMovement = hero->movement;
+	layer = hero->boat ? hero->boat->layer : EPathfindingLayer::LAND;
+	initialMovement = hero->movementPointsRemaining();
 	initialTurn = 0;
 	armyValue = hero->getArmyStrength();
 	heroFightingStrength = hero->getFightingStrength();
@@ -75,7 +75,7 @@ int ChainActor::maxMovePoints(CGPathNode::ELayer layer)
 		throw std::logic_error("Asking movement points for static actor");
 #endif
 
-	return hero->maxMovePointsCached(layer, tiCache.get());
+	return hero->movementPointsLimitCached(layer, tiCache.get());
 }
 
 std::string ChainActor::toString() const
@@ -134,6 +134,7 @@ void ChainActor::setBaseActor(HeroActor * base)
 	armyCost = base->armyCost;
 	actorAction = base->actorAction;
 	tiCache = base->tiCache;
+	actorExchangeCount = base->actorExchangeCount;
 }
 
 void HeroActor::setupSpecialActors()
@@ -350,7 +351,7 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
 		{
 			auto targetSlot = target->getFreeSlot();
 
-			target->addToSlot(targetSlot, slotInfo.creature->idNumber, TQuantity(slotInfo.count));
+			target->addToSlot(targetSlot, slotInfo.creature->getId(), TQuantity(slotInfo.count));
 		}
 
 		resources -= upgradeInfo.upgradeCost;
@@ -372,10 +373,10 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
 
 		for(auto & creatureToBuy : buyArmy)
 		{
-			auto targetSlot = target->getSlotFor(creatureToBuy.cre);
+			auto targetSlot = target->getSlotFor(dynamic_cast<const CCreature*>(creatureToBuy.cre));
 
 			target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count);
-			target->armyCost += creatureToBuy.cre->cost * creatureToBuy.count;
+			target->armyCost += creatureToBuy.cre->getFullRecruitCost() * creatureToBuy.count;
 			target->requireBuyArmy = true;
 		}
 	}
@@ -399,7 +400,7 @@ HeroExchangeArmy * HeroExchangeMap::pickBestCreatures(const CCreatureSet * army1
 	{
 		auto targetSlot = target->getFreeSlot();
 
-		target->addToSlot(targetSlot, slotInfo.creature->idNumber, TQuantity(slotInfo.count));
+		target->addToSlot(targetSlot, slotInfo.creature->getId(), TQuantity(slotInfo.count));
 	}
 
 	return target;
@@ -420,7 +421,7 @@ DwellingActor::DwellingActor(const CGDwelling * dwelling, uint64_t chainMask, bo
 {
 	for(auto & slot : creatureSet->Slots())
 	{
-		armyCost += slot.second->getCreatureID().toCreature()->cost * slot.second->count;
+		armyCost += slot.second->getCreatureID().toCreature()->getFullRecruitCost() * slot.second->count;
 	}
 }
 
@@ -454,7 +455,7 @@ CCreatureSet * DwellingActor::getDwellingCreatures(const CGDwelling * dwelling,
 		auto creature = creatureInfo.second.back().toCreature();
 		dwellingCreatures->addToSlot(
 			dwellingCreatures->getSlotFor(creature),
-			creature->idNumber,
+			creature->getId(),
 			TQuantity(creatureInfo.first));
 	}
 

+ 2 - 5
AI/Nullkiller/Pathfinding/Actors.h

@@ -10,7 +10,6 @@
 
 #pragma once
 
-#include "../../../lib/CPathfinder.h"
 #include "../../../lib/mapObjects/CGHeroInstance.h"
 #include "../AIUtility.h"
 #include "Actions/SpecialAction.h"
@@ -32,9 +31,7 @@ public:
 	virtual bool needsLastStack() const override;
 	std::shared_ptr<SpecialAction> getActorAction() const;
 
-	HeroExchangeArmy() : CArmedInstance(true), armyCost(), requireBuyArmy(false)
-	{
-	}
+	HeroExchangeArmy(): CArmedInstance(true), requireBuyArmy(false) {}
 };
 
 struct ExchangeResult
@@ -85,7 +82,7 @@ public:
 	ExchangeResult tryExchangeNoLock(const ChainActor * other) const { return tryExchangeNoLock(this, other); }
 	void setBaseActor(HeroActor * base);
 	virtual const CGObjectInstance * getActorObject() const	{ return hero; }
-	int maxMovePoints(CGPathNode::ELayer layer);
+	int maxMovePoints(EPathfindingLayer layer);
 
 protected:
 	virtual ExchangeResult tryExchangeNoLock(const ChainActor * specialActor, const ChainActor * other) const;

+ 4 - 4
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp

@@ -129,13 +129,13 @@ namespace AIPathfinding
 
 			if(boatNodeOptional)
 			{
-				AIPathNode * boatNode = boatNodeOptional.get();
+				AIPathNode * boatNode = boatNodeOptional.value();
 
-				if(boatNode->action == CGPathNode::UNKNOWN)
+				if(boatNode->action == EPathNodeAction::UNKNOWN)
 				{
-					boatNode->specialAction = virtualBoat;
+					boatNode->addSpecialAction(virtualBoat);
 					destination.blocked = false;
-					destination.action = CGPathNode::ENodeAction::EMBARK;
+					destination.action = EPathNodeAction::EMBARK;
 					destination.node = boatNode;
 					result = true;
 				}

+ 1 - 1
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.h

@@ -14,8 +14,8 @@
 #include "../../AIGateway.h"
 #include "../Actions/BoatActions.h"
 #include "../../../../CCallback.h"
-#include "../../../../lib/mapping/CMap.h"
 #include "../../../../lib/mapObjects/MapObjects.h"
+#include "../../../../lib/pathfinder/PathfindingRules.h"
 
 namespace NKAI
 {

+ 11 - 6
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

@@ -139,17 +139,17 @@ namespace AIPathfinding
 		{
 			if(!destinationNode->actor->allowUseResources)
 			{
-				boost::optional<AIPathNode *> questNode = nodeStorage->getOrCreateNode(
+				std::optional<AIPathNode *> questNode = nodeStorage->getOrCreateNode(
 					destination.coord,
 					destination.node->layer,
 					destinationNode->actor->resourceActor);
 
-				if(!questNode || questNode.get()->getCost() < destination.cost)
+				if(!questNode || questNode.value()->getCost() < destination.cost)
 				{
 					return false;
 				}
 
-				destination.node = questNode.get();
+				destination.node = questNode.value();
 
 				nodeStorage->commit(destination, source);
 				AIPreviousNodeRule(nodeStorage).process(source, destination, pathfinderConfig, pathfinderHelper);
@@ -157,7 +157,7 @@ namespace AIPathfinding
 
 			nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
 			{
-				node->specialAction.reset(new QuestAction(questAction));
+				node->addSpecialAction(std::make_shared<QuestAction>(questAction));
 			});
 		}
 
@@ -259,7 +259,7 @@ namespace AIPathfinding
 			return false;
 		}
 
-		AIPathNode * battleNode = battleNodeOptional.get();
+		auto * battleNode = battleNodeOptional.value();
 
 		if(battleNode->locked)
 		{
@@ -279,6 +279,11 @@ namespace AIPathfinding
 
 		if(loss < actualArmyValue)
 		{
+			if(destNode->specialAction)
+			{
+				battleNode->specialAction = destNode->specialAction;
+			}
+
 			destination.node = battleNode;
 			nodeStorage->commit(destination, source);
 
@@ -288,7 +293,7 @@ namespace AIPathfinding
 
 			AIPreviousNodeRule(nodeStorage).process(source, destination, pathfinderConfig, pathfinderHelper);
 
-			battleNode->specialAction = std::make_shared<BattleAction>(destination.coord);
+			battleNode->addSpecialAction(std::make_shared<BattleAction>(destination.coord));
 
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 1
 			logAi->trace(

+ 1 - 1
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.h

@@ -13,8 +13,8 @@
 #include "../AINodeStorage.h"
 #include "../../AIGateway.h"
 #include "../../../../CCallback.h"
-#include "../../../../lib/mapping/CMap.h"
 #include "../../../../lib/mapObjects/MapObjects.h"
+#include "../../../../lib/pathfinder/PathfindingRules.h"
 
 namespace NKAI
 {

+ 1 - 1
AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.cpp

@@ -31,7 +31,7 @@ namespace AIPathfinding
 			return;
 
 		if(blocker == BlockingReason::DESTINATION_BLOCKED
-			&& destination.action == CGPathNode::EMBARK
+			&& destination.action == EPathNodeAction::EMBARK
 			&& nodeStorage->getAINode(destination.node)->specialAction)
 		{
 			return;

+ 1 - 1
AI/Nullkiller/Pathfinding/Rules/AIMovementToDestinationRule.h

@@ -13,8 +13,8 @@
 #include "../AINodeStorage.h"
 #include "../../AIGateway.h"
 #include "../../../../CCallback.h"
-#include "../../../../lib/mapping/CMap.h"
 #include "../../../../lib/mapObjects/MapObjects.h"
+#include "../../../../lib/pathfinder/PathfindingRules.h"
 
 namespace NKAI
 {

+ 4 - 2
AI/Nullkiller/Pathfinding/Rules/AIPreviousNodeRule.cpp

@@ -10,6 +10,8 @@
 #include "StdInc.h"
 #include "AIPreviousNodeRule.h"
 
+#include "../../../../lib/pathfinder/CPathfinder.h"
+
 namespace NKAI
 {
 namespace AIPathfinding
@@ -25,8 +27,8 @@ namespace AIPathfinding
 		const PathfinderConfig * pathfinderConfig,
 		CPathfinderHelper * pathfinderHelper) const
 	{
-		if(source.node->action == CGPathNode::ENodeAction::BLOCKING_VISIT 
-			|| source.node->action == CGPathNode::ENodeAction::VISIT)
+		if(source.node->action == EPathNodeAction::BLOCKING_VISIT 
+			|| source.node->action == EPathNodeAction::VISIT)
 		{
 			if(source.nodeObject
 				&& isObjectPassable(source.nodeObject, pathfinderHelper->hero->tempOwner, source.objectRelations))

+ 1 - 1
AI/Nullkiller/Pathfinding/Rules/AIPreviousNodeRule.h

@@ -13,8 +13,8 @@
 #include "../AINodeStorage.h"
 #include "../../AIGateway.h"
 #include "../../../../CCallback.h"
-#include "../../../../lib/mapping/CMap.h"
 #include "../../../../lib/mapObjects/MapObjects.h"
+#include "../../../../lib/pathfinder/PathfindingRules.h"
 
 namespace NKAI
 {

+ 40 - 17
AI/StupidAI/StupidAI.cpp

@@ -18,14 +18,21 @@ static std::shared_ptr<CBattleCallback> cbc;
 
 CStupidAI::CStupidAI()
 	: side(-1)
+	, wasWaitingForRealize(false)
+	, wasUnlockingGs(false)
 {
 	print("created");
 }
 
-
 CStupidAI::~CStupidAI()
 {
 	print("destroyed");
+	if(cb)
+	{
+		//Restore previous state of CB - it may be shared with the main AI (like VCAI)
+		cb->waitTillRealize = wasWaitingForRealize;
+		cb->unlockGsWhenWaiting = wasUnlockingGs;
+	}
 }
 
 void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
@@ -33,6 +40,11 @@ void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
 	print("init called, saving ptr to IBattleCallback");
 	env = ENV;
 	cbc = cb = CB;
+
+	wasWaitingForRealize = CB->waitTillRealize;
+	wasUnlockingGs = CB->unlockGsWhenWaiting;
+	CB->waitTillRealize = false;
+	CB->unlockGsWhenWaiting = false;
 }
 
 void CStupidAI::actionFinished(const BattleAction &action)
@@ -80,7 +92,7 @@ static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const Battl
 	for(int i = 0; i < 2; i++)
 	{
 		for (auto & neighbour : (i ? h2 : h1).neighbouringTiles())
-			if(const CStack * s = cbc->battleGetStackByPos(neighbour))
+			if(const auto * s = cbc->battleGetUnitByPos(neighbour))
 				if(s->isShooter())
 					shooters[i]++;
 	}
@@ -88,14 +100,19 @@ static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const Battl
 	return shooters[0] < shooters[1];
 }
 
-BattleAction CStupidAI::activeStack( const CStack * stack )
+void CStupidAI::yourTacticPhase(int distance)
+{
+	cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide()));
+}
+
+void CStupidAI::activeStack( const CStack * stack )
 {
 	//boost::this_thread::sleep(boost::posix_time::seconds(2));
 	print("activeStack called for " + stack->nodeName());
 	ReachabilityInfo dists = cb->getReachability(stack);
 	std::vector<EnemyInfo> enemiesShootable, enemiesReachable, enemiesUnreachable;
 
-	if(stack->type->idNumber == CreatureID::CATAPULT)
+	if(stack->creatureId() == CreatureID::CATAPULT)
 	{
 		BattleAction attack;
 		static const std::vector<int> wallHexes = {50, 183, 182, 130, 78, 29, 12, 95};
@@ -103,13 +120,15 @@ BattleAction CStupidAI::activeStack( const CStack * stack )
 		attack.aimToHex(seletectedHex);
 		attack.actionType = EActionType::CATAPULT;
 		attack.side = side;
-		attack.stackNumber = stack->ID;
+		attack.stackNumber = stack->unitId();
 
-		return attack;
+		cb->battleMakeUnitAction(attack);
+		return;
 	}
-	else if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON))
+	else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON))
 	{
-		return BattleAction::makeDefend(stack);
+		cb->battleMakeUnitAction(BattleAction::makeDefend(stack));
+		return;
 	}
 
 	for (const CStack *s : cb->battleGetStacks(CBattleCallback::ONLY_ENEMY))
@@ -120,7 +139,7 @@ BattleAction CStupidAI::activeStack( const CStack * stack )
 		}
 		else
 		{
-			std::vector<BattleHex> avHexes = cb->battleGetAvailableHexes(stack);
+			std::vector<BattleHex> avHexes = cb->battleGetAvailableHexes(stack, false);
 
 			for (BattleHex hex : avHexes)
 			{
@@ -151,12 +170,14 @@ BattleAction CStupidAI::activeStack( const CStack * stack )
 	if(enemiesShootable.size())
 	{
 		const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable);
-		return BattleAction::makeShotAttack(stack, ei.s);
+		cb->battleMakeUnitAction(BattleAction::makeShotAttack(stack, ei.s));
+		return;
 	}
 	else if(enemiesReachable.size())
 	{
 		const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable);
-		return BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), &willSecondHexBlockMoreEnemyShooters));
+		cb->battleMakeUnitAction(BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), &willSecondHexBlockMoreEnemyShooters)));
+		return;
 	}
 	else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies
 	{
@@ -167,11 +188,13 @@ BattleAction CStupidAI::activeStack( const CStack * stack )
 
 		if(dists.distToNearestNeighbour(stack, closestEnemy->s) < GameConstants::BFIELD_SIZE)
 		{
-			return goTowards(stack, closestEnemy->s->getAttackableHexes(stack));
+			cb->battleMakeUnitAction(goTowards(stack, closestEnemy->s->getAttackableHexes(stack)));
+			return;
 		}
 	}
 
-	return BattleAction::makeDefend(stack);
+	cb->battleMakeUnitAction(BattleAction::makeDefend(stack));
+	return;
 }
 
 void CStupidAI::battleAttack(const BattleAttack *ba)
@@ -184,7 +207,7 @@ void CStupidAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bs
 	print("battleStacksAttacked called");
 }
 
-void CStupidAI::battleEnd(const BattleResult *br)
+void CStupidAI::battleEnd(const BattleResult *br, QueryID queryID)
 {
 	print("battleEnd called");
 }
@@ -219,7 +242,7 @@ void CStupidAI::battleStacksEffectsSet(const SetStackEffect & sse)
 	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");
 	side = Side;
@@ -238,7 +261,7 @@ void CStupidAI::print(const std::string &text) const
 BattleAction CStupidAI::goTowards(const CStack * stack, std::vector<BattleHex> hexes) const
 {
 	auto reachability = cb->getReachability(stack);
-	auto avHexes = cb->battleGetAvailableHexes(reachability, stack);
+	auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false);
 
 	if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
 	{
@@ -270,7 +293,7 @@ BattleAction CStupidAI::goTowards(const CStack * stack, std::vector<BattleHex> h
 		return BattleAction::makeDefend(stack);
 	}
 
-	if(stack->hasBonusOfType(Bonus::FLYING))
+	if(stack->hasBonusOfType(BonusType::FLYING))
 	{
 		// Flying stack doesn't go hex by hex, so we can't backtrack using predecessors.
 		// We just check all available hexes and pick the one closest to the target.

Някои файлове не бяха показани, защото твърде много файлове са промени