Browse Source

Merge branch 'beta' into map_editor_open_recent-v2

Ivan Savenko 11 months ago
parent
commit
c5eb285a08
100 changed files with 1517 additions and 445 deletions
  1. 5 3
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 6 0
      .github/workflows/github.yml
  3. 26 7
      AI/BattleAI/BattleEvaluator.cpp
  4. 1 1
      AI/BattleAI/BattleExchangeVariant.cpp
  5. 5 7
      AI/Nullkiller/AIGateway.cpp
  6. 5 5
      AI/Nullkiller/AIUtility.cpp
  7. 2 7
      AI/Nullkiller/AIUtility.h
  8. 9 16
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  9. 31 12
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  10. 4 3
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  11. 2 0
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h
  12. 13 11
      AI/Nullkiller/Analyzers/HeroManager.cpp
  13. 2 2
      AI/Nullkiller/Analyzers/HeroManager.h
  14. 11 6
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  15. 35 12
      AI/Nullkiller/Behaviors/BuildingBehavior.cpp
  16. 9 8
      AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp
  17. 4 9
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp
  18. 27 20
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  19. 13 29
      AI/Nullkiller/Behaviors/ExplorationBehavior.cpp
  20. 6 31
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  21. 64 25
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  22. 1 10
      AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp
  23. 1 2
      AI/Nullkiller/Engine/FuzzyEngines.cpp
  24. 12 3
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  25. 174 26
      AI/Nullkiller/Engine/Nullkiller.cpp
  26. 2 1
      AI/Nullkiller/Engine/Nullkiller.h
  27. 403 48
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  28. 30 2
      AI/Nullkiller/Engine/PriorityEvaluator.h
  29. 31 43
      AI/Nullkiller/Engine/Settings.cpp
  30. 17 1
      AI/Nullkiller/Engine/Settings.h
  31. 1 0
      AI/Nullkiller/Goals/AbstractGoal.h
  32. 3 0
      AI/Nullkiller/Goals/AdventureSpellCast.cpp
  33. 5 2
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp
  34. 2 0
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  35. 1 0
      AI/Nullkiller/Goals/RecruitHero.cpp
  36. 1 0
      AI/Nullkiller/Goals/RecruitHero.h
  37. 2 6
      AI/Nullkiller/Goals/StayAtTown.cpp
  38. 1 1
      AI/Nullkiller/Helpers/ExplorationHelper.cpp
  39. 31 11
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  40. 5 5
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  41. 1 1
      AI/Nullkiller/Pathfinding/Actors.cpp
  42. 0 2
      AI/VCAI/AIUtility.h
  43. 0 2
      AI/VCAI/ResourceManager.cpp
  44. 0 2
      AI/VCAI/VCAI.cpp
  45. 278 0
      CI/example.markdownlint-cli2.jsonc
  46. 227 63
      ChangeLog.md
  47. 0 0
      Mods/vcmi/Content/Data/NotoSans-Medium.ttf
  48. 0 0
      Mods/vcmi/Content/Data/NotoSerif-Black.ttf
  49. 0 0
      Mods/vcmi/Content/Data/NotoSerif-Bold.ttf
  50. 0 0
      Mods/vcmi/Content/Data/NotoSerif-Medium.ttf
  51. 0 0
      Mods/vcmi/Content/Data/s/std.verm
  52. 0 0
      Mods/vcmi/Content/Data/s/testy.erm
  53. 0 0
      Mods/vcmi/Content/Sounds/we5.wav
  54. 0 0
      Mods/vcmi/Content/Sprites/PortraitsLarge.json
  55. 0 0
      Mods/vcmi/Content/Sprites/PortraitsSmall.json
  56. 0 0
      Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/CreaturePurchaseCard.png
  57. 0 0
      Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/QuickRecruitmentAllButton.def
  58. 0 0
      Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/QuickRecruitmentNoneButton.def
  59. 0 0
      Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/costBackground.png
  60. 0 0
      Mods/vcmi/Content/Sprites/ScSelC.json
  61. 0 0
      Mods/vcmi/Content/Sprites/StackQueueLarge.png
  62. 0 0
      Mods/vcmi/Content/Sprites/StackQueueSmall.png
  63. 0 0
      Mods/vcmi/Content/Sprites/UnitMaxMovementHighlight.png
  64. 0 0
      Mods/vcmi/Content/Sprites/UnitMovementHighlight.png
  65. BIN
      Mods/vcmi/Content/Sprites/battle/queueDefend.png
  66. BIN
      Mods/vcmi/Content/Sprites/battle/queueWait.png
  67. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/empty.png
  68. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/fullHex.png
  69. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/left.png
  70. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/leftHalf.png
  71. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/top.png
  72. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeft.png
  73. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeftCorner.png
  74. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeftHalfCorner.png
  75. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json
  76. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsRed.json
  77. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/empty.png
  78. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/fullHex.png
  79. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/left.png
  80. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/leftHalf.png
  81. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/top.png
  82. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeft.png
  83. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeftCorner.png
  84. 0 0
      Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeftHalfCorner.png
  85. 8 0
      Mods/vcmi/Content/Sprites/cprsmall.json
  86. 0 0
      Mods/vcmi/Content/Sprites/debug/blocked.png
  87. 0 0
      Mods/vcmi/Content/Sprites/debug/grid.png
  88. 0 0
      Mods/vcmi/Content/Sprites/debug/spellRange.png
  89. 0 0
      Mods/vcmi/Content/Sprites/debug/visitable.png
  90. 0 0
      Mods/vcmi/Content/Sprites/heroWindow/artifactSlotEmpty.png
  91. 0 0
      Mods/vcmi/Content/Sprites/heroWindow/backpackButtonIcon.png
  92. 0 0
      Mods/vcmi/Content/Sprites/heroWindow/commanderButtonIcon.png
  93. 0 0
      Mods/vcmi/Content/Sprites/itpa.json
  94. 0 0
      Mods/vcmi/Content/Sprites/lobby/checkbox.json
  95. 0 0
      Mods/vcmi/Content/Sprites/lobby/checkboxBlueOff.png
  96. 0 0
      Mods/vcmi/Content/Sprites/lobby/checkboxBlueOn.png
  97. 0 0
      Mods/vcmi/Content/Sprites/lobby/checkboxOff.png
  98. 0 0
      Mods/vcmi/Content/Sprites/lobby/checkboxOn.png
  99. 0 0
      Mods/vcmi/Content/Sprites/lobby/delete-normal.png
  100. 0 0
      Mods/vcmi/Content/Sprites/lobby/delete-pressed.png

+ 5 - 3
.github/ISSUE_TEMPLATE/bug_report.md

@@ -15,6 +15,7 @@ Please attach game logs: `VCMI_client.txt`, `VCMI_server.txt` etc.
 
 **To Reproduce**
 Steps to reproduce the behavior:
+
 1. Go to '...'
 2. Click on '....'
 3. Scroll down to '....'
@@ -24,7 +25,7 @@ Steps to reproduce the behavior:
 A clear and concise description of what you expected to happen.
 
 **Actual behavior**
-A clear description what is currently happening 
+A clear description what is currently happening
 
 **Did it work earlier?**
 If this something which worked well some time ago, please let us know about version where it works or at date when it worked.
@@ -33,8 +34,9 @@ If this something which worked well some time ago, please let us know about vers
 If applicable, add screenshots to help explain your problem.
 
 **Version**
- - OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
- - Version: [VCMI version]
+
+- OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
+- Version: [VCMI version]
 
 **Additional context**
 Add any other context about the problem here.

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

@@ -402,3 +402,9 @@ jobs:
           run: |
             sudo apt install python3-jstyleson
             python3 CI/validate_json.py
+
+        - name: Validate Markdown
+          uses: DavidAnson/markdownlint-cli2-action@v18
+          with:
+            config: 'CI/example.markdownlint-cli2.jsonc'
+            globs: '**/*.md'

+ 26 - 7
AI/BattleAI/BattleEvaluator.cpp

@@ -675,7 +675,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 				spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
 				cast.castEval(state->getServerCallback(), ps.dest);
 
-				auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; });
+				auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->isValidTarget(); });
 
 				auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
 					{
@@ -731,7 +731,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 					ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
 				}
-
 				for(const auto & unit : allUnits)
 				{
 					if(!unit->isValidTarget(true))
@@ -771,11 +770,31 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 							ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
 
 #if BATTLE_TRACE_LEVEL >= 1
-						logAi->trace(
-							"Spell affects %s (%d), dps: %2f",
-							unit->creatureId().toCreature()->getNameSingularTranslated(),
-							unit->getCount(),
-							dpsReduce);
+						// Ensure ps.dest is not empty before accessing the first element
+						if (!ps.dest.empty()) 
+						{
+							logAi->trace(
+								"Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
+								ps.spell->getNameTranslated(),
+								ps.dest.at(0).hexValue.hex,  // Safe to access .at(0) now
+								unit->creatureId().toCreature()->getNameSingularTranslated(),
+								unit->getCount(),
+								dpsReduce,
+								oldHealth,
+								newHealth);
+						}
+						else 
+						{
+							// Handle the case where ps.dest is empty
+							logAi->trace(
+								"Spell %s has no destination, affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
+								ps.spell->getNameTranslated(),
+								unit->creatureId().toCreature()->getNameSingularTranslated(),
+								unit->getCount(),
+								dpsReduce,
+								oldHealth,
+								newHealth);
+						}
 #endif
 					}
 				}

+ 1 - 1
AI/BattleAI/BattleExchangeVariant.cpp

@@ -906,7 +906,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
 {
 	std::vector<const battle::Unit *> result;
 
-	for(int i = 0; i < turnOrder.size(); i++)
+	for(int i = 0; i < turnOrder.size(); i++, turn++)
 	{
 		auto & turnQueue = turnOrder[i];
 		HypotheticBattle turnBattle(env.get(), cb);

+ 5 - 7
AI/Nullkiller/AIGateway.cpp

@@ -34,11 +34,6 @@
 namespace NKAI
 {
 
-// our to enemy strength ratio constants
-const float SAFE_ATTACK_CONSTANT = 1.1f;
-const float RETREAT_THRESHOLD = 0.3f;
-const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
-
 //one thread may be turn of AI and another will be handling a side effect for AI2
 thread_local CCallback * cb = nullptr;
 thread_local AIGateway * ai = nullptr;
@@ -286,6 +281,9 @@ void AIGateway::tileRevealed(const std::unordered_set<int3> & pos)
 		for(const CGObjectInstance * obj : myCb->getVisitableObjs(tile))
 			addVisitableObj(obj);
 	}
+
+	if (nullkiller->settings->isUpdateHitmapOnTileReveal())
+		nullkiller->dangerHitMap->reset();
 }
 
 void AIGateway::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query)
@@ -553,7 +551,7 @@ std::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(const Battle
 	double fightRatio = ourStrength / (double)battleState.getEnemyStrength();
 
 	// if we have no towns - things are already bad, so retreat is not an option.
-	if(cb->getTownsInfo().size() && ourStrength < RETREAT_ABSOLUTE_THRESHOLD && fightRatio < RETREAT_THRESHOLD && battleState.canFlee)
+	if(cb->getTownsInfo().size() && ourStrength < nullkiller->settings->getRetreatThresholdAbsolute() && fightRatio < nullkiller->settings->getRetreatThresholdRelative() && battleState.canFlee)
 	{
 		return BattleAction::makeRetreat(battleState.ourSide);
 	}
@@ -670,7 +668,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
 				else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
 				{
 					bool dangerUnknown = danger == 0;
-					bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT);
+					bool dangerTooHigh = ratio * nullkiller->settings->getSafeAttackRatio() > 1;
 
 					answer = !dangerUnknown && !dangerTooHigh;
 				}

+ 5 - 5
AI/Nullkiller/AIUtility.cpp

@@ -146,21 +146,21 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
 	return h == rhs.get(true);
 }
 
-bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength)
+bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength, float safeAttackRatio)
 {
-	const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength();
+	const ui64 heroStrength = h->getHeroStrength() * heroArmy->getArmyStrength();
 
 	if(dangerStrength)
 	{
-		return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength;
+		return heroStrength > dangerStrength * safeAttackRatio;
 	}
 
 	return true; //there's no danger
 }
 
-bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength)
+bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio)
 {
-	return isSafeToVisit(h, h, dangerStrength);
+	return isSafeToVisit(h, h, dangerStrength, safeAttackRatio);
 }
 
 bool isObjectRemovable(const CGObjectInstance * obj)

+ 2 - 7
AI/Nullkiller/AIUtility.h

@@ -61,11 +61,6 @@ const int GOLD_MINE_PRODUCTION = 1000;
 const int WOOD_ORE_MINE_PRODUCTION = 2;
 const int RESOURCE_MINE_PRODUCTION = 1;
 const int ACTUAL_RESOURCE_COUNT = 7;
-const int ALLOWED_ROAMING_HEROES = 8;
-
-//implementation-dependent
-extern const float SAFE_ATTACK_CONSTANT;
-extern const int GOLD_RESERVE;
 
 extern thread_local CCallback * cb;
 
@@ -213,8 +208,8 @@ bool isBlockVisitObj(const int3 & pos);
 bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj);
 
 bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property!
-bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength);
-bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength);
+bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio);
+bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength, float safeAttackRatio);
 
 bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2);
 bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2);

+ 9 - 16
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -13,6 +13,7 @@
 #include "../Engine/Nullkiller.h"
 #include "../../../CCallback.h"
 #include "../../../lib/mapObjects/MapObjects.h"
+#include "../../../lib/IGameSettings.h"
 #include "../../../lib/GameConstants.h"
 
 namespace NKAI
@@ -152,16 +153,6 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 	uint64_t armyValue = 0;
 
 	TemporaryArmy newArmyInstance;
-	auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE));
-
-	for(auto bonus : *bonusModifiers)
-	{
-		// army bonuses will change and object bonuses are temporary
-		if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE)
-		{
-			newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
-		}
-	}
 
 	while(allowedFactions.size() < alignmentMap.size())
 	{
@@ -197,16 +188,18 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 			auto morale = slot.second->moraleVal();
 			auto multiplier = 1.0f;
 
-			const float BadMoraleChance = 0.083f;
-			const float HighMoraleChance = 0.04f;
+			const auto & badMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_DICE);
+			const auto & highMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE);
 
-			if(morale < 0)
+			if(morale < 0 && !badMoraleDice.empty())
 			{
-				multiplier += morale * BadMoraleChance;
+				size_t diceIndex = std::min<size_t>(badMoraleDice.size(), -morale) - 1;
+				multiplier -= 1.0 / badMoraleDice.at(diceIndex);
 			}
-			else if(morale > 0)
+			else if(morale > 0 && !highMoraleDice.empty())
 			{
-				multiplier += morale * HighMoraleChance;
+				size_t diceIndex = std::min<size_t>(highMoraleDice.size(), morale) - 1;
+				multiplier += 1.0 / highMoraleDice.at(diceIndex);
 			}
 
 			newValue += multiplier * slot.second->getPower();

+ 31 - 12
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -39,7 +39,6 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
 		for(int upgradeIndex : {1, 0})
 		{
 			BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
-
 			if(!vstd::contains(buildings, building))
 				continue; // no such building in town
 
@@ -73,11 +72,18 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
 
 	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});
 	}
 
+	otherBuildings.push_back({ BuildingID::CITADEL, BuildingID::CASTLE });
+	otherBuildings.push_back({ BuildingID::RESOURCE_SILO });
+	otherBuildings.push_back({ BuildingID::SPECIAL_1 });
+	otherBuildings.push_back({ BuildingID::SPECIAL_2 });
+	otherBuildings.push_back({ BuildingID::SPECIAL_3 });
+	otherBuildings.push_back({ BuildingID::SPECIAL_4 });
+	otherBuildings.push_back({ BuildingID::MARKETPLACE });
+
 	for(auto & buildingSet : otherBuildings)
 	{
 		for(auto & buildingID : buildingSet)
@@ -141,6 +147,8 @@ void BuildAnalyzer::update()
 
 	auto towns = ai->cb->getTownsInfo();
 
+	float economyDevelopmentCost = 0;
+
 	for(const CGTownInstance* town : towns)
 	{
 		logAi->trace("Checking town %s", town->getNameTranslated());
@@ -153,6 +161,11 @@ void BuildAnalyzer::update()
 
 		requiredResources += developmentInfo.requiredResources;
 		totalDevelopmentCost += developmentInfo.townDevelopmentCost;
+		for(auto building : developmentInfo.toBuild)
+		{
+			if (building.dailyIncome[EGameResID::GOLD] > 0)
+				economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD];
+		}
 		armyCost += developmentInfo.armyCost;
 
 		for(auto bi : developmentInfo.toBuild)
@@ -171,15 +184,7 @@ void BuildAnalyzer::update()
 
 	updateDailyIncome();
 
-	if(ai->cb->getDate(Date::DAY) == 1)
-	{
-		goldPressure = 1;
-	}
-	else
-	{
-		goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
-			+ (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
-	}
+	goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
 
 	logAi->trace("Gold pressure: %f", goldPressure);
 }
@@ -237,6 +242,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 	logAi->trace("checking %s", info.name);
 	logAi->trace("buildInfo %s", info.toString());
 
+	int highestFort = 0;
+	for (auto twn : ai->cb->getTownsInfo())
+	{
+		highestFort = std::max(highestFort, (int)twn->fortLevel());
+	}
+
 	if(!town->hasBuilt(building))
 	{
 		auto canBuild = ai->cb->canBuildStructure(town, building);
@@ -281,7 +292,15 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 				prerequisite.baseCreatureID = info.baseCreatureID;
 				prerequisite.prerequisitesCount++;
 				prerequisite.armyCost = info.armyCost;
-				prerequisite.dailyIncome = info.dailyIncome;
+				bool haveSameOrBetterFort = false;
+				if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT)
+					haveSameOrBetterFort = true;
+				if (prerequisite.id == BuildingID::CITADEL && highestFort >= CGTownInstance::EFortLevel::CITADEL)
+					haveSameOrBetterFort = true;
+				if (prerequisite.id == BuildingID::CASTLE && highestFort >= CGTownInstance::EFortLevel::CASTLE)
+					haveSameOrBetterFort = true;
+				if(!haveSameOrBetterFort)
+					prerequisite.dailyIncome = info.dailyIncome;
 
 				return prerequisite;
 			}

+ 4 - 3
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -89,7 +89,6 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 			heroes[hero->tempOwner][hero] = HeroRole::MAIN;
 		}
-
 		if(obj->ID == Obj::TOWN)
 		{
 			auto town = dynamic_cast<const CGTownInstance *>(obj);
@@ -140,6 +139,7 @@ void DangerHitMapAnalyzer::updateHitMap()
 
 				newThreat.hero = path.targetHero;
 				newThreat.turn = path.turn();
+				newThreat.threat = path.getHeroStrength() * (1 - path.movementCost() / 2.0);
 				newThreat.danger = path.getHeroStrength();
 
 				if(newThreat.value() > node.maximumDanger.value())
@@ -316,8 +316,8 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath &
 
 	const auto& info = getTileThreat(tile);
 	
-	return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger))
-		|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger));
+	return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger, ai->settings->getSafeAttackRatio()))
+		|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger, ai->settings->getSafeAttackRatio()));
 }
 
 const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance * obj) const
@@ -348,6 +348,7 @@ std::set<const CGObjectInstance *> DangerHitMapAnalyzer::getOneTurnAccessibleObj
 void DangerHitMapAnalyzer::reset()
 {
 	hitMapUpToDate = false;
+	tileOwnersUpToDate = false;
 }
 
 }

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

@@ -22,6 +22,7 @@ struct HitMapInfo
 
 	uint64_t danger;
 	uint8_t turn;
+	float threat;
 	HeroPtr hero;
 
 	HitMapInfo()
@@ -33,6 +34,7 @@ struct HitMapInfo
 	{
 		danger = 0;
 		turn = 255;
+		threat = 0;
 		hero = HeroPtr();
 	}
 

+ 13 - 11
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -95,7 +95,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
 
 float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
 {
-	return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f;
+	return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->getBasePrimarySkillValue(PrimarySkill::ATTACK) + hero->getBasePrimarySkillValue(PrimarySkill::DEFENSE) + hero->getBasePrimarySkillValue(PrimarySkill::SPELL_POWER) + hero->getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE);
 }
 
 void HeroManager::update()
@@ -108,7 +108,7 @@ void HeroManager::update()
 	for(auto & hero : myHeroes)
 	{
 		scores[hero] = evaluateFightingStrength(hero);
-		knownFightingStrength[hero->id] = hero->getFightingStrength();
+		knownFightingStrength[hero->id] = hero->getHeroStrength();
 	}
 
 	auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
@@ -147,7 +147,10 @@ void HeroManager::update()
 
 HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const
 {
-	return heroRoles.at(hero);
+	if (heroRoles.find(hero) != heroRoles.end())
+		return heroRoles.at(hero);
+	else
+		return HeroRole::SCOUT;
 }
 
 const std::map<HeroPtr, HeroRole> & HeroManager::getHeroRoles() const
@@ -188,13 +191,11 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
 	return evaluateFightingStrength(hero);
 }
 
-bool HeroManager::heroCapReached() const
+bool HeroManager::heroCapReached(bool includeGarrisoned) const
 {
-	const bool includeGarnisoned = true;
-	int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
+	int heroCount = cb->getHeroCount(ai->playerID, includeGarrisoned);
 
-	return heroCount >= ALLOWED_ROAMING_HEROES
-		|| heroCount >= ai->settings->getMaxRoamingHeroes()
+	return heroCount >= ai->settings->getMaxRoamingHeroes()
 		|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)
 		|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
 }
@@ -204,7 +205,7 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
 	auto cached = knownFightingStrength.find(hero->id);
 
 	//FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?)
-	return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength();
+	return cached != knownFightingStrength.end() ? cached->second : hero->getHeroStrength();
 }
 
 float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
@@ -281,7 +282,7 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const
 	return nullptr;
 }
 
-const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const
+const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance* townToSpare) const
 {
 	const CGHeroInstance * weakestHero = nullptr;
 	auto myHeroes = ai->cb->getHeroesInfo();
@@ -292,12 +293,13 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co
 			|| existingHero->getArmyStrength() >armyLimit
 			|| getHeroRole(existingHero) == HeroRole::MAIN
 			|| existingHero->movementPointsRemaining()
+			|| (townToSpare != nullptr && existingHero->visitedTown == townToSpare)
 			|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
 		{
 			continue;
 		}
 
-		if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
+		if(!weakestHero || weakestHero->getHeroStrength() > existingHero->getHeroStrength())
 		{
 			weakestHero = existingHero;
 		}

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

@@ -56,9 +56,9 @@ public:
 	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
 	float evaluateHero(const CGHeroInstance * hero) const;
 	bool canRecruitHero(const CGTownInstance * t = nullptr) const;
-	bool heroCapReached() const;
+	bool heroCapReached(bool includeGarrisoned = true) const;
 	const CGHeroInstance * findHeroWithGrail() const;
-	const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const;
+	const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance * townToSpare = nullptr) const;
 	float getMagicStrength(const CGHeroInstance * hero) const;
 	float getFightingStrengthCached(const CGHeroInstance * hero) const;
 

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

@@ -97,9 +97,10 @@ std::optional<const CGObjectInstance *> ObjectClusterizer::getBlocker(const AIPa
 	{
 		auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord);
 
-		blockers = ai->cb->getVisitableObjs(node.coord);
+		if (ai->cb->isVisible(node.coord))
+			blockers = ai->cb->getVisitableObjs(node.coord);
 
-		if(guardPos.valid())
+		if(guardPos.valid() && ai->cb->isVisible(guardPos))
 		{
 			auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord));
 
@@ -474,9 +475,11 @@ void ObjectClusterizer::clusterizeObject(
 
 				heroesProcessed.insert(path.targetHero);
 
-				float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
+				float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
 
-				if(priority < MIN_PRIORITY)
+				if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
+					continue;
+				else if (priority <= 0)
 					continue;
 
 				ClusterMap::accessor cluster;
@@ -495,9 +498,11 @@ void ObjectClusterizer::clusterizeObject(
 
 		heroesProcessed.insert(path.targetHero);
 
-		float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
+		float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
 
-		if(priority < MIN_PRIORITY)
+		if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
+			continue;
+		else if (priority <= 0)
 			continue;
 
 		bool interestingObject = path.turn() <= 2 || priority > 0.5f;

+ 35 - 12
AI/Nullkiller/Behaviors/BuildingBehavior.cpp

@@ -49,26 +49,49 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
 	auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo();
 	auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh();
 
+	ai->dangerHitMap->updateHitMap();
+
 	for(auto & developmentInfo : developmentInfos)
 	{
-		for(auto & buildingInfo : developmentInfo.toBuild)
+		bool emergencyDefense = false;
+		uint8_t closestThreat = std::numeric_limits<uint8_t>::max();
+		for (auto threat : ai->dangerHitMap->getTownThreats(developmentInfo.town))
+		{
+			closestThreat = std::min(closestThreat, threat.turn);
+		}
+		for (auto& buildingInfo : developmentInfo.toBuild)
 		{
-			if(isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
+			if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes)
 			{
-				if(buildingInfo.notEnoughRes)
+				if (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE)
 				{
-					if(ai->getLockedResources().canAfford(buildingInfo.buildCost))
-						continue;
-
-					Composition composition;
+					tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
+					emergencyDefense = true;
+				}
+			}
+		}
+		if (!emergencyDefense)
+		{
+			for (auto& buildingInfo : developmentInfo.toBuild)
+			{
+				if (isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
+				{
+					if (buildingInfo.notEnoughRes)
+					{
+						if (ai->getLockedResources().canAfford(buildingInfo.buildCost))
+							continue;
 
-					composition.addNext(BuildThis(buildingInfo, developmentInfo));
-					composition.addNext(SaveResources(buildingInfo.buildCost));
+						Composition composition;
 
-					tasks.push_back(sptr(composition));
+						composition.addNext(BuildThis(buildingInfo, developmentInfo));
+						composition.addNext(SaveResources(buildingInfo.buildCost));
+						tasks.push_back(sptr(composition));
+					}
+					else
+					{
+						tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
+					}
 				}
-				else
-					tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
 			}
 		}
 	}

+ 9 - 8
AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp

@@ -28,9 +28,6 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
 
-	if(ai->cb->getDate(Date::DAY) == 1)
-		return tasks;
-		
 	auto heroes = cb->getHeroesInfo();
 
 	if(heroes.empty())
@@ -38,19 +35,23 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
 		return tasks;
 	}
 
+	ai->dangerHitMap->updateHitMap();
+
 	for(auto town : cb->getTownsInfo())
 	{
+		uint8_t closestThreat = ai->dangerHitMap->getTileThreat(town->visitablePos()).fastestDanger.turn;
+
+		if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN)
+		{
+			return tasks;
+		}
+		
 		auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet(
 			town,
 			ai->getFreeResources());
 
 		for(const CGHeroInstance * targetHero : heroes)
 		{
-			if(ai->buildAnalyzer->isGoldPressureHigh()	&& !town->hasBuilt(BuildingID::CITY_HALL))
-			{
-				continue;
-			}
-
 			if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN)
 			{
 				auto reinforcement = ai->armyManager->howManyReinforcementsCanGet(

+ 4 - 9
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

@@ -68,14 +68,6 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
 		logAi->trace("Path found %s", path.toString());
 #endif
 
-		if(nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
-		{
-#if NKAI_TRACE_LEVEL >= 2
-			logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength());
-#endif
-			continue;
-		}
-
 		if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit))
 		{
 #if NKAI_TRACE_LEVEL >= 2
@@ -87,6 +79,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
 		auto hero = path.targetHero;
 		auto danger = path.getTotalDanger();
 
+		if (hero->getOwner() != nullkiller->playerID)
+			continue;
+
 		if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
 			&& (path.getTotalDanger() == 0 || path.turn() > 0)
 			&& path.exchangeCount > 1)
@@ -119,7 +114,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
 			continue;
 		}
 
-		auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
+		auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, nullkiller->settings->getSafeAttackRatio());
 
 #if NKAI_TRACE_LEVEL >= 2
 		logAi->trace(

+ 27 - 20
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -41,6 +41,9 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const
 	for(auto town : ai->cb->getTownsInfo())
 	{
 		evaluateDefence(tasks, town, ai);
+		//Let's do only one defence-task per pass since otherwise it can try to hire the same hero twice
+		if (!tasks.empty())
+			break;
 	}
 
 	return tasks;
@@ -130,7 +133,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
 
 			tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
 
-			return true;
+			return false;
 		}
 		else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
 		{
@@ -141,7 +144,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
 			{
 				tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
 
-				return true;
+				return false;
 			}
 		}
 	}
@@ -158,11 +161,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	
 	threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there
 
-	if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
+	if (town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
 	{
 		return;
 	}
-
 	if(!threatNode.fastestDanger.hero)
 	{
 		logAi->trace("No threat found for town %s", town->getNameTranslated());
@@ -250,6 +252,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				continue;
 			}
 
+			if (!path.targetHero->canBeMergedWith(*town))
+			{
+#if NKAI_TRACE_LEVEL >= 1
+				logAi->trace("Can't merge armies of hero %s and town %s",
+					path.targetHero->getObjectName(),
+					town->getObjectName());
+#endif
+				continue;
+			}
+
 			if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
 			{
 #if NKAI_TRACE_LEVEL >= 1
@@ -261,6 +273,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				// dismiss creatures we are not able to pick to be able to hide in garrison
 				if(town->garrisonHero
 					|| town->getUpperArmy()->stacksCount() == 0
+					|| path.targetHero->canBeMergedWith(*town)
 					|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
 				{
 					tasks.push_back(
@@ -292,7 +305,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 				continue;
 			}
 				
-			if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= threat.danger))
+			if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * ai->settings->getSafeAttackRatio() >= threat.danger))
 			{
 				if(ai->arePathHeroesLocked(path))
 				{
@@ -343,23 +356,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 			}
 			else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
 			{
-				if(town->garrisonHero)
+				if(town->garrisonHero && town->garrisonHero != path.targetHero)
 				{
-					if(ai->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());
+					logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
+						path.targetHero->getObjectName(),
+						town->getObjectName());
 #endif
-						continue;
-					}
+					continue;
 				}
 				else if(path.turn() == 0)
 				{
@@ -405,6 +409,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const
 {
+	if (threat.turn > 0 || town->garrisonHero || town->visitingHero)
+		return;
+	
 	if(town->hasBuilt(BuildingID::TAVERN)
 		&& ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
 	{
@@ -451,7 +458,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
 			}
 			else if(ai->heroManager->heroCapReached())
 			{
-				heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength());
+				heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town);
 
 				if(!heroToDismiss)
 					continue;

+ 13 - 29
AI/Nullkiller/Behaviors/ExplorationBehavior.cpp

@@ -33,48 +33,32 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
 {
 	Goals::TGoalVec tasks;
 
-	for(auto obj : ai->memory->visitableObjs)
+	for (auto obj : ai->memory->visitableObjs)
 	{
-		if(!vstd::contains(ai->memory->alreadyVisited, obj))
+		switch (obj->ID.num)
 		{
-			switch(obj->ID.num)
-			{
 			case Obj::REDWOOD_OBSERVATORY:
 			case Obj::PILLAR_OF_FIRE:
-				tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
+			{
+				auto rObj = dynamic_cast<const CRewardableObject*>(obj);
+				if (!rObj->wasScouted(ai->playerID))
+					tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
 				break;
+			}
 			case Obj::MONOLITH_ONE_WAY_ENTRANCE:
 			case Obj::MONOLITH_TWO_WAY:
 			case Obj::SUBTERRANEAN_GATE:
 			case Obj::WHIRLPOOL:
-				auto tObj = dynamic_cast<const CGTeleport *>(obj);
-				if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability)
-				{
-					tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
-				}
-				break;
-			}
-		}
-		else
-		{
-			switch(obj->ID.num)
 			{
-			case Obj::MONOLITH_TWO_WAY:
-			case Obj::SUBTERRANEAN_GATE:
-			case Obj::WHIRLPOOL:
-				auto tObj = dynamic_cast<const CGTeleport *>(obj);
-				if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability)
-					break;
-				for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits)
+				auto tObj = dynamic_cast<const CGTeleport*>(obj);
+				for (auto exit : cb->getTeleportChannelExits(tObj->channel))
 				{
-					if(!cb->getObj(exit))
-					{ 
-						// Always attempt to visit two-way teleports if one of channel exits is not visible
-						tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
-						break;
+					if (exit != tObj->id)
+					{
+						if (!cb->isVisible(cb->getObjInstance(exit)))
+							tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
 					}
 				}
-				break;
 			}
 		}
 	}

+ 6 - 31
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

@@ -81,6 +81,9 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
 		logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
 #endif
 		
+		if (path.targetHero->getOwner() != ai->playerID)
+			continue;
+		
 		if(path.containsHero(hero))
 		{
 #if NKAI_TRACE_LEVEL >= 2
@@ -89,14 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
 			continue;
 		}
 
-		if(path.turn() > 0 && ai->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());
-#endif
-			continue;
-		}
-
 		if(ai->arePathHeroesLocked(path))
 		{
 #if NKAI_TRACE_LEVEL >= 2
@@ -150,7 +145,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
 		}
 
 		auto danger = path.getTotalDanger();
-		auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
+		auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, ai->settings->getSafeAttackRatio());
 
 #if NKAI_TRACE_LEVEL >= 2
 		logAi->trace(
@@ -292,17 +287,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
 			continue;
 		}
 
-		auto heroRole = ai->heroManager->getHeroRole(path.targetHero);
-
-		if(heroRole == HeroRole::SCOUT
-			&& ai->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());
-#endif
-			continue;
-		}
-
 		auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
 
 		if(!upgrader->garrisonHero
@@ -320,14 +304,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
 
 			armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
 
-			armyToGetOrBuy.addArmyToBuy(
-				ai->armyManager->toSlotInfo(
-					ai->armyManager->getArmyAvailableToBuy(
-						path.heroArmy,
-						upgrader,
-						ai->getFreeResources(),
-						path.turn())));
-
 			upgrade.upgradeValue += armyToGetOrBuy.upgradeValue;
 			upgrade.upgradeCost += armyToGetOrBuy.upgradeCost;
 			vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy);
@@ -339,8 +315,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
 			{
 				for(auto hero : cb->getAvailableHeroes(upgrader))
 				{
-					auto scoutReinforcement =  ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader)
-						+ ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
+					auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
 
 					if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
 						&& ai->getFreeGold() >20000
@@ -366,7 +341,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
 
 		auto danger = path.getTotalDanger();
 
-		auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger);
+		auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger, ai->settings->getSafeAttackRatio());
 
 #if NKAI_TRACE_LEVEL >= 2
 		logAi->trace(

+ 64 - 25
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

@@ -31,9 +31,11 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 
 	auto ourHeroes = ai->heroManager->getHeroRoles();
 	auto minScoreToHireMain = std::numeric_limits<float>::max();
+	int currentArmyValue = 0;
 
 	for(auto hero : ourHeroes)
 	{
+		currentArmyValue += hero.first->getArmyCost();
 		if(hero.second != HeroRole::MAIN)
 			continue;
 
@@ -45,51 +47,88 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 			minScoreToHireMain = newScore;
 		}
 	}
-
+	// If we don't have any heros we might want to lower our expectations.
+	if (ourHeroes.empty())
+		minScoreToHireMain = 0;
+
+	const CGHeroInstance* bestHeroToHire = nullptr;
+	const CGTownInstance* bestTownToHireFrom = nullptr;
+	float bestScore = 0;
+	bool haveCapitol = false;
+
+	ai->dangerHitMap->updateHitMap();
+	int treasureSourcesCount = 0;
+	
 	for(auto town : towns)
 	{
+		uint8_t closestThreat = UINT8_MAX;
+		for (auto threat : ai->dangerHitMap->getTownThreats(town))
+		{
+			closestThreat = std::min(closestThreat, threat.turn);
+		}
+		//Don't hire a hero where there already is one present
+		if (town->visitingHero && town->garrisonHero)
+			continue;
+		float visitability = 0;
+		for (auto checkHero : ourHeroes)
+		{
+			if (ai->dangerHitMap->getClosestTown(checkHero.first.get()->visitablePos()) == town)
+				visitability++;
+		}
 		if(ai->heroManager->canRecruitHero(town))
 		{
 			auto availableHeroes = ai->cb->getAvailableHeroes(town);
-
-			for(auto hero : availableHeroes)
+			
+			for (auto obj : ai->objectClusterizer->getNearbyObjects())
 			{
-				auto score = ai->heroManager->evaluateHero(hero);
-
-				if(score > minScoreToHireMain)
-				{
-					tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(200)));
-					break;
-				}
-			}
-
-			int treasureSourcesCount = 0;
-
-			for(auto obj : ai->objectClusterizer->getNearbyObjects())
-			{
-				if((obj->ID == Obj::RESOURCE)
+				if ((obj->ID == Obj::RESOURCE)
 					|| obj->ID == Obj::TREASURE_CHEST
 					|| obj->ID == Obj::CAMPFIRE
 					|| isWeeklyRevisitable(ai, obj)
-					|| obj->ID ==Obj::ARTIFACT)
+					|| obj->ID == Obj::ARTIFACT)
 				{
 					auto tile = obj->visitablePos();
 					auto closestTown = ai->dangerHitMap->getClosestTown(tile);
 
-					if(town == closestTown)
+					if (town == closestTown)
 						treasureSourcesCount++;
 				}
 			}
 
-			if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000))
-				continue;
-
-			if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1
-				|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh()))
+			for(auto hero : availableHeroes)
 			{
-				tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3)));
+				auto score = ai->heroManager->evaluateHero(hero);
+				if(score > minScoreToHireMain)
+				{
+					score *= score / minScoreToHireMain;
+				}
+				score *= (hero->getArmyCost() + currentArmyValue);
+				if (hero->getFactionID() == town->getFactionID())
+					score *= 1.5;
+				if (vstd::isAlmostZero(visitability))
+					score *= 30 * town->getTownLevel();
+				else
+					score *= town->getTownLevel() / visitability;
+				if (score > bestScore)
+				{
+					bestScore = score;
+					bestHeroToHire = hero;
+					bestTownToHireFrom = town;
+				}
 			}
 		}
+		if (town->hasCapitol())
+			haveCapitol = true;
+	}
+	if (bestHeroToHire && bestTownToHireFrom)
+	{
+		if (ai->cb->getHeroesInfo().size() == 0
+			|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
+			|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
+			|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
+		{
+			tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / (ourHeroes.size() + 1))));
+		}
 	}
 
 	return tasks;

+ 1 - 10
AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp

@@ -39,9 +39,6 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
 
 	for(auto town : towns)
 	{
-		if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
-			continue;
-
 		ai->pathfinder->calculatePathInfo(paths, town->visitablePos());
 
 		for(auto & path : paths)
@@ -49,14 +46,8 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
 			if(town->visitingHero && town->visitingHero.get() != path.targetHero)
 				continue;
 
-			if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit())
-				continue;
-
-			if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
+			if(!path.getFirstBlockedAction() && path.exchangeCount <= 1)
 			{
-				if(path.targetHero->mana == path.targetHero->manaLimit())
-					continue;
-
 				Composition stayAtTown;
 
 				stayAtTown.addNextSequence({

+ 1 - 2
AI/Nullkiller/Engine/FuzzyEngines.cpp

@@ -17,8 +17,7 @@
 namespace NKAI
 {
 
-#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter
-#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
+constexpr float MIN_AI_STRENGTH = 0.5f; //lower when combat AI gets smarter
 
 engineBase::engineBase()
 {

+ 12 - 3
AI/Nullkiller/Engine/FuzzyHelper.cpp

@@ -52,6 +52,15 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit
 			{
 				objectDanger += evaluateDanger(hero->visitedTown.get());
 			}
+			objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
+		}
+		if (objWithID<Obj::TOWN>(dangerousObject))
+		{
+			auto town = dynamic_cast<const CGTownInstance*>(dangerousObject);
+			auto hero = town->garrisonHero;
+
+			if (hero)
+				objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
 		}
 
 		if(objectDanger)
@@ -117,10 +126,10 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 		{
 			auto fortLevel = town->fortLevel();
 
-			if(fortLevel == CGTownInstance::EFortLevel::CASTLE)
-				danger += 10000;
+			if (fortLevel == CGTownInstance::EFortLevel::CASTLE)
+				danger = std::max(danger * 2, danger + 10000);
 			else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
-				danger += 4000;
+				danger = std::max(ui64(danger * 1.4), danger + 4000);
 		}
 
 		return danger;

+ 174 - 26
AI/Nullkiller/Engine/Nullkiller.cpp

@@ -34,13 +34,12 @@ using namespace Goals;
 std::unique_ptr<ObjectGraph> Nullkiller::baseGraph;
 
 Nullkiller::Nullkiller()
-	:activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true)
+	: activeHero(nullptr)
+	, scanDepth(ScanDepth::MAIN_FULL)
+	, useHeroChain(true)
+	, memory(std::make_unique<AIMemory>())
 {
-	memory = std::make_unique<AIMemory>();
-	settings = std::make_unique<Settings>();
 
-	useObjectGraph = settings->isObjectGraphAllowed();
-	openMap = settings->isOpenMap() || useObjectGraph;
 }
 
 bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
@@ -62,17 +61,23 @@ bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
 		return false;
 	}
 
-	return cb->getStartInfo()->difficulty >= 3;
+	return true;
 }
 
 void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
 {
 	this->cb = cb;
 	this->gateway = gateway;
-	
-	playerID = gateway->playerID;
+	this->playerID = gateway->playerID;
 
-	if(openMap && !canUseOpenMap(cb, playerID))
+	settings = std::make_unique<Settings>(cb->getStartInfo()->difficulty);
+
+	if(canUseOpenMap(cb, playerID))
+	{
+		useObjectGraph = settings->isObjectGraphAllowed();
+		openMap = settings->isOpenMap() || useObjectGraph;
+	}
+	else
 	{
 		useObjectGraph = false;
 		openMap = false;
@@ -122,11 +127,14 @@ void TaskPlan::merge(TSubgoal task)
 {
 	TGoalVec blockers;
 
+	if (task->asTask()->priority <= 0)
+		return;
+
 	for(auto & item : tasks)
 	{
 		for(auto objid : item.affectedObjects)
 		{
-			if(task == item.task || task->asTask()->isObjectAffected(objid))
+			if(task == item.task || task->asTask()->isObjectAffected(objid) || (task->asTask()->getHero() != nullptr && task->asTask()->getHero() == item.task->asTask()->getHero()))
 			{
 				if(item.task->asTask()->priority >= task->asTask()->priority)
 					return;
@@ -166,20 +174,19 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
 	return taskptr(*bestTask);
 }
 
-Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const
+Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const
 {
 	TaskPlan taskPlan;
 
-	tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks](const tbb::blocked_range<size_t> & r)
+	tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks, priorityTier](const tbb::blocked_range<size_t> & r)
 		{
 			auto evaluator = this->priorityEvaluators->acquire();
 
 			for(size_t i = r.begin(); i != r.end(); i++)
 			{
 				auto task = tasks[i];
-
-				if(task->asTask()->priority <= 0)
-					task->asTask()->priority = evaluator->evaluate(task);
+				if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS)
+					task->asTask()->priority = evaluator->evaluate(task, priorityTier);
 			}
 		});
 
@@ -326,7 +333,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
 		if(lockReason != HeroLockedReason::NOT_LOCKED)
 		{
 #if NKAI_TRACE_LEVEL >= 1
-			logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString());
+			logAi->trace("Hero %s is locked by %d. Discarding %s", path.targetHero->getObjectName(), (int)lockReason,  path.toString());
 #endif
 			return true;
 		}
@@ -347,12 +354,24 @@ void Nullkiller::makeTurn()
 	boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
 
 	const int MAX_DEPTH = 10;
-	const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
 
 	resetAiState();
 
 	Goals::TGoalVec bestTasks;
 
+#if NKAI_TRACE_LEVEL >= 1
+	float totalHeroStrength = 0;
+	int totalTownLevel = 0;
+	for (auto heroInfo : cb->getHeroesInfo())
+	{
+		totalHeroStrength += heroInfo->getTotalStrength();
+	}
+	for (auto townInfo : cb->getTownsInfo())
+	{
+		totalTownLevel += townInfo->getTownLevel();
+	}
+	logAi->info("Beginning: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
+#endif
 	for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
 	{
 		auto start = std::chrono::high_resolution_clock::now();
@@ -360,17 +379,21 @@ void Nullkiller::makeTurn()
 
 		Goals::TTask bestTask = taskptr(Goals::Invalid());
 
-		for(;i <= settings->getMaxPass(); i++)
+		while(true)
 		{
 			bestTasks.clear();
 
+			decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
 			decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
 			decompose(bestTasks, sptr(BuildingBehavior()), 1);
 
 			bestTask = choseBestTask(bestTasks);
 
-			if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY)
+			if(bestTask->priority > 0)
 			{
+#if NKAI_TRACE_LEVEL >= 1
+				logAi->info("Pass %d: Performing prio 0 task %s with prio: %d", i, bestTask->toString(), bestTask->priority);
+#endif
 				if(!executeTask(bestTask))
 					return;
 
@@ -382,7 +405,6 @@ void Nullkiller::makeTurn()
 			}
 		}
 
-		decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
 		decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
 		decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
 		decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
@@ -392,12 +414,24 @@ void Nullkiller::makeTurn()
 		if(!isOpenMap())
 			decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH);
 
-		if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty())
+		TTaskVec selectedTasks;
+#if NKAI_TRACE_LEVEL >= 1
+		int prioOfTask = 0;
+#endif
+		for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio)
 		{
-			decompose(bestTasks, sptr(StartupBehavior()), 1);
+#if NKAI_TRACE_LEVEL >= 1
+			prioOfTask = prio;
+#endif
+			selectedTasks = buildPlan(bestTasks, prio);
+			if (!selectedTasks.empty() || settings->isUseFuzzy())
+				break;
 		}
 
-		auto selectedTasks = buildPlan(bestTasks);
+		std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b) 
+		{
+			return a->priority > b->priority;
+		});
 
 		logAi->debug("Decision madel in %ld", timeElapsed(start));
 
@@ -438,7 +472,7 @@ void Nullkiller::makeTurn()
 					bestTask->priority);
 			}
 
-			if(bestTask->priority < MIN_PRIORITY)
+			if((settings->isUseFuzzy() && bestTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && bestTask->priority <= 0))
 			{
 				auto heroes = cb->getHeroesInfo();
 				auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
@@ -463,7 +497,9 @@ void Nullkiller::makeTurn()
 
 				continue;
 			}
-
+#if NKAI_TRACE_LEVEL >= 1
+			logAi->info("Pass %d: Performing prio %d task %s with prio: %d", i, prioOfTask, bestTask->toString(), bestTask->priority);
+#endif
 			if(!executeTask(bestTask))
 			{
 				if(hasAnySuccess)
@@ -471,13 +507,27 @@ void Nullkiller::makeTurn()
 				else
 					return;
 			}
-
 			hasAnySuccess = true;
 		}
 
+		hasAnySuccess |= handleTrading();
+
 		if(!hasAnySuccess)
 		{
 			logAi->trace("Nothing was done this turn. Ending turn.");
+#if NKAI_TRACE_LEVEL >= 1
+			totalHeroStrength = 0;
+			totalTownLevel = 0;
+			for (auto heroInfo : cb->getHeroesInfo())
+			{
+				totalHeroStrength += heroInfo->getTotalStrength();
+			}
+			for (auto townInfo : cb->getTownsInfo())
+			{
+				totalTownLevel += townInfo->getTownLevel();
+			}
+			logAi->info("End: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
+#endif
 			return;
 		}
 
@@ -554,4 +604,102 @@ void Nullkiller::lockResources(const TResources & res)
 	lockedResources += res;
 }
 
+bool Nullkiller::handleTrading()
+{
+	bool haveTraded = false;
+	bool shouldTryToTrade = true;
+	int marketId = -1;
+	for (auto town : cb->getTownsInfo())
+	{
+		if (town->hasBuiltSomeTradeBuilding())
+		{
+			marketId = town->id;
+		}
+	}
+	if (marketId == -1)
+		return false;
+	if (const CGObjectInstance* obj = cb->getObj(ObjectInstanceID(marketId), false))
+	{
+		if (const auto* m = dynamic_cast<const IMarket*>(obj))
+		{
+			while (shouldTryToTrade)
+			{
+				shouldTryToTrade = false;
+				buildAnalyzer->update();
+				TResources required = buildAnalyzer->getTotalResourcesRequired();
+				TResources income = buildAnalyzer->getDailyIncome();
+				TResources available = cb->getResourceAmount();
+#if NKAI_TRACE_LEVEL >= 2
+				logAi->debug("Available %s", available.toString());
+				logAi->debug("Required  %s", required.toString());
+#endif
+				int mostWanted = -1;
+				int mostExpendable = -1;
+				float minRatio = std::numeric_limits<float>::max();
+				float maxRatio = std::numeric_limits<float>::min();
+
+				for (int i = 0; i < required.size(); ++i)
+				{
+					if (required[i] <= 0)
+						continue;
+					float ratio = static_cast<float>(available[i]) / required[i];
+
+					if (ratio < minRatio) {
+						minRatio = ratio;
+						mostWanted = i;
+					}
+				}
+
+				for (int i = 0; i < required.size(); ++i)
+				{
+					float ratio = available[i];
+					if (required[i] > 0)
+						ratio = static_cast<float>(available[i]) / required[i];
+					else
+						ratio = available[i];
+
+					bool okToSell = false;
+
+					if (i == GameResID::GOLD)
+					{
+						if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh())
+							okToSell = true;
+					}
+					else
+					{
+						if (required[i] <= 0 && income[i] > 0)
+							okToSell = true;
+					}
+
+					if (ratio > maxRatio && okToSell) {
+						maxRatio = ratio;
+						mostExpendable = i;
+					}
+				}
+#if NKAI_TRACE_LEVEL >= 2
+				logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted);
+#endif
+				if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1)
+					return false;
+
+				int toGive;
+				int toGet;
+				m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
+				//logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName());
+				//TODO trade only as much as needed
+				if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources
+				{
+					cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive);
+#if NKAI_TRACE_LEVEL >= 1
+					logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
+#endif
+					haveTraded = true;
+					shouldTryToTrade = true;
+				}
+			}
+		}
+	}
+	return haveTraded;
+}
+
 }

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

@@ -120,13 +120,14 @@ public:
 	ScanDepth getScanDepth() const { return scanDepth; }
 	bool isOpenMap() const { return openMap; }
 	bool isObjectGraphAllowed() const { return useObjectGraph; }
+	bool handleTrading();
 
 private:
 	void resetAiState();
 	void updateAiState(int pass, bool fast = false);
 	void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const;
 	Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const;
-	Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const;
+	Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier) const;
 	bool executeTask(Goals::TTask task);
 	bool areAffectedObjectsPresent(Goals::TTask task) const;
 	HeroRole getTaskRole(Goals::TTask task) const;

+ 403 - 48
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -15,6 +15,8 @@
 #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
 #include "../../../lib/mapObjects/MapObjects.h"
+#include "../../../lib/mapping/CMapDefines.h"
+#include "../../../lib/RoadHandler.h"
 #include "../../../lib/CCreatureHandler.h"
 #include "../../../lib/VCMI_Lib.h"
 #include "../../../lib/StartInfo.h"
@@ -33,11 +35,9 @@
 namespace NKAI
 {
 
-#define MIN_AI_STRENGTH (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;
+constexpr float MIN_CRITICAL_VALUE = 2.0f;
 
-EvaluationContext::EvaluationContext(const Nullkiller * ai)
+EvaluationContext::EvaluationContext(const Nullkiller* ai)
 	: movementCost(0.0),
 	manaCost(0),
 	danger(0),
@@ -51,9 +51,22 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
 	heroRole(HeroRole::SCOUT),
 	turn(0),
 	strategicalValue(0),
+	conquestValue(0),
 	evaluator(ai),
 	enemyHeroDangerRatio(0),
-	armyGrowth(0)
+	threat(0),
+	armyGrowth(0),
+	armyInvolvement(0),
+	defenseValue(0),
+	isDefend(false),
+	threatTurns(INT_MAX),
+	involvesSailing(false),
+	isTradeBuilding(false),
+	isExchange(false),
+	isArmyUpgrade(false),
+	isHero(false),
+	isEnemy(false),
+	explorePriority(0)
 {
 }
 
@@ -225,7 +238,7 @@ int getDwellingArmyCost(const CGObjectInstance * target)
 			auto creature = creLevel.second.back().toCreature();
 			auto creaturesAreFree = creature->getLevel() == 1;
 			if(!creaturesAreFree)
-				cost += creature->getRecruitCost(EGameResID::GOLD) * creLevel.first;
+				cost += creature->getFullRecruitCost().marketValue() * creLevel.first;
 		}
 	}
 
@@ -251,6 +264,8 @@ static uint64_t evaluateArtifactArmyValue(const CArtifact * art)
 
 	switch(art->aClass)
 	{
+	case CArtifact::EartClass::ART_TREASURE:
+		//FALL_THROUGH
 	case CArtifact::EartClass::ART_MINOR:
 		classValue = 1000;
 		break;
@@ -289,6 +304,8 @@ uint64_t RewardEvaluator::getArmyReward(
 	case Obj::CREATURE_GENERATOR3:
 	case Obj::CREATURE_GENERATOR4:
 		return getDwellingArmyValue(ai->cb.get(), target, checkGold);
+	case Obj::SPELL_SCROLL:
+		//FALL_THROUGH
 	case Obj::ARTIFACT:
 		return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType());
 	case Obj::HERO:
@@ -479,7 +496,7 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
 	return result;
 }
 
-uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
+float RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
 {
 	return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit()));
 }
@@ -581,6 +598,54 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons
 	return 0;
 }
 
+float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const
+{
+	if (!target)
+		return 0;
+	if (target->getOwner() == ai->playerID)
+		return 0;
+	switch (target->ID)
+	{
+	case Obj::TOWN:
+	{
+		if (ai->buildAnalyzer->getDevelopmentInfo().empty())
+			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 = 1.0f;
+
+		if (town->hasCapitol())
+			return booster * 1.5;
+
+		if (fortLevel < CGTownInstance::CITADEL)
+			return booster * (town->hasFort() ? 1.0 : 0.8);
+		else
+			return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
+	}
+
+	case Obj::HERO:
+		return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
+			? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance*>(target))
+			: 0;
+
+	case Obj::KEYMASTER:
+		return 0.6f;
+
+	default:
+		return 0;
+	}
+}
+
 float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const
 {
 	auto rewardable = dynamic_cast<const CRewardableObject *>(hut);
@@ -705,7 +770,7 @@ int32_t getArmyCost(const CArmedInstance * army)
 
 	for(auto stack : army->Slots())
 	{
-		value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count;
+		value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count;
 	}
 
 	return value;
@@ -786,7 +851,9 @@ public:
 		uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
 
 		evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength());
+		evaluationContext.conquestValue += 2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength();
 		evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
+		evaluationContext.isExchange = true;
 	}
 };
 
@@ -804,6 +871,7 @@ public:
 
 		evaluationContext.armyReward += upgradeValue;
 		evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
+		evaluationContext.isArmyUpgrade = true;
 	}
 };
 
@@ -818,22 +886,46 @@ public:
 		int tilesDiscovered = task->value;
 
 		evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered);
+		for (auto obj : evaluationContext.evaluator.ai->cb->getVisitableObjs(task->tile))
+		{
+			switch (obj->ID.num)
+			{
+			case Obj::MONOLITH_ONE_WAY_ENTRANCE:
+			case Obj::MONOLITH_TWO_WAY:
+			case Obj::SUBTERRANEAN_GATE:
+				evaluationContext.explorePriority = 1;
+				break;
+			case Obj::REDWOOD_OBSERVATORY:
+			case Obj::PILLAR_OF_FIRE:
+				evaluationContext.explorePriority = 2;
+				break;
+			}
+		}
+		if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType != RoadId::NO_ROAD)
+			evaluationContext.explorePriority = 1;
+		if (evaluationContext.explorePriority == 0)
+			evaluationContext.explorePriority = 3;
 	}
 };
 
 class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
 {
 public:
-	void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
+	void buildEvaluationContext(EvaluationContext& evaluationContext, Goals::TSubgoal task) const override
 	{
-		if(task->goalType != Goals::STAY_AT_TOWN)
+		if (task->goalType != Goals::STAY_AT_TOWN)
 			return;
 
-		Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task);
+		Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
 
 		evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
-		evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
-		evaluationContext.movementCost += stayAtTown.getMovementWasted();
+		if (evaluationContext.armyReward == 0)
+			evaluationContext.isDefend = true;
+		else
+		{
+			evaluationContext.movementCost += stayAtTown.getMovementWasted();
+			evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
+		}
 	}
 };
 
@@ -844,15 +936,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
 	if(enemyDanger.danger)
 	{
 		auto dangerRatio = enemyDanger.danger / (double)ourStrength;
-		auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false);
-		bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb);
-
-		if(isAI)
-		{
-			dangerRatio *= 1.5; // lets make AI bit more afraid of other AI.
-		}
-
 		vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio);
+		vstd::amax(evaluationContext.threat, enemyDanger.threat);
 	}
 }
 
@@ -896,6 +981,10 @@ public:
 		else
 			evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
 
+		evaluationContext.defenseValue = town->fortLevel();
+		evaluationContext.isDefend = true;
+		evaluationContext.threatTurns = treat.turn;
+
 		vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
 		addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
 	}
@@ -926,6 +1015,8 @@ public:
 		for(auto & node : path.nodes)
 		{
 			vstd::amax(costsPerHero[node.targetHero], node.cost);
+			if (node.layer == EPathfindingLayer::SAIL)
+				evaluationContext.involvesSailing = true;
 		}
 
 		for(auto pair : costsPerHero)
@@ -952,10 +1043,18 @@ public:
 			evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
 			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
 			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
+			evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
+			if (target->ID == Obj::HERO)
+				evaluationContext.isHero = true;
+			if (target->getOwner() != PlayerColor::NEUTRAL && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
+				evaluationContext.isEnemy = true;
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
+			evaluationContext.armyInvolvement += army->getArmyCost();
+			if(evaluationContext.danger > 0)
+				evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
 		}
 
-		vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
+		vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
 		addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
 		vstd::amax(evaluationContext.turn, path.turn());
 	}
@@ -996,6 +1095,7 @@ public:
 			evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
 			evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
 			evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
+			evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
 			evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
 			evaluationContext.movementCost += objInfo.second.movementCost / boost;
@@ -1021,6 +1121,14 @@ public:
 		Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
 		const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero();
 
+		logAi->trace("buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d", swapCommand.toString(), swapCommand.getAffectedObjects().size());
+		for (auto obj : swapCommand.getAffectedObjects())
+		{
+			logAi->trace("affected object: %s", evaluationContext.evaluator.ai->cb->getObj(obj)->getObjectName());
+		}
+		if (garrisonHero)
+			logAi->debug("with %s and %d", garrisonHero->getNameTranslated(), int(swapCommand.getLockingReason()));
+
 		if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
 		{
 			auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
@@ -1029,6 +1137,9 @@ public:
 			evaluationContext.movementCost += mpLeft;
 			evaluationContext.movementCostByRole[defenderRole] += mpLeft;
 			evaluationContext.heroRole = defenderRole;
+			evaluationContext.isDefend = true;
+			evaluationContext.armyInvolvement = garrisonHero->getArmyStrength();
+			logAi->debug("evaluationContext.isDefend: %d", evaluationContext.isDefend);
 		}
 	}
 };
@@ -1072,8 +1183,14 @@ public:
 		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.buildCostWithPrerequisites[EGameResID::GOLD];
+		int32_t cost = bi.buildCost[EGameResID::GOLD];
+		evaluationContext.goldCost += cost;
 		evaluationContext.closestWayRatio = 1;
+		evaluationContext.buildingCost += bi.buildCostWithPrerequisites;
+		if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
+			evaluationContext.isTradeBuilding = true;
+
+		logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue());
 
 		if(bi.creatureID != CreatureID::NONE)
 		{
@@ -1100,7 +1217,18 @@ public:
 		else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
 		{
 			evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
+			for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo())
+			{
+				evaluationContext.armyInvolvement += hero->getArmyCost();
+			}
 		}
+		int sameTownBonus = 0;
+		for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo())
+		{
+			if (buildThis.town->getFaction() == town->getFaction())
+				sameTownBonus += town->getTownLevel();
+		}
+		evaluationContext.armyReward *= sameTownBonus;
 		
 		if(evaluationContext.goldReward)
 		{
@@ -1162,6 +1290,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
 	for(auto subgoal : parts)
 	{
 		context.goldCost += subgoal->goldCost;
+		context.buildingCost += subgoal->buildingCost;
 
 		for(auto builder : evaluationContextBuilders)
 		{
@@ -1172,7 +1301,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
 	return context;
 }
 
-float PriorityEvaluator::evaluate(Goals::TSubgoal task)
+float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 {
 	auto evaluationContext = buildEvaluationContext(task);
 
@@ -1185,36 +1314,257 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 	
 	double result = 0;
 
-	try
+	if (ai->settings->isUseFuzzy())
 	{
-		armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
-		heroRoleVariable->setValue(evaluationContext.heroRole);
-		mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
-		scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
-		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);
-		goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
-		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);
-
-		engine->process();
-
-		result = value->getValue();
+		float fuzzyResult = 0;
+		try
+		{
+			armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
+			heroRoleVariable->setValue(evaluationContext.heroRole);
+			mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
+			scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
+			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);
+			goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
+			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);
+
+			engine->process();
+
+			fuzzyResult = value->getValue();
+		}
+		catch (fl::Exception& fe)
+		{
+			logAi->error("evaluate VisitTile: %s", fe.getWhat());
+		}
+		result = fuzzyResult;
 	}
-	catch(fl::Exception & fe)
+	else
 	{
-		logAi->error("evaluate VisitTile: %s", fe.getWhat());
+		float score = 0;
+		const bool amIInDanger = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0);
+		const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget();
+
+		bool arriveNextWeek = false;
+		if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7)
+			arriveNextWeek = true;
+
+#if NKAI_TRACE_LEVEL >= 2
+		logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d",
+			priorityTier,
+			task->toString(),
+			evaluationContext.armyLossPersentage,
+			(int)evaluationContext.turn,
+			evaluationContext.movementCostByRole[HeroRole::MAIN],
+			evaluationContext.movementCostByRole[HeroRole::SCOUT],
+			goldRewardPerTurn,
+			evaluationContext.goldCost,
+			evaluationContext.armyReward,
+			evaluationContext.armyGrowth,
+			evaluationContext.skillReward,
+			evaluationContext.danger,
+			evaluationContext.threatTurns,
+			evaluationContext.threat,
+			evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
+			evaluationContext.strategicalValue,
+			evaluationContext.conquestValue,
+			evaluationContext.closestWayRatio,
+			evaluationContext.enemyHeroDangerRatio,
+			evaluationContext.explorePriority,
+			evaluationContext.isDefend);
+#endif
+
+		switch (priorityTier)
+		{
+			case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach
+			{
+				if (evaluationContext.turn > 0)
+					return 0;
+				if(evaluationContext.conquestValue > 0)
+					score = 1000;
+				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score *= evaluationContext.closestWayRatio;
+				if (evaluationContext.movementCost > 0)
+					score /= evaluationContext.movementCost;
+				break;
+			}
+			case PriorityTier::INSTADEFEND: //Defend immediately threatened towns
+			{
+				if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0)
+					score = evaluationContext.armyInvolvement;
+				if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score *= evaluationContext.closestWayRatio;
+				break;
+			}
+			case PriorityTier::KILL: //Take towns / kill heroes that are further away
+			{
+				if (evaluationContext.turn > 0 && evaluationContext.isHero)
+					return 0;
+				if (arriveNextWeek && evaluationContext.isEnemy)
+					return 0;
+				if (evaluationContext.conquestValue > 0)
+					score = 1000;
+				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score *= evaluationContext.closestWayRatio;
+				if (evaluationContext.movementCost > 0)
+					score /= evaluationContext.movementCost;
+				break;
+			}
+			case PriorityTier::UPGRADE:
+			{
+				if (!evaluationContext.isArmyUpgrade)
+					return 0;
+				if (evaluationContext.enemyHeroDangerRatio > 1)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score = 1000;
+				score *= evaluationContext.closestWayRatio;
+				if (evaluationContext.movementCost > 0)
+					score /= evaluationContext.movementCost;
+				break;
+			}
+			case PriorityTier::HIGH_PRIO_EXPLORE:
+			{
+				if (evaluationContext.enemyHeroDangerRatio > 1)
+					return 0;
+				if (evaluationContext.explorePriority != 1)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score = 1000;
+				score *= evaluationContext.closestWayRatio;
+				if (evaluationContext.movementCost > 0)
+					score /= evaluationContext.movementCost;
+				break;
+			}
+			case PriorityTier::HUNTER_GATHER: //Collect guarded stuff
+			{
+				if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
+					return 0;
+				if (evaluationContext.buildingCost.marketValue() > 0)
+					return 0;
+				if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0))
+					return 0;
+				if (evaluationContext.explorePriority == 3)
+					return 0;
+				if (evaluationContext.isArmyUpgrade)
+					return 0;
+				if ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > 1)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score += evaluationContext.strategicalValue * 1000;
+				score += evaluationContext.goldReward;
+				score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
+				score += evaluationContext.armyReward;
+				score += evaluationContext.armyGrowth;
+				score -= evaluationContext.goldCost;
+				score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage;
+				if (score > 0)
+				{
+					score = 1000;
+					score *= evaluationContext.closestWayRatio;
+					if (evaluationContext.movementCost > 0)
+						score /= evaluationContext.movementCost;
+				}
+				break;
+			}
+			case PriorityTier::LOW_PRIO_EXPLORE:
+			{
+				if (evaluationContext.enemyHeroDangerRatio > 1)
+					return 0;
+				if (evaluationContext.explorePriority != 3)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				score = 1000;
+				score *= evaluationContext.closestWayRatio;
+				if (evaluationContext.movementCost > 0)
+					score /= evaluationContext.movementCost;
+				break;
+			}
+			case PriorityTier::DEFEND: //Defend whatever if nothing else is to do
+			{
+				if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
+					return 0;
+				if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
+					score = 1000;
+				score *= evaluationContext.closestWayRatio;
+				score /= (evaluationContext.turn + 1);
+				break;
+			}
+			case PriorityTier::BUILDINGS: //For buildings and buying army
+			{
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
+					return 0;
+				//If we already have locked resources, we don't look at other buildings
+				if (ai->getLockedResources().marketValue() > 0)
+					return 0;
+				score += evaluationContext.conquestValue * 1000;
+				score += evaluationContext.strategicalValue * 1000;
+				score += evaluationContext.goldReward;
+				score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
+				score += evaluationContext.armyReward;
+				score += evaluationContext.armyGrowth;
+				if (evaluationContext.buildingCost.marketValue() > 0)
+				{
+					if (!evaluationContext.isTradeBuilding && ai->getFreeResources()[EGameResID::WOOD] - evaluationContext.buildingCost[EGameResID::WOOD] < 5 && ai->buildAnalyzer->getDailyIncome()[EGameResID::WOOD] < 1)
+					{
+						logAi->trace("Should make sure to build market-place instead of %s", task->toString());
+						for (auto town : ai->cb->getTownsInfo())
+						{
+							if (!town->hasBuiltSomeTradeBuilding())
+								return 0;
+						}
+					}
+					score += 1000;
+					auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources();
+					auto income = ai->buildAnalyzer->getDailyIncome();
+					if(ai->buildAnalyzer->isGoldPressureHigh())
+						score /= evaluationContext.buildingCost.marketValue();
+					if (!resourcesAvailable.canAfford(evaluationContext.buildingCost))
+					{
+						TResources needed = evaluationContext.buildingCost - resourcesAvailable;
+						needed.positive();
+						int turnsTo = needed.maxPurchasableCount(income);
+						if (turnsTo == INT_MAX)
+							return 0;
+						else
+							score /= turnsTo;
+					}
+				}
+				else
+				{
+					if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && vstd::isAlmostZero(evaluationContext.conquestValue))
+						return 0;
+				}
+				break;
+			}
+		}
+		result = score;
+		//TODO: Figure out the root cause for why evaluationContext.closestWayRatio has become -nan(ind).
+		if (std::isnan(result))
+			return 0;
 	}
 
 #if NKAI_TRACE_LEVEL >= 2
-	logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
+	logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, result %f",
+		priorityTier,
 		task->toString(),
 		evaluationContext.armyLossPersentage,
 		(int)evaluationContext.turn,
@@ -1223,9 +1573,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
 		goldRewardPerTurn,
 		evaluationContext.goldCost,
 		evaluationContext.armyReward,
+		evaluationContext.armyGrowth,
+		evaluationContext.skillReward,
 		evaluationContext.danger,
+		evaluationContext.threatTurns,
+		evaluationContext.threat,
 		evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
 		evaluationContext.strategicalValue,
+		evaluationContext.conquestValue,
 		evaluationContext.closestWayRatio,
 		evaluationContext.enemyHeroDangerRatio,
 		result);

+ 30 - 2
AI/Nullkiller/Engine/PriorityEvaluator.h

@@ -41,6 +41,7 @@ public:
 	float getResourceRequirementStrength(int resType) const;
 	float getResourceRequirementStrength(const TResources & res) const;
 	float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const;
+	float getConquestValue(const CGObjectInstance* target) const;
 	float getTotalResourceRequirementStrength(int resType) const;
 	float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const;
 	float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const;
@@ -48,7 +49,7 @@ public:
 	uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
 	const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
 	uint64_t townArmyGrowth(const CGTownInstance * town) const;
-	uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
+	float getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
 };
 
 struct DLL_EXPORT EvaluationContext
@@ -65,10 +66,24 @@ struct DLL_EXPORT EvaluationContext
 	int32_t goldCost;
 	float skillReward;
 	float strategicalValue;
+	float conquestValue;
 	HeroRole heroRole;
 	uint8_t turn;
 	RewardEvaluator evaluator;
 	float enemyHeroDangerRatio;
+	float threat;
+	float armyInvolvement;
+	int defenseValue;
+	bool isDefend;
+	int threatTurns;
+	TResources buildingCost;
+	bool involvesSailing;
+	bool isTradeBuilding;
+	bool isExchange;
+	bool isArmyUpgrade;
+	bool isHero;
+	bool isEnemy;
+	int explorePriority;
 
 	EvaluationContext(const Nullkiller * ai);
 
@@ -91,7 +106,20 @@ public:
 	~PriorityEvaluator();
 	void initVisitTile();
 
-	float evaluate(Goals::TSubgoal task);
+	float evaluate(Goals::TSubgoal task, int priorityTier = BUILDINGS);
+
+	enum PriorityTier : int32_t
+	{
+		BUILDINGS = 0,
+		INSTAKILL,
+		INSTADEFEND,
+		KILL,
+		UPGRADE,
+		HIGH_PRIO_EXPLORE,
+		HUNTER_GATHER,
+		LOW_PRIO_EXPLORE,
+		DEFEND
+	};
 
 private:
 	const Nullkiller * ai;

+ 31 - 43
AI/Nullkiller/Engine/Settings.cpp

@@ -11,6 +11,8 @@
 #include <limits>
 
 #include "Settings.h"
+
+#include "../../../lib/constants/StringConstants.h"
 #include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
 #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
 #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
@@ -22,56 +24,42 @@
 
 namespace NKAI
 {
-	Settings::Settings()
+	Settings::Settings(int difficultyLevel)
 		: maxRoamingHeroes(8),
 		mainHeroTurnDistanceLimit(10),
 		scoutHeroTurnDistanceLimit(5),
-		maxGoldPressure(0.3f), 
+		maxGoldPressure(0.3f),
+		retreatThresholdRelative(0.3),
+		retreatThresholdAbsolute(10000),
+		safeAttackRatio(1.1),
 		maxpass(10),
+		pathfinderBucketsCount(1),
+		pathfinderBucketSize(32),
 		allowObjectGraph(true),
 		useTroopsFromGarrisons(false),
-		openMap(true)
+		updateHitmapOnTileReveal(false),
+		openMap(true),
+		useFuzzy(false)
 	{
-		JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
-
-		if(node.Struct()["maxRoamingHeroes"].isNumber())
-		{
-			maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer();
-		}
-
-		if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber())
-		{
-			mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer();
-		}
-
-		if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber())
-		{
-			scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer();
-		}
-
-		if(node.Struct()["maxpass"].isNumber())
-		{
-			maxpass = node.Struct()["maxpass"].Integer();
-		}
-
-		if(node.Struct()["maxGoldPressure"].isNumber())
-		{
-			maxGoldPressure = node.Struct()["maxGoldPressure"].Float();
-		}
-
-		if(!node.Struct()["allowObjectGraph"].isNull())
-		{
-			allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
-		}
-
-		if(!node.Struct()["openMap"].isNull())
-		{
-			openMap = node.Struct()["openMap"].Bool();
-		}
+		const std::string & difficultyName = GameConstants::DIFFICULTY_NAMES[difficultyLevel];
+		const JsonNode & rootNode = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
+		const JsonNode & node = rootNode[difficultyName];
 
-		if(!node.Struct()["useTroopsFromGarrisons"].isNull())
-		{
-			useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();
-		}
+		maxRoamingHeroes = node["maxRoamingHeroes"].Integer();
+		mainHeroTurnDistanceLimit = node["mainHeroTurnDistanceLimit"].Integer();
+		scoutHeroTurnDistanceLimit = node["scoutHeroTurnDistanceLimit"].Integer();
+		maxpass = node["maxpass"].Integer();
+		pathfinderBucketsCount = node["pathfinderBucketsCount"].Integer();
+		pathfinderBucketSize = node["pathfinderBucketSize"].Integer();
+		maxGoldPressure = node["maxGoldPressure"].Float();
+		retreatThresholdRelative = node["retreatThresholdRelative"].Float();
+		retreatThresholdAbsolute = node["retreatThresholdAbsolute"].Float();
+		maxArmyLossTarget = node["maxArmyLossTarget"].Float();
+		safeAttackRatio = node["safeAttackRatio"].Float();
+		allowObjectGraph = node["allowObjectGraph"].Bool();
+		updateHitmapOnTileReveal = node["updateHitmapOnTileReveal"].Bool();
+		openMap = node["openMap"].Bool();
+		useFuzzy = node["useFuzzy"].Bool();
+		useTroopsFromGarrisons = node["useTroopsFromGarrisons"].Bool();
 	}
 }

+ 17 - 1
AI/Nullkiller/Engine/Settings.h

@@ -25,21 +25,37 @@ namespace NKAI
 		int mainHeroTurnDistanceLimit;
 		int scoutHeroTurnDistanceLimit;
 		int maxpass;
+		int pathfinderBucketsCount;
+		int pathfinderBucketSize;
 		float maxGoldPressure;
+		float retreatThresholdRelative;
+		float retreatThresholdAbsolute;
+		float safeAttackRatio;
+		float maxArmyLossTarget;
 		bool allowObjectGraph;
 		bool useTroopsFromGarrisons;
+		bool updateHitmapOnTileReveal;
 		bool openMap;
+		bool useFuzzy;
 
 	public:
-		Settings();
+		explicit Settings(int difficultyLevel);
 
 		int getMaxPass() const { return maxpass; }
 		float getMaxGoldPressure() const { return maxGoldPressure; }
+		float getRetreatThresholdRelative() const { return retreatThresholdRelative; }
+		float getRetreatThresholdAbsolute() const { return retreatThresholdAbsolute; }
+		float getSafeAttackRatio() const { return safeAttackRatio; }
+		float getMaxArmyLossTarget() const { return maxArmyLossTarget; }
 		int getMaxRoamingHeroes() const { return maxRoamingHeroes; }
 		int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
 		int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
+		int getPathfinderBucketsCount() const { return pathfinderBucketsCount; }
+		int getPathfinderBucketSize() const { return pathfinderBucketSize; }
 		bool isObjectGraphAllowed() const { return allowObjectGraph; }
 		bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
+		bool isUpdateHitmapOnTileReveal() const { return updateHitmapOnTileReveal; }
 		bool isOpenMap() const { return openMap; }
+		bool isUseFuzzy() const { return useFuzzy; }
 	};
 }

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

@@ -104,6 +104,7 @@ namespace Goals
 		bool isAbstract; SETTER(bool, isAbstract)
 		int value; SETTER(int, value)
 		ui64 goldCost; SETTER(ui64, goldCost)
+		TResources buildingCost; SETTER(TResources, buildingCost)
 		int resID; SETTER(int, resID)
 		int objid; SETTER(int, objid)
 		int aid; SETTER(int, aid)

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

@@ -53,6 +53,9 @@ void AdventureSpellCast::accept(AIGateway * ai)
 			throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
 	}
 
+	if (hero->inTownGarrison)
+		ai->myCb->swapGarrisonHero(hero->visitedTown);
+
 	auto wait = cb->waitTillRealize;
 
 	cb->waitTillRealize = true;

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

@@ -90,9 +90,12 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai)
 	
 	if(!town->garrisonHero)
 	{
-		while(upperArmy->stacksCount() != 0)
+		if (!garrisonHero->canBeMergedWith(*town))
 		{
-			cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
+			while (upperArmy->stacksCount() != 0)
+			{
+				cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
+			}
 		}
 	}
 	

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

@@ -22,6 +22,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance *
 {
 	hero = path.targetHero;
 	tile = path.targetTile();
+	closestWayRatio = 1;
 
 	if(obj)
 	{
@@ -85,6 +86,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
 
 	ai->nullkiller->setActive(chainPath.targetHero, tile);
 	ai->nullkiller->setTargetObject(objid);
+	ai->nullkiller->objectClusterizer->reset();
 
 	auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);
 

+ 1 - 0
AI/Nullkiller/Goals/RecruitHero.cpp

@@ -73,6 +73,7 @@ void RecruitHero::accept(AIGateway * ai)
 		std::unique_lock lockGuard(ai->nullkiller->aiStateMutex);
 
 		ai->nullkiller->heroManager->update();
+		ai->nullkiller->objectClusterizer->reset();
 	}
 }
 

+ 1 - 0
AI/Nullkiller/Goals/RecruitHero.h

@@ -44,6 +44,7 @@ namespace Goals
 		}
 
 		std::string toString() const override;
+		const CGHeroInstance* getHero() const override { return heroToBuy; }
 		void accept(AIGateway * ai) override;
 	};
 }

+ 2 - 6
AI/Nullkiller/Goals/StayAtTown.cpp

@@ -36,16 +36,12 @@ std::string StayAtTown::toString() const
 {
 	return "Stay at town " + town->getNameTranslated()
 		+ " hero " + hero->getNameTranslated()
-		+ ", mana: " + std::to_string(hero->mana);
+		+ ", mana: " + std::to_string(hero->mana)
+		+ " / " + std::to_string(hero->manaLimit());
 }
 
 void StayAtTown::accept(AIGateway * ai)
 {
-	if(hero->visitedTown != town)
-	{
-		logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated());
-	}
-
 	ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE);
 }
 

+ 1 - 1
AI/Nullkiller/Helpers/ExplorationHelper.cpp

@@ -175,7 +175,7 @@ void ExplorationHelper::scanTile(const int3 & tile)
 				continue;
 			}
 
-			if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger()))
+			if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger(), ai->settings->getSafeAttackRatio()))
 			{
 				bestGoal = goal;
 				bestValue = ourValue;

+ 31 - 11
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -39,17 +39,17 @@ const uint64_t CHAIN_MAX_DEPTH = 4;
 
 const bool DO_NOT_SAVE_TO_COMMITTED_TILES = false;
 
-AISharedStorage::AISharedStorage(int3 sizes)
+AISharedStorage::AISharedStorage(int3 sizes, int numChains)
 {
 	if(!shared){
 		shared.reset(new boost::multi_array<AIPathNode, 4>(
-			boost::extents[sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS]));
+			boost::extents[sizes.z][sizes.x][sizes.y][numChains]));
 
 		nodes = shared;
 
 		foreach_tile_pos([&](const int3 & pos)
 			{
-				for(auto i = 0; i < AIPathfinding::NUM_CHAINS; i++)
+				for(auto i = 0; i < numChains; i++)
 				{
 					auto & node = get(pos)[i];
 						
@@ -92,8 +92,18 @@ void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
 	}
 }
 
+int AINodeStorage::getBucketCount() const
+{
+	return ai->settings->getPathfinderBucketsCount();
+}
+
+int AINodeStorage::getBucketSize() const
+{
+	return ai->settings->getPathfinderBucketSize();
+}
+
 AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
-	: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
+	: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes, ai->settings->getPathfinderBucketSize() * ai->settings->getPathfinderBucketsCount())
 {
 	accessibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
 		boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
@@ -169,8 +179,8 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 	const EPathfindingLayer layer, 
 	const ChainActor * actor)
 {
-	int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % AIPathfinding::BUCKET_COUNT;
-	int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE;
+	int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % ai->settings->getPathfinderBucketsCount();
+	int bucketOffset = bucketIndex * ai->settings->getPathfinderBucketSize();
 	auto chains = nodes.get(pos);
 
 	if(blocked(pos, layer))
@@ -178,7 +188,7 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
 		return std::nullopt;
 	}
 
-	for(auto i = AIPathfinding::BUCKET_SIZE - 1; i >= 0; i--)
+	for(auto i = ai->settings->getPathfinderBucketSize() - 1; i >= 0; i--)
 	{
 		AIPathNode & node = chains[i + bucketOffset];
 
@@ -486,8 +496,8 @@ public:
 		AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
 		:existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
 	{
-		existingChains.reserve(AIPathfinding::NUM_CHAINS);
-		newChains.reserve(AIPathfinding::NUM_CHAINS);
+		existingChains.reserve(storage.getBucketCount() * storage.getBucketSize());
+		newChains.reserve(storage.getBucketCount() * storage.getBucketSize());
 	}
 
 	void execute(const tbb::blocked_range<size_t>& r)
@@ -719,6 +729,7 @@ void HeroChainCalculationTask::calculateHeroChain(
 		if(node->action == EPathNodeAction::BATTLE
 			|| node->action == EPathNodeAction::TELEPORT_BATTLE
 			|| node->action == EPathNodeAction::TELEPORT_NORMAL
+			|| node->action == EPathNodeAction::DISEMBARK
 			|| node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT)
 		{
 			continue;
@@ -961,7 +972,7 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
 		// do not allow our own heroes in garrison to act on map
 		if(hero.first->getOwner() == ai->playerID
 			&& hero.first->inTownGarrison
-			&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached()))
+			&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached(false)))
 		{
 			continue;
 		}
@@ -1196,6 +1207,11 @@ void AINodeStorage::calculateTownPortal(
 					continue;
 			}
 
+			if (targetTown->visitingHero
+				&& (targetTown->visitingHero.get()->getFactionID() != actor->hero->getFactionID()
+					|| targetTown->getUpperArmy()->stacksCount()))
+				continue;
+
 			auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown);
 
 			if(nodeOptional)
@@ -1418,6 +1434,10 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
 		path.heroArmy = node.actor->creatureSet;
 		path.armyLoss = node.armyLoss;
 		path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle);
+		for (auto pathNode : path.nodes)
+		{
+			path.targetObjectDanger = std::max(ai->dangerEvaluator->evaluateDanger(pathNode.coord, path.targetHero, !node.actor->allowBattle), path.targetObjectDanger);
+		}
 
 		if(path.targetObjectDanger > 0)
 		{
@@ -1564,7 +1584,7 @@ uint8_t AIPath::turn() const
 
 uint64_t AIPath::getHeroStrength() const
 {
-	return targetHero->getFightingStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
+	return targetHero->getHeroStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
 }
 
 uint64_t AIPath::getTotalDanger() const

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

@@ -29,9 +29,6 @@ namespace NKAI
 {
 namespace AIPathfinding
 {
-	const int BUCKET_COUNT = 3;
-	const int BUCKET_SIZE = 7;
-	const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
 	const int CHAIN_MAX_DEPTH = 4;
 }
 
@@ -157,7 +154,7 @@ public:
 	static boost::mutex locker;
 	static uint32_t version;
 
-	AISharedStorage(int3 mapSize);
+	AISharedStorage(int3 sizes, int numChains);
 	~AISharedStorage();
 
 	STRONG_INLINE
@@ -197,6 +194,9 @@ public:
 	bool selectFirstActor();
 	bool selectNextActor();
 
+	int getBucketCount() const;
+	int getBucketSize() const;
+
 	std::vector<CGPathNode *> getInitialNodes() override;
 
 	virtual void calculateNeighbours(
@@ -298,7 +298,7 @@ public:
 
 	inline int getBucket(const ChainActor * actor) const
 	{
-		return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT;
+		return ((uintptr_t)actor * 395) % getBucketCount();
 	}
 
 	void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);

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

@@ -46,7 +46,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t
 	initialMovement = hero->movementPointsRemaining();
 	initialTurn = 0;
 	armyValue = getHeroArmyStrengthWithCommander(hero, hero);
-	heroFightingStrength = hero->getFightingStrength();
+	heroFightingStrength = hero->getHeroStrength();
 	tiCache.reset(new TurnInfo(hero));
 }
 

+ 0 - 2
AI/VCAI/AIUtility.h

@@ -25,11 +25,9 @@ using crstring = const std::string &;
 using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
 
 const int ACTUAL_RESOURCE_COUNT = 7;
-const int ALLOWED_ROAMING_HEROES = 8;
 
 //implementation-dependent
 extern const double SAFE_ATTACK_CONSTANT;
-extern const int GOLD_RESERVE;
 
 extern thread_local CCallback * cb;
 extern thread_local VCAI * ai;

+ 0 - 2
AI/VCAI/ResourceManager.cpp

@@ -14,8 +14,6 @@
 #include "../../CCallback.h"
 #include "../../lib/mapObjects/MapObjects.h"
 
-#define GOLD_RESERVE (10000); //at least we'll be able to reach capitol
-
 ResourceObjective::ResourceObjective(const TResources & Res, Goals::TSubgoal Goal)
 	: resources(Res), goal(Goal)
 {

+ 0 - 2
AI/VCAI/VCAI.cpp

@@ -1314,8 +1314,6 @@ bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const
 		return false;
 	if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager
 		return false;
-	if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES)
-		return false;
 	if(cb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
 		return false;
 	if(!cb->getAvailableHeroes(t).size())

+ 278 - 0
CI/example.markdownlint-cli2.jsonc

@@ -0,0 +1,278 @@
+{
+	"config" : {
+		"default" : true,
+
+		// MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md001.md
+		"MD001": false,
+
+		// MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md003.md
+		"MD003": {
+			"style": "atx"
+		},
+
+		// MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md004.md
+		"MD004": false,
+		// FIXME: enable and consider fixing
+		//{
+		//	"style": "consistent"
+		//},
+
+		// MD005/list-indent : Inconsistent indentation for list items at the same level : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md005.md
+		"MD005": true,
+
+		// MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md007.md
+		"MD007": {
+			// Spaces for indent
+			"indent": 2,
+			// Whether to indent the first level of the list
+			"start_indented": false,
+			// Spaces for first level indent (when start_indented is set)
+			"start_indent": 0
+		},
+
+		// MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md009.md
+		"MD009": {
+			// Spaces for line break
+			"br_spaces": 2,
+			// Allow spaces for empty lines in list items
+			"list_item_empty_lines": false,
+			// Include unnecessary breaks
+			"strict": false
+		},
+
+		// MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md010.md
+		"MD010": {
+			// Include code blocks
+			"code_blocks": false,
+			// Fenced code languages to ignore
+			"ignore_code_languages": [],
+			// Number of spaces for each hard tab
+			"spaces_per_tab": 4
+		},
+		
+		// MD011/no-reversed-links : Reversed link syntax : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md011.md
+		"MD011": true,
+		
+		// MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md012.md
+		"MD012": {
+			// Consecutive blank lines
+			"maximum": 1
+		},
+
+		// MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md013.md
+		"MD013": false,
+		
+		// MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md014.md
+		"MD014": true,
+
+		// MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md018.md
+		"MD018": true,
+
+		// MD019/no-multiple-space-atx : Multiple spaces after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md019.md
+		"MD019": true,
+
+		// MD020/no-missing-space-closed-atx : No space inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md020.md
+		"MD020": true,
+
+		// MD021/no-multiple-space-closed-atx : Multiple spaces inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md021.md
+		"MD021": true,
+
+		// MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md022.md
+		"MD022": {
+			// Blank lines above heading
+			"lines_above": 1,
+			// Blank lines below heading
+			"lines_below": 1
+		},
+
+		// MD023/heading-start-left : Headings must start at the beginning of the line : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md023.md
+		"MD023": true,
+
+		// MD024/no-duplicate-heading : Multiple headings with the same content : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md024.md
+		"MD024": false,
+		// FIXME: false positives?
+		//{
+		//	// Only check sibling headings
+		//	"allow_different_nesting": true,
+		//	// Only check sibling headings
+		//	"siblings_only": true
+		//},
+
+		// MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md025.md
+		"MD025": {
+			// Heading level
+			"level": 1,
+			// RegExp for matching title in front matter
+			"front_matter_title": "^\\s*title\\s*[:=]"
+		},
+
+		// MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md026.md
+		"MD026": {
+			// Punctuation characters
+			"punctuation": ".,;:!。,;:!"
+		},
+
+		// MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md027.md
+		"MD027": true,
+
+		// MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md028.md
+		"MD028": true,
+
+		// MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md029.md
+		"MD029": false,
+		// FIXME: false positives or broken formatting
+		//{
+		//	// List style
+		//	"style": "ordered"
+		//},
+
+		// MD030/list-marker-space : Spaces after list markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md030.md
+		"MD030": {
+			// Spaces for single-line unordered list items
+			"ul_single": 1,
+			// Spaces for single-line ordered list items
+			"ol_single": 1,
+			// Spaces for multi-line unordered list items
+			"ul_multi": 1,
+			// Spaces for multi-line ordered list items
+			"ol_multi": 1
+		},
+
+		// MD031/blanks-around-fences : Fenced code blocks should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md031.md
+		"MD031": {
+			// Include list items
+			"list_items": false
+		},
+
+		// MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md032.md
+		"MD032": true,
+
+		// MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md033.md
+		"MD033": false,
+		// FIXME: enable and consider fixing
+		//{
+		//	// Allowed elements
+		//	"allowed_elements": []
+		//},
+
+		// MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md034.md
+		"MD034": true,
+
+		// MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md035.md
+		"MD035": {
+			// Horizontal rule style
+			"style": "consistent"
+		},
+
+		// MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md036.md
+		"MD036": false,
+		// FIXME: enable and consider fixing
+		// {
+		// 	// Punctuation characters
+		// 	"punctuation": ".,;:!?。,;:!?"
+		// },
+
+		// MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md037.md
+		"MD037": true,
+
+		// MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md038.md
+		"MD038": true,
+
+		// MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md039.md
+		"MD039": true,
+
+		// MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md040.md
+		"MD040": {
+		// List of languages
+			"allowed_languages": [ "cpp", "json", "sh", "text", "nix", "powershell", "lua" ],
+		// Require language only
+			"language_only": true
+		},
+
+		// MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md041.md
+		"MD041": {
+			// Heading level
+			"level": 1,
+			// RegExp for matching title in front matter
+			"front_matter_title": "^\\s*title\\s*[:=]"
+		},
+
+		// MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md042.md
+		"MD042": true,
+
+		// MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md043.md
+		"MD043": false,
+
+		// MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md044.md
+		"MD044": false,
+
+		// MD045/no-alt-text : Images should have alternate text (alt text) : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md045.md
+		"MD045": false,
+
+		// MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md046.md
+		"MD046": {
+			// Block style
+			"style": "fenced"
+		},
+
+		// MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md047.md
+		"MD047": true,
+		
+		// MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md048.md
+		"MD048": {
+			// Code fence style
+			"style": "backtick"
+		},
+
+		// MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md049.md
+		"MD049": {
+			// Emphasis style
+			"style": "asterisk"
+		},
+
+		// MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md050.md
+		"MD050": {
+			// Strong style
+			"style": "asterisk"
+		},
+		
+
+
+		// MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md051.md
+		"MD051": true,
+
+		// MD052/reference-links-images : Reference links and images should use a label that is defined : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md052.md
+		"MD052": {
+			// Include shortcut syntax
+			"shortcut_syntax": false
+		},
+
+		// MD053/link-image-reference-definitions : Link and image reference definitions should be needed : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md053.md
+		"MD053": {
+			// Ignored definitions
+			"ignored_definitions": [
+			  "//"
+			]
+		},
+
+		// MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md054.md
+		"MD054": {
+			// Allow autolinks
+			"autolink": true,
+			// Allow inline links and images
+			"inline": true,
+			// Allow full reference links and images
+			"full": true,
+			// Allow collapsed reference links and images
+			"collapsed": true,
+			// Allow shortcut reference links and images
+			"shortcut": true,
+			// Allow URLs as inline links
+			"url_inline": true
+		},
+		
+		// MD058 - Tables should be surrounded by blank lines
+		"MD058" : true
+
+	}
+}

File diff suppressed because it is too large
+ 227 - 63
ChangeLog.md


+ 0 - 0
Mods/vcmi/Data/NotoSans-Medium.ttf → Mods/vcmi/Content/Data/NotoSans-Medium.ttf


+ 0 - 0
Mods/vcmi/Data/NotoSerif-Black.ttf → Mods/vcmi/Content/Data/NotoSerif-Black.ttf


+ 0 - 0
Mods/vcmi/Data/NotoSerif-Bold.ttf → Mods/vcmi/Content/Data/NotoSerif-Bold.ttf


+ 0 - 0
Mods/vcmi/Data/NotoSerif-Medium.ttf → Mods/vcmi/Content/Data/NotoSerif-Medium.ttf


+ 0 - 0
Mods/vcmi/Data/s/std.verm → Mods/vcmi/Content/Data/s/std.verm


+ 0 - 0
Mods/vcmi/Data/s/testy.erm → Mods/vcmi/Content/Data/s/testy.erm


+ 0 - 0
Mods/vcmi/Sounds/we5.wav → Mods/vcmi/Content/Sounds/we5.wav


+ 0 - 0
Mods/vcmi/Sprites/PortraitsLarge.json → Mods/vcmi/Content/Sprites/PortraitsLarge.json


+ 0 - 0
Mods/vcmi/Sprites/PortraitsSmall.json → Mods/vcmi/Content/Sprites/PortraitsSmall.json


+ 0 - 0
Mods/vcmi/Data/QuickRecruitmentWindow/CreaturePurchaseCard.png → Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/CreaturePurchaseCard.png


+ 0 - 0
Mods/vcmi/Sprites/QuickRecruitmentWindow/QuickRecruitmentAllButton.def → Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/QuickRecruitmentAllButton.def


+ 0 - 0
Mods/vcmi/Sprites/QuickRecruitmentWindow/QuickRecruitmentNoneButton.def → Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/QuickRecruitmentNoneButton.def


+ 0 - 0
Mods/vcmi/Sprites/QuickRecruitmentWindow/costBackground.png → Mods/vcmi/Content/Sprites/QuickRecruitmentWindow/costBackground.png


+ 0 - 0
Mods/vcmi/Sprites/ScSelC.json → Mods/vcmi/Content/Sprites/ScSelC.json


+ 0 - 0
Mods/vcmi/Data/StackQueueLarge.png → Mods/vcmi/Content/Sprites/StackQueueLarge.png


+ 0 - 0
Mods/vcmi/Data/StackQueueSmall.png → Mods/vcmi/Content/Sprites/StackQueueSmall.png


+ 0 - 0
Mods/vcmi/Data/UnitMaxMovementHighlight.png → Mods/vcmi/Content/Sprites/UnitMaxMovementHighlight.png


+ 0 - 0
Mods/vcmi/Data/UnitMovementHighlight.png → Mods/vcmi/Content/Sprites/UnitMovementHighlight.png


BIN
Mods/vcmi/Content/Sprites/battle/queueDefend.png


BIN
Mods/vcmi/Content/Sprites/battle/queueWait.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/green/empty.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/empty.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/green/fullHex.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/fullHex.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/green/left.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/left.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/green/leftHalf.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/leftHalf.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/green/top.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/top.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeft.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeft.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftCorner.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeftCorner.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/green/topLeftHalfCorner.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/green/topLeftHalfCorner.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json → Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsGreen.json


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/rangeHighlightsRed.json → Mods/vcmi/Content/Sprites/battle/rangeHighlights/rangeHighlightsRed.json


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/red/empty.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/empty.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/red/fullHex.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/fullHex.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/red/left.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/left.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/red/leftHalf.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/leftHalf.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/red/top.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/top.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeft.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeft.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeftCorner.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeftCorner.png


+ 0 - 0
Mods/vcmi/Sprites/battle/rangeHighlights/red/topLeftHalfCorner.png → Mods/vcmi/Content/Sprites/battle/rangeHighlights/red/topLeftHalfCorner.png


+ 8 - 0
Mods/vcmi/Content/Sprites/cprsmall.json

@@ -0,0 +1,8 @@
+{
+	"images" :
+	[
+		// Fix for swapped in H3 icons of Wight and Wraith
+		{ "frame" : 62, "defFile" : "cprsmall.def", "defFrame" : 63},
+		{ "frame" : 63, "defFile" : "cprsmall.def", "defFrame" : 62}
+	]
+}

+ 0 - 0
Mods/vcmi/Data/debug/blocked.png → Mods/vcmi/Content/Sprites/debug/blocked.png


+ 0 - 0
Mods/vcmi/Data/debug/grid.png → Mods/vcmi/Content/Sprites/debug/grid.png


+ 0 - 0
Mods/vcmi/Data/debug/spellRange.png → Mods/vcmi/Content/Sprites/debug/spellRange.png


+ 0 - 0
Mods/vcmi/Data/debug/visitable.png → Mods/vcmi/Content/Sprites/debug/visitable.png


+ 0 - 0
Mods/vcmi/Data/heroWindow/artifactSlotEmpty.png → Mods/vcmi/Content/Sprites/heroWindow/artifactSlotEmpty.png


+ 0 - 0
Mods/vcmi/Data/heroWindow/backpackButtonIcon.png → Mods/vcmi/Content/Sprites/heroWindow/backpackButtonIcon.png


+ 0 - 0
Mods/vcmi/Data/heroWindow/commanderButtonIcon.png → Mods/vcmi/Content/Sprites/heroWindow/commanderButtonIcon.png


+ 0 - 0
Mods/vcmi/Sprites/itpa.json → Mods/vcmi/Content/Sprites/itpa.json


+ 0 - 0
Mods/vcmi/Sprites/lobby/checkbox.json → Mods/vcmi/Content/Sprites/lobby/checkbox.json


+ 0 - 0
Mods/vcmi/Sprites/lobby/checkboxBlueOff.png → Mods/vcmi/Content/Sprites/lobby/checkboxBlueOff.png


+ 0 - 0
Mods/vcmi/Sprites/lobby/checkboxBlueOn.png → Mods/vcmi/Content/Sprites/lobby/checkboxBlueOn.png


+ 0 - 0
Mods/vcmi/Sprites/lobby/checkboxOff.png → Mods/vcmi/Content/Sprites/lobby/checkboxOff.png


+ 0 - 0
Mods/vcmi/Sprites/lobby/checkboxOn.png → Mods/vcmi/Content/Sprites/lobby/checkboxOn.png


+ 0 - 0
Mods/vcmi/Sprites/lobby/delete-normal.png → Mods/vcmi/Content/Sprites/lobby/delete-normal.png


+ 0 - 0
Mods/vcmi/Sprites/lobby/delete-pressed.png → Mods/vcmi/Content/Sprites/lobby/delete-pressed.png


Some files were not shown because too many files changed in this diff