Procházet zdrojové kódy

Merge branch 'master' into 'develop'

Ivan Savenko před 8 měsíci
rodič
revize
0548f325e4
100 změnil soubory, kde provedl 1944 přidání a 463 odebrání
  1. 5 2
      .github/workflows/github.yml
  2. 3 2
      AI/Nullkiller/AIGateway.cpp
  3. 2 2
      AI/Nullkiller/AIUtility.cpp
  4. 1 1
      AI/Nullkiller/AIUtility.h
  5. 56 12
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  6. 7 6
      AI/Nullkiller/Analyzers/ArmyManager.h
  7. 1 0
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  8. 10 2
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  9. 2 1
      AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp
  10. 12 1
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  11. 2 1
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  12. 3 1
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  13. 1 1
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  14. 2 2
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  15. 6 3
      AI/Nullkiller/Engine/Nullkiller.cpp
  16. 148 43
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  17. 3 2
      AI/Nullkiller/Engine/PriorityEvaluator.h
  18. 23 26
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  19. 2 1
      AI/Nullkiller/Pathfinding/Actors.cpp
  20. 2 0
      CMakeLists.txt
  21. 25 0
      CMakePresets.json
  22. 75 0
      ChangeLog.md
  23. binární
      Mods/vcmi/Content/Sprites/radialMenu/upgradeCreatures.png
  24. binární
      Mods/vcmi/Content/Sprites/stackWindow/button-panel.png
  25. 3 5
      Mods/vcmi/Content/config/chinese.json
  26. 23 17
      Mods/vcmi/Content/config/czech.json
  27. 16 10
      Mods/vcmi/Content/config/english.json
  28. 0 2
      Mods/vcmi/Content/config/french.json
  29. 47 19
      Mods/vcmi/Content/config/german.json
  30. 3 5
      Mods/vcmi/Content/config/hungarian.json
  31. 809 0
      Mods/vcmi/Content/config/italian.json
  32. 9 8
      Mods/vcmi/Content/config/polish.json
  33. 3 5
      Mods/vcmi/Content/config/portuguese.json
  34. 4 4
      Mods/vcmi/Content/config/rmg/hdmod/coldshadowsFantasy.json
  35. 0 2
      Mods/vcmi/Content/config/russian.json
  36. 0 2
      Mods/vcmi/Content/config/spanish.json
  37. 39 11
      Mods/vcmi/Content/config/ukrainian.json
  38. 3 5
      Mods/vcmi/Content/config/vietnamese.json
  39. 11 0
      Mods/vcmi/mod.json
  40. 1 1
      android/AndroidManifest.xml
  41. 2 0
      android/vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java
  42. 4 0
      client/CMakeLists.txt
  43. 2 0
      client/CPlayerInterface.cpp
  44. 9 5
      client/CServerHandler.cpp
  45. 23 10
      client/ServerRunner.cpp
  46. 15 5
      client/ServerRunner.h
  47. 51 39
      client/adventureMap/AdventureMapInterface.cpp
  48. 0 3
      client/adventureMap/AdventureMapInterface.h
  49. 13 8
      client/battle/BattleAnimationClasses.cpp
  50. 4 1
      client/battle/BattleAnimationClasses.h
  51. 2 1
      client/battle/BattleEffectsController.cpp
  52. 8 1
      client/battle/BattleEffectsController.h
  53. 1 1
      client/battle/BattleFieldController.cpp
  54. 3 3
      client/battle/BattleInterfaceClasses.cpp
  55. 23 9
      client/battle/BattleStacksController.cpp
  56. 4 3
      client/battle/BattleStacksController.h
  57. 12 13
      client/battle/CreatureAnimation.cpp
  58. 1 4
      client/battle/CreatureAnimation.h
  59. 15 0
      client/eventsSDL/InputHandler.cpp
  60. 3 0
      client/eventsSDL/InputHandler.h
  61. 15 4
      client/eventsSDL/InputSourceTouch.cpp
  62. 3 0
      client/eventsSDL/InputSourceTouch.h
  63. 10 0
      client/globalLobby/GlobalLobbyWidget.cpp
  64. 1 1
      client/gui/CursorHandler.cpp
  65. 1 0
      client/gui/Shortcut.h
  66. 1 0
      client/gui/ShortcutHandler.cpp
  67. 8 0
      client/lobby/CBonusSelection.cpp
  68. 2 0
      client/lobby/CBonusSelection.h
  69. 1 1
      client/lobby/SelectionTab.cpp
  70. 12 3
      client/mainmenu/CHighScoreScreen.cpp
  71. 2 2
      client/mainmenu/CHighScoreScreen.h
  72. 1 1
      client/mapView/MapRenderer.cpp
  73. 3 0
      client/mapView/MapRendererContext.cpp
  74. 1 1
      client/mapView/MapViewController.cpp
  75. 0 2
      client/media/CMusicHandler.cpp
  76. 1 2
      client/media/CMusicHandler.h
  77. 3 3
      client/render/AssetGenerator.cpp
  78. 5 0
      client/render/CanvasImage.cpp
  79. 2 0
      client/render/CanvasImage.h
  80. 0 37
      client/render/ColorFilter.cpp
  81. 0 9
      client/render/ColorFilter.h
  82. 10 5
      client/render/IImage.h
  83. 5 2
      client/renderSDL/FontChain.cpp
  84. 1 1
      client/renderSDL/FontChain.h
  85. 10 6
      client/renderSDL/RenderHandler.cpp
  86. 119 33
      client/renderSDL/ScalableImage.cpp
  87. 7 1
      client/renderSDL/ScalableImage.h
  88. 11 18
      client/renderSDL/ScreenHandler.cpp
  89. 16 0
      client/widgets/CExchangeController.cpp
  90. 1 0
      client/widgets/CExchangeController.h
  91. 13 3
      client/widgets/Images.cpp
  92. 2 1
      client/widgets/Images.h
  93. 11 2
      client/widgets/MiscWidgets.cpp
  94. 1 0
      client/widgets/MiscWidgets.h
  95. 5 0
      client/widgets/ObjectLists.cpp
  96. 2 0
      client/widgets/ObjectLists.h
  97. 3 0
      client/widgets/RadialMenu.cpp
  98. 88 10
      client/windows/CCastleInterface.cpp
  99. 2 0
      client/windows/CCastleInterface.h
  100. 8 4
      client/windows/CCreatureWindow.cpp

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

@@ -29,7 +29,7 @@ jobs:
             before_install: linux_qt5.sh
             preset: linux-gcc-test
           - platform: linux
-            os: ubuntu-20.04
+            os: ubuntu-22.04
             test: 0
             before_install: linux_qt5.sh
             preset: linux-gcc-debug
@@ -246,6 +246,9 @@ jobs:
         if [[ ${{matrix.preset}} == linux-gcc-test ]]
         then
             cmake -DENABLE_CCACHE:BOOL=ON -DCMAKE_C_COMPILER=gcc-14 -DCMAKE_CXX_COMPILER=g++-14 --preset ${{ matrix.preset }}
+        elif [[ ${{matrix.preset}} == linux-gcc-debug ]]
+        then
+            cmake -DENABLE_CCACHE:BOOL=ON -DCMAKE_C_COMPILER=gcc-10 -DCMAKE_CXX_COMPILER=g++-10 --preset ${{ matrix.preset }}
         elif [[ (${{matrix.preset}} == android-conan-ninja-release) && (${{github.ref}} != 'refs/heads/master') ]]
         then
             cmake -DENABLE_CCACHE:BOOL=ON -DANDROID_GRADLE_PROPERTIES="applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily;applicationVariant=daily" --preset ${{ matrix.preset }}
@@ -355,7 +358,7 @@ jobs:
 
   deploy-src:
     if: always() && github.ref == 'refs/heads/master'
-    runs-on: ubuntu-latest
+    runs-on: ubuntu-24.04
     defaults:
       run:
         shell: bash

+ 3 - 2
AI/Nullkiller/AIGateway.cpp

@@ -411,6 +411,7 @@ void AIGateway::heroCreated(const CGHeroInstance * h)
 {
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
+	nullkiller->invalidatePathfinderData(); // new hero needs to look around
 }
 
 void AIGateway::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID)
@@ -929,7 +930,7 @@ void AIGateway::pickBestCreatures(const CArmedInstance * destinationArmy, const
 
 	const CArmedInstance * armies[] = {destinationArmy, source};
 
-	auto bestArmy = nullkiller->armyManager->getBestArmy(destinationArmy, destinationArmy, source);
+	auto bestArmy = nullkiller->armyManager->getBestArmy(destinationArmy, destinationArmy, source, myCb->getTile(source->visitablePos())->getTerrainID());
 
 	for(auto army : armies)
 	{
@@ -983,7 +984,7 @@ void AIGateway::pickBestCreatures(const CArmedInstance * destinationArmy, const
 						&& source->stacksCount() == 1
 						&& (!destinationArmy->hasStackAtSlot(i) || destinationArmy->getCreature(i) == targetCreature))
 					{
-						auto weakest = nullkiller->armyManager->getWeakestCreature(bestArmy);
+						auto weakest = nullkiller->armyManager->getBestUnitForScout(bestArmy, myCb->getTile(source->visitablePos())->getTerrainID());
 						
 						if(weakest->creature == targetCreature)
 						{

+ 2 - 2
AI/Nullkiller/AIUtility.cpp

@@ -774,9 +774,9 @@ bool townHasFreeTavern(const CGTownInstance * town)
 	return canMoveVisitingHeroToGarrison;
 }
 
-uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy)
+uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy, int fortLevel)
 {
-	auto armyStrength = heroArmy->getArmyStrength();
+	auto armyStrength = heroArmy->getArmyStrength(fortLevel);
 
 	if(hero && hero->commander && hero->commander->alive)
 	{

+ 1 - 1
AI/Nullkiller/AIUtility.h

@@ -217,7 +217,7 @@ int64_t getArtifactScoreForHero(const CGHeroInstance * hero, const CArtifactInst
 int64_t getPotentialArtifactScore(const CArtifact * art);
 bool townHasFreeTavern(const CGTownInstance * town);
 
-uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy);
+uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy, int fortLevel = 0);
 
 uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock> start);
 

+ 56 - 12
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -13,8 +13,10 @@
 #include "../Engine/Nullkiller.h"
 #include "../../../CCallback.h"
 #include "../../../lib/mapObjects/MapObjects.h"
+#include "../../../lib/mapping/CMapDefines.h"
 #include "../../../lib/IGameSettings.h"
 #include "../../../lib/GameConstants.h"
+#include "../../../lib/TerrainHandler.h"
 
 namespace NKAI
 {
@@ -76,7 +78,7 @@ std::vector<SlotInfo> ArmyManager::toSlotInfo(std::vector<creInfo> army) const
 
 uint64_t ArmyManager::howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const
 {
-	return howManyReinforcementsCanGet(hero, hero, source);
+	return howManyReinforcementsCanGet(hero, hero, source, ai->cb->getTile(hero->visitablePos())->getTerrainID());
 }
 
 std::vector<SlotInfo> ArmyManager::getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const
@@ -111,17 +113,59 @@ std::vector<SlotInfo> ArmyManager::getSortedSlots(const CCreatureSet * target, c
 	return resultingArmy;
 }
 
-std::vector<SlotInfo>::iterator ArmyManager::getWeakestCreature(std::vector<SlotInfo> & army) const
+std::vector<SlotInfo>::iterator ArmyManager::getBestUnitForScout(std::vector<SlotInfo> & army, const TerrainId & armyTerrain) const
 {
-	auto weakest = boost::min_element(army, [](const SlotInfo & left, const SlotInfo & right) -> bool
+	uint64_t totalPower = 0;
+
+	for (const auto & unit : army)
+		totalPower += unit.power;
+
+	int baseMovementCost = cb->getSettings().getInteger(EGameSettings::HEROES_MOVEMENT_COST_BASE);
+	bool terrainHasPenalty = armyTerrain.hasValue() && armyTerrain.toEntity(VLC)->moveCost != baseMovementCost;
+
+	// arbitrary threshold - don't give scout more than specified part of total AI value of our army
+	uint64_t maxUnitValue = totalPower / 100;
+
+	const auto & movementPointsLimits = cb->getSettings().getVector(EGameSettings::HEROES_MOVEMENT_POINTS_LAND);
+
+	auto fastest = boost::min_element(army, [&](const SlotInfo & left, const SlotInfo & right) -> bool
 	{
-		if(left.creature->getLevel() != right.creature->getLevel())
-			return left.creature->getLevel() < right.creature->getLevel();
-		
-		return left.creature->getMovementRange() > right.creature->getMovementRange();
+		uint64_t leftUnitPower = left.power / left.count;
+		uint64_t rightUnitPower = left.power / left.count;
+		bool leftUnitIsWeak = leftUnitPower < maxUnitValue || left.creature->getLevel() < 4;
+		bool rightUnitIsWeak = rightUnitPower < maxUnitValue || right.creature->getLevel() < 4;
+
+		if (leftUnitIsWeak != rightUnitIsWeak)
+			return leftUnitIsWeak;
+
+		if (terrainHasPenalty)
+		{
+			auto leftNativeTerrain = left.creature->getFactionID().toFaction()->nativeTerrain;
+			auto rightNativeTerrain = right.creature->getFactionID().toFaction()->nativeTerrain;
+
+			if (leftNativeTerrain != rightNativeTerrain)
+			{
+				if (leftNativeTerrain == armyTerrain)
+					return true;
+
+				if (rightNativeTerrain == armyTerrain)
+					return false;
+			}
+		}
+
+		int leftEffectiveMovement = std::min<int>(movementPointsLimits.size() - 1, left.creature->getMovementRange());
+		int rightEffectiveMovement = std::min<int>(movementPointsLimits.size() - 1, right.creature->getMovementRange());
+
+		int leftMovementPointsLimit = movementPointsLimits[leftEffectiveMovement];
+		int rightMovementPointsLimit = movementPointsLimits[rightEffectiveMovement];
+
+		if (leftMovementPointsLimit != rightMovementPointsLimit)
+			return leftMovementPointsLimit > rightMovementPointsLimit;
+
+		return leftUnitPower < rightUnitPower;
 	});
 
-	return weakest;
+	return fastest;
 }
 
 class TemporaryArmy : public CArmedInstance
@@ -134,7 +178,7 @@ public:
 	}
 };
 
-std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const
+std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source, const TerrainId & armyTerrain) const
 {
 	auto sortedSlots = getSortedSlots(target, source);
 
@@ -218,7 +262,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 		&& allowedFactions.size() == alignmentMap.size()
 		&& source->needsLastStack())
 	{
-		auto weakest = getWeakestCreature(resultingArmy);
+		auto weakest = getBestUnitForScout(resultingArmy, armyTerrain);
 
 		if(weakest->count == 1) 
 		{
@@ -398,14 +442,14 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
 	return creaturesInDwellings;
 }
 
-ui64 ArmyManager::howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const
+ui64 ArmyManager::howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source, const TerrainId & armyTerrain) const
 {
 	if(source->stacksCount() == 0)
 	{
 		return 0;
 	}
 
-	auto bestArmy = getBestArmy(armyCarrier, target, source);
+	auto bestArmy = getBestArmy(armyCarrier, target, source, armyTerrain);
 	uint64_t newArmy = 0;
 	uint64_t oldArmy = target->getArmyStrength();
 

+ 7 - 6
AI/Nullkiller/Analyzers/ArmyManager.h

@@ -53,10 +53,11 @@ public:
 	virtual ui64 howManyReinforcementsCanGet(
 		const IBonusBearer * armyCarrier,
 		const CCreatureSet * target,
-		const CCreatureSet * source) const = 0;
+		const CCreatureSet * source,
+		const TerrainId & armyTerrain) const = 0;
 
-	virtual std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const = 0;
-	virtual std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const = 0;
+	virtual std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source, const TerrainId & armyTerrain) const = 0;
+	virtual std::vector<SlotInfo>::iterator getBestUnitForScout(std::vector<SlotInfo> & army, const TerrainId & armyTerrain) const = 0;
 	virtual std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const = 0;
 	virtual std::vector<SlotInfo> toSlotInfo(std::vector<creInfo> creatures) const = 0;
 
@@ -97,9 +98,9 @@ public:
 		uint8_t turn = 0) const override;
 
 	ui64 howManyReinforcementsCanGet(const CGHeroInstance * hero, const CCreatureSet * source) const override;
-	ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
-	std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source) const override;
-	std::vector<SlotInfo>::iterator getWeakestCreature(std::vector<SlotInfo> & army) const override;
+	ui64 howManyReinforcementsCanGet(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source, const TerrainId & armyTerrain) const override;
+	std::vector<SlotInfo> getBestArmy(const IBonusBearer * armyCarrier, const CCreatureSet * target, const CCreatureSet * source, const TerrainId & armyTerrain) const override;
+	std::vector<SlotInfo>::iterator getBestUnitForScout(std::vector<SlotInfo> & army, const TerrainId & armyTerrain) const override;
 	std::vector<SlotInfo> getSortedSlots(const CCreatureSet * target, const CCreatureSet * source) const override;
 	std::vector<SlotInfo> toSlotInfo(std::vector<creInfo> creatures) const override;
 

+ 1 - 0
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

@@ -291,6 +291,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
 				prerequisite.baseCreatureID = info.baseCreatureID;
 				prerequisite.prerequisitesCount++;
 				prerequisite.armyCost = info.armyCost;
+				prerequisite.armyStrength = info.armyStrength;
 				bool haveSameOrBetterFort = false;
 				if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT)
 					haveSameOrBetterFort = true;

+ 10 - 2
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

@@ -459,6 +459,8 @@ void ObjectClusterizer::clusterizeObject(
 			continue;
 		}
 
+		float priority = 0;
+
 		if(path.nodes.size() > 1)
 		{
 			auto blocker = getBlocker(path);
@@ -475,7 +477,10 @@ void ObjectClusterizer::clusterizeObject(
 
 				heroesProcessed.insert(path.targetHero);
 
-				float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
+				for (int prio = PriorityEvaluator::PriorityTier::BUILDINGS; prio <= PriorityEvaluator::PriorityTier::MAX_PRIORITY_TIER; ++prio)
+				{
+					priority = std::max(priority, priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), prio));
+				}
 
 				if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
 					continue;
@@ -498,7 +503,10 @@ void ObjectClusterizer::clusterizeObject(
 
 		heroesProcessed.insert(path.targetHero);
 
-		float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
+		for (int prio = PriorityEvaluator::PriorityTier::BUILDINGS; prio <= PriorityEvaluator::PriorityTier::MAX_PRIORITY_TIER; ++prio)
+		{
+			priority = std::max(priority, priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), prio));
+		}
 
 		if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
 			continue;

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

@@ -57,7 +57,8 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
 				auto reinforcement = ai->armyManager->howManyReinforcementsCanGet(
 					targetHero,
 					targetHero,
-					&*townArmyAvailableToBuy);
+					&*townArmyAvailableToBuy,
+					TerrainId::NONE);
 
 				if(reinforcement)
 					vstd::amin(reinforcement, ai->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town));

+ 12 - 1
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -214,11 +214,15 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 		std::vector<int> pathsToDefend;
 		std::map<const CGHeroInstance *, std::vector<int>> defferedPaths;
+		AIPath* closestWay = nullptr;
 
 		for(int i = 0; i < paths.size(); i++)
 		{
 			auto & path = paths[i];
 
+			if (!closestWay || path.movementCost() < closestWay->movementCost())
+				closestWay = &path;
+
 #if NKAI_TRACE_LEVEL >= 1
 			logAi->trace(
 				"Hero %s can defend town with force %lld in %s turns, cost: %f, path: %s",
@@ -382,7 +386,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 					town->getObjectName());
 #endif
 
-			sequence.push_back(sptr(ExecuteHeroChain(path, town)));
+			ExecuteHeroChain heroChain = ExecuteHeroChain(path, town);
+				
+			if (closestWay)
+			{
+				heroChain.closestWayRatio = closestWay->movementCost() / heroChain.getPath().movementCost();
+			}
+
+			sequence.push_back(sptr(heroChain));
 			composition.addNextSequence(sequence);
 
 			auto firstBlockedAction = path.getFirstBlockedAction();

+ 2 - 1
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

@@ -300,7 +300,8 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
 				ai->armyManager->getBestArmy(
 					path.targetHero,
 					path.heroArmy,
-					upgrader->getUpperArmy()));
+					upgrader->getUpperArmy(),
+					TerrainId::NONE));
 
 			armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
 

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

@@ -58,6 +58,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 
 	ai->dangerHitMap->updateHitMap();
 	int treasureSourcesCount = 0;
+	int bestClosestThreat = UINT8_MAX;
 	
 	for(auto town : towns)
 	{
@@ -118,6 +119,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 					bestScore = score;
 					bestHeroToHire = hero;
 					bestTownToHireFrom = town;
+					bestClosestThreat = closestThreat;
 				}
 			}
 		}
@@ -128,7 +130,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
 	{
 		if (ai->cb->getHeroesInfo().size() == 0
 			|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
-			|| bestHeroToHire->getArmyCost() > GameConstants::HERO_GOLD_COST / 2.0
+			|| (bestHeroToHire->getArmyCost() > GameConstants::HERO_GOLD_COST / 2.0 && (bestClosestThreat < 1 || !ai->buildAnalyzer->isGoldPressureHigh()))
 			|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
 			|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
 		{

+ 1 - 1
AI/Nullkiller/Behaviors/StartupBehavior.cpp

@@ -149,7 +149,7 @@ Goals::TGoalVec StartupBehavior::decompose(const Nullkiller * ai) const
 	{
 		if(!startupTown->visitingHero)
 		{
-			if(ai->armyManager->howManyReinforcementsCanGet(startupTown->getUpperArmy(), startupTown->getUpperArmy(), closestHero) > 200)
+			if(ai->armyManager->howManyReinforcementsCanGet(startupTown->getUpperArmy(), startupTown->getUpperArmy(), closestHero, TerrainId::NONE) > 200)
 			{
 				auto paths = ai->pathfinder->getPathInfo(startupTown->visitablePos());
 

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

@@ -127,9 +127,9 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
 			auto fortLevel = town->fortLevel();
 
 			if (fortLevel == CGTownInstance::EFortLevel::CASTLE)
-				danger = std::max(danger * 2, danger + 10000);
+				danger += 10000;
 			else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
-				danger = std::max(ui64(danger * 1.4), danger + 4000);
+				danger += 4000;
 		}
 
 		return danger;

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

@@ -446,7 +446,7 @@ void Nullkiller::makeTurn()
 #if NKAI_TRACE_LEVEL >= 1
 		int prioOfTask = 0;
 #endif
-		for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio)
+		for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::MAX_PRIORITY_TIER; ++prio)
 		{
 #if NKAI_TRACE_LEVEL >= 1
 			prioOfTask = prio;
@@ -535,7 +535,10 @@ void Nullkiller::makeTurn()
 				else
 					return;
 			}
-			hasAnySuccess = true;
+			else
+			{
+				hasAnySuccess = true;
+			}
 		}
 
 		hasAnySuccess |= handleTrading();
@@ -721,7 +724,7 @@ bool Nullkiller::handleTrading()
 				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
+#if NKAI_TRACE_LEVEL >= 2
 					logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
 #endif
 					haveTraded = true;

+ 148 - 43
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -66,7 +66,8 @@ EvaluationContext::EvaluationContext(const Nullkiller* ai)
 	isArmyUpgrade(false),
 	isHero(false),
 	isEnemy(false),
-	explorePriority(0)
+	explorePriority(0),
+	powerRatio(0)
 {
 }
 
@@ -609,9 +610,6 @@ float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const
 			? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance*>(target))
 			: 0;
 
-	case Obj::KEYMASTER:
-		return 0.6f;
-
 	default:
 		return 0;
 	}
@@ -889,7 +887,14 @@ public:
 
 		Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
 
-		evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
+		if (stayAtTown.getHero() != nullptr && stayAtTown.getHero()->movementPointsRemaining() < 100)
+		{
+			return;
+		}
+
+		if(stayAtTown.town->mageGuildLevel() > 0)
+			evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
+
 		if (evaluationContext.armyReward == 0)
 			evaluationContext.isDefend = true;
 		else
@@ -1018,6 +1023,45 @@ public:
 		if(heroRole == HeroRole::MAIN)
 			evaluationContext.heroRole = heroRole;
 
+		if (hero)
+		{
+			// Assuming Slots() returns a collection of slots with slot.second->getCreatureID() and slot.second->getPower()
+			float heroPower = 0;
+			float totalPower = 0;
+
+			// Map to store the aggregated power of creatures by CreatureID
+			std::map<int, float> totalPowerByCreatureID;
+
+			// Calculate hero power and total power by CreatureID
+			for (auto slot : hero->Slots())
+			{
+				int creatureID = slot.second->getCreatureID();
+				float slotPower = slot.second->getPower();
+
+				// Add the power of this slot to the heroPower
+				heroPower += slotPower;
+
+				// Accumulate the total power for the specific CreatureID
+				if (totalPowerByCreatureID.find(creatureID) == totalPowerByCreatureID.end())
+				{
+					// First time encountering this CreatureID, retrieve total creatures' power
+					totalPowerByCreatureID[creatureID] = ai->armyManager->getTotalCreaturesAvailable(creatureID).power;
+				}
+			}
+
+			// Calculate total power based on unique CreatureIDs
+			for (const auto& entry : totalPowerByCreatureID)
+			{
+				totalPower += entry.second;
+			}
+
+			// Compute the power ratio if total power is greater than zero
+			if (totalPower > 0)
+			{
+				evaluationContext.powerRatio = heroPower / totalPower;
+			}
+		}
+
 		if (target)
 		{
 			evaluationContext.goldReward += evaluationContext.evaluator.getGoldReward(target, hero);
@@ -1030,6 +1074,8 @@ public:
 				evaluationContext.isHero = true;
 			if (target->getOwner().isValidPlayer() && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
 				evaluationContext.isEnemy = true;
+			if (target->ID == Obj::TOWN)
+				evaluationContext.defenseValue = dynamic_cast<const CGTownInstance*>(target)->fortLevel();
 			evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
 			if(evaluationContext.danger > 0)
 				evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
@@ -1169,6 +1215,19 @@ public:
 		evaluationContext.goldCost += cost;
 		evaluationContext.closestWayRatio = 1;
 		evaluationContext.buildingCost += bi.buildCostWithPrerequisites;
+
+		bool alreadyOwn = false;
+		int highestMageGuildPossible = BuildingID::MAGES_GUILD_3;
+		for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo())
+		{
+			if (town->hasBuilt(bi.id))
+				alreadyOwn = true;
+			if (evaluationContext.evaluator.ai->cb->canBuildStructure(town, BuildingID::MAGES_GUILD_5) != EBuildingState::FORBIDDEN)
+				highestMageGuildPossible = BuildingID::MAGES_GUILD_5;
+			else if (evaluationContext.evaluator.ai->cb->canBuildStructure(town, BuildingID::MAGES_GUILD_4) != EBuildingState::FORBIDDEN)
+				highestMageGuildPossible = BuildingID::MAGES_GUILD_4;
+		}
+
 		if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
 			evaluationContext.isTradeBuilding = true;
 
@@ -1183,14 +1242,19 @@ public:
 			if(bi.baseCreatureID == bi.creatureID)
 			{
 				evaluationContext.addNonCriticalStrategicalValue((0.5f + 0.1f * bi.creatureLevel) / (float)bi.prerequisitesCount);
-				evaluationContext.armyReward += bi.armyStrength;
+				evaluationContext.armyReward += bi.armyStrength * 1.5;
 			}
 			else
 			{
 				auto potentialUpgradeValue = evaluationContext.evaluator.getUpgradeArmyReward(buildThis.town, bi);
 				
 				evaluationContext.addNonCriticalStrategicalValue(potentialUpgradeValue / 10000.0f / (float)bi.prerequisitesCount);
-				evaluationContext.armyReward += potentialUpgradeValue / (float)bi.prerequisitesCount;
+				if(bi.id.IsDwelling())
+					evaluationContext.armyReward += bi.armyStrength - evaluationContext.evaluator.ai->armyManager->evaluateStackPower(bi.baseCreatureID.toCreature(), bi.creatureGrows);
+				else //This is for prerequisite-buildings
+					evaluationContext.armyReward += evaluationContext.evaluator.ai->armyManager->evaluateStackPower(bi.baseCreatureID.toCreature(), bi.creatureGrows);
+				if(alreadyOwn)
+					evaluationContext.armyReward /= bi.buildCostWithPrerequisites.marketValue();
 			}
 		}
 		else if(bi.id == BuildingID::CITADEL || bi.id == BuildingID::CASTLE)
@@ -1201,9 +1265,14 @@ 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())
+			if (!alreadyOwn && evaluationContext.evaluator.ai->cb->canBuildStructure(buildThis.town, highestMageGuildPossible) != EBuildingState::FORBIDDEN)
 			{
-				evaluationContext.armyInvolvement += hero->getArmyCost();
+				for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo())
+				{
+					if(hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER) + hero->getPrimSkillLevel(PrimarySkill::KNOWLEDGE) > hero->getPrimSkillLevel(PrimarySkill::ATTACK) + hero->getPrimSkillLevel(PrimarySkill::DEFENSE)
+						&& hero->manaLimit() > 30)
+						evaluationContext.armyReward += hero->getArmyCost();
+				}
 			}
 		}
 		int sameTownBonus = 0;
@@ -1333,18 +1402,35 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 	else
 	{
 		float score = 0;
-		const bool amIInDanger = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0);
-		const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget();
+		bool currentPositionThreatened = false;
+		if (task->hero)
+		{
+			auto currentTileThreat = ai->dangerHitMap->getTileThreat(task->hero->visitablePos());
+			if (currentTileThreat.fastestDanger.turn < 1 && currentTileThreat.fastestDanger.danger > task->hero->getTotalStrength())
+				currentPositionThreatened = true;
+		}
+		if (priorityTier == PriorityTier::FAR_HUNTER_GATHER && currentPositionThreatened == false)
+		{
+#if NKAI_TRACE_LEVEL >= 2
+			logAi->trace("Skip FAR_HUNTER_GATHER because hero is not threatened.");
+#endif
+			return 0;
+		}
+		const bool amIInDanger = ai->cb->getTownsInfo().empty();
+		const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget() * evaluationContext.powerRatio > 0 ? ai->settings->getMaxArmyLossTarget() * evaluationContext.powerRatio : 1.0;
+		float dangerThreshold = 1;
+		dangerThreshold *= evaluationContext.powerRatio > 0 ? evaluationContext.powerRatio : 1.0;
 
 		bool arriveNextWeek = false;
 		if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7 && priorityTier < PriorityTier::FAR_KILL)
 			arriveNextWeek = true;
 
 #if NKAI_TRACE_LEVEL >= 2
-		logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %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 isEnemy: %d arriveNextWeek: %d",
+		logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, maxWillingToLose: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %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, dangerThreshold: %f explorePriority: %d isDefend: %d isEnemy: %d arriveNextWeek: %d powerRatio: %f",
 			priorityTier,
 			task->toString(),
 			evaluationContext.armyLossPersentage,
+			maxWillingToLose,
 			(int)evaluationContext.turn,
 			evaluationContext.movementCostByRole[HeroRole::MAIN],
 			evaluationContext.movementCostByRole[HeroRole::SCOUT],
@@ -1362,23 +1448,27 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 			evaluationContext.conquestValue,
 			evaluationContext.closestWayRatio,
 			evaluationContext.enemyHeroDangerRatio,
+			dangerThreshold,
 			evaluationContext.explorePriority,
 			evaluationContext.isDefend,
 			evaluationContext.isEnemy,
-			arriveNextWeek);
+			arriveNextWeek,
+			evaluationContext.powerRatio);
 #endif
 
 		switch (priorityTier)
 		{
 			case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach
 			{
-				if (evaluationContext.turn > 0)
+				if (evaluationContext.turn > 0 || evaluationContext.isExchange)
 					return 0;
 				if (evaluationContext.movementCost >= 1)
 					return 0;
+				if (evaluationContext.defenseValue < 2 && evaluationContext.enemyHeroDangerRatio > dangerThreshold)
+					return 0;
 				if(evaluationContext.conquestValue > 0)
 					score = evaluationContext.armyInvolvement;
-				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
+				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > dangerThreshold && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
@@ -1388,23 +1478,47 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 			}
 			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)
+				//No point defending if we don't have defensive-structures
+				if (evaluationContext.defenseValue < 2)
+					return 0;
+				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
+				if (evaluationContext.closestWayRatio < 1.0)
+					return 0;
+				if (evaluationContext.isEnemy && evaluationContext.turn > 0)
+					return 0;
+				if (evaluationContext.isDefend && evaluationContext.threatTurns <= evaluationContext.turn)
+				{
+					const float OPTIMAL_PERCENTAGE = 0.75f; // We want army to be 75% of the threat
+					float optimalStrength = evaluationContext.threat * OPTIMAL_PERCENTAGE;
+
+					// Calculate how far the army is from optimal strength
+					float deviation = std::abs(evaluationContext.armyInvolvement - optimalStrength);
+
+					// Convert deviation to a percentage of the threat to normalize it
+					float deviationPercentage = deviation / evaluationContext.threat;
+
+					// Calculate score: 1.0 is perfect, decreasing as deviation increases
+					score = 1.0f / (1.0f + deviationPercentage);
+
+					// Apply turn penalty to still prefer earlier moves when scores are close
+					score = score / (evaluationContext.turn + 1);
+				}
 				break;
 			}
 			case PriorityTier::KILL: //Take towns / kill heroes that are further away
 				//FALL_THROUGH
 			case PriorityTier::FAR_KILL:
 			{
+				if (evaluationContext.defenseValue < 2 && evaluationContext.enemyHeroDangerRatio > dangerThreshold)
+					return 0;
 				if (evaluationContext.turn > 0 && evaluationContext.isHero)
 					return 0;
 				if (arriveNextWeek && evaluationContext.isEnemy)
 					return 0;
 				if (evaluationContext.conquestValue > 0)
 					score = evaluationContext.armyInvolvement;
-				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
+				if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > dangerThreshold && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
@@ -1413,24 +1527,9 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					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;
-				if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
-					return 0;
-				score = 1000;
-				if (evaluationContext.movementCost > 0)
-					score /= evaluationContext.movementCost;
-				break;
-			}
 			case PriorityTier::HIGH_PRIO_EXPLORE:
 			{
-				if (evaluationContext.enemyHeroDangerRatio > 1)
+				if (evaluationContext.enemyHeroDangerRatio > dangerThreshold)
 					return 0;
 				if (evaluationContext.explorePriority != 1)
 					return 0;
@@ -1447,17 +1546,15 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 				//FALL_THROUGH
 			case PriorityTier::FAR_HUNTER_GATHER:
 			{
-				if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
+				if (evaluationContext.enemyHeroDangerRatio > dangerThreshold && !evaluationContext.isDefend && priorityTier != PriorityTier::FAR_HUNTER_GATHER)
 					return 0;
 				if (evaluationContext.buildingCost.marketValue() > 0)
 					return 0;
-				if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0))
+				if (priorityTier != PriorityTier::FAR_HUNTER_GATHER && evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio > dangerThreshold || 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)
+				if (priorityTier != PriorityTier::FAR_HUNTER_GATHER && ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > dangerThreshold))
 					return 0;
 				if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
 					return 0;
@@ -1475,12 +1572,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 					score = 1000;
 					if (evaluationContext.movementCost > 0)
 						score /= evaluationContext.movementCost;
+					if(priorityTier == PriorityTier::FAR_HUNTER_GATHER && evaluationContext.enemyHeroDangerRatio > 0)
+						score /= evaluationContext.enemyHeroDangerRatio;
 				}
 				break;
 			}
 			case PriorityTier::LOW_PRIO_EXPLORE:
 			{
-				if (evaluationContext.enemyHeroDangerRatio > 1)
+				if (evaluationContext.enemyHeroDangerRatio > dangerThreshold)
 					return 0;
 				if (evaluationContext.explorePriority != 3)
 					return 0;
@@ -1495,7 +1594,7 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 			}
 			case PriorityTier::DEFEND: //Defend whatever if nothing else is to do
 			{
-				if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
+				if (evaluationContext.enemyHeroDangerRatio > dangerThreshold)
 					return 0;
 				if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
 					score = evaluationContext.armyInvolvement;
@@ -1536,9 +1635,15 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
 						TResources needed = evaluationContext.buildingCost - resourcesAvailable;
 						needed.positive();
 						int turnsTo = needed.maxPurchasableCount(income);
+						bool haveEverythingButGold = true;
+						for (int i = 0; i < GameConstants::RESOURCE_QUANTITY; i++)
+						{
+							if (i != GameResID::GOLD && resourcesAvailable[i] < evaluationContext.buildingCost[i])
+								haveEverythingButGold = false;
+						}
 						if (turnsTo == INT_MAX)
 							return 0;
-						else
+						if (!haveEverythingButGold)
 							score /= turnsTo;
 					}
 				}

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

@@ -84,6 +84,7 @@ struct DLL_EXPORT EvaluationContext
 	bool isHero;
 	bool isEnemy;
 	int explorePriority;
+	float powerRatio;
 
 	EvaluationContext(const Nullkiller * ai);
 
@@ -114,13 +115,13 @@ public:
 		INSTAKILL,
 		INSTADEFEND,
 		KILL,
-		UPGRADE,
 		HIGH_PRIO_EXPLORE,
 		HUNTER_GATHER,
 		LOW_PRIO_EXPLORE,
 		FAR_KILL,
+		DEFEND,
 		FAR_HUNTER_GATHER,
-		DEFEND
+		MAX_PRIORITY_TIER = FAR_HUNTER_GATHER
 	};
 
 private:

+ 23 - 26
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

@@ -583,42 +583,28 @@ public:
 
 bool AINodeStorage::calculateHeroChain()
 {
-	std::random_device randomDevice;
-	std::mt19937 randomEngine(randomDevice());
-
 	heroChainPass = EHeroChainPass::CHAIN;
 	heroChain.clear();
 
 	std::vector<int3> data(committedTiles.begin(), committedTiles.end());
 
-	if(data.size() > 100)
-	{
-		boost::mutex resultMutex;
-
-		std::shuffle(data.begin(), data.end(), randomEngine);
-
-		tbb::parallel_for(tbb::blocked_range<size_t>(0, data.size()), [&](const tbb::blocked_range<size_t>& r)
-		{
-			//auto r = blocked_range<size_t>(0, data.size());
-			HeroChainCalculationTask task(*this, data, chainMask, heroChainTurn);
-
-			task.execute(r);
+	int maxConcurrency = tbb::this_task_arena::max_concurrency();
+	std::vector<std::vector<CGPathNode *>> results(maxConcurrency);
 
-			{
-				boost::lock_guard<boost::mutex> resultLock(resultMutex);
+	logAi->trace("Caculating hero chain for %d items", data.size());
 
-				task.flushResult(heroChain);
-			}
-		});
-	}
-	else
+	tbb::parallel_for(tbb::blocked_range<size_t>(0, data.size()), [&](const tbb::blocked_range<size_t>& r)
 	{
-		auto r = tbb::blocked_range<size_t>(0, data.size());
 		HeroChainCalculationTask task(*this, data, chainMask, heroChainTurn);
 
+		int ourThread = tbb::this_task_arena::current_thread_index();
 		task.execute(r);
-		task.flushResult(heroChain);
-	}
+		task.flushResult(results.at(ourThread));
+	});
+
+	// FIXME: potentially non-deterministic behavior due to parallel_for
+	for (const auto & result : results)
+		vstd::concatenate(heroChain, result);
 
 	committedTiles.clear();
 
@@ -1464,9 +1450,20 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
 			}
 		}
 
+		int fortLevel = 0;
+		auto visitableObjects = cb->getVisitableObjs(pos);
+		for (auto obj : visitableObjects)
+		{
+			if (objWithID<Obj::TOWN>(obj))
+			{
+				auto town = dynamic_cast<const CGTownInstance*>(obj);
+				fortLevel = town->fortLevel();
+			}
+		}
+
 		path.targetObjectArmyLoss = evaluateArmyLoss(
 			path.targetHero,
-			getHeroArmyStrengthWithCommander(path.targetHero, path.heroArmy),
+			getHeroArmyStrengthWithCommander(path.targetHero, path.heroArmy, fortLevel),
 			path.targetObjectDanger);
 
 		path.chainMask = node.actor->chainMask;

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

@@ -13,6 +13,7 @@
 #include "../Engine/Nullkiller.h"
 #include "../../../CCallback.h"
 #include "../../../lib/mapObjects/MapObjects.h"
+#include "../../../lib/mapping/CMapDefines.h"
 #include "../../../lib/pathfinder/TurnInfo.h"
 #include "Actions/BuyArmyAction.h"
 
@@ -394,7 +395,7 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
 HeroExchangeArmy * HeroExchangeMap::pickBestCreatures(const CCreatureSet * army1, const CCreatureSet * army2) const
 {
 	auto * target = new HeroExchangeArmy();
-	auto bestArmy = ai->armyManager->getBestArmy(actor->hero, army1, army2);
+	auto bestArmy = ai->armyManager->getBestArmy(actor->hero, army1, army2, ai->cb->getTile(actor->hero->visitablePos())->getTerrainID());
 
 	for(auto & slotInfo : bestArmy)
 	{

+ 2 - 0
CMakeLists.txt

@@ -109,6 +109,8 @@ include(CMakeDependentOption)
 cmake_dependent_option(ENABLE_INNOEXTRACT "Enable innoextract for GOG file extraction in launcher" ON "ENABLE_LAUNCHER" OFF)
 cmake_dependent_option(ENABLE_GITVERSION "Enable Version.cpp with Git commit hash" ON "NOT ENABLE_GOLDMASTER" OFF)
 
+option(VCMI_PORTMASTER "PortMaster build" OFF)
+
 ############################################
 #        Miscellaneous options             #
 ############################################

+ 25 - 0
CMakePresets.json

@@ -319,6 +319,25 @@
             "cacheVariables": {
                 "ANDROID_GRADLE_PROPERTIES": "applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily;applicationVariant=daily"
             }
+        },
+        {
+            "name": "portmaster-release",
+            "displayName": "PortMaster",
+            "description": "VCMI PortMaster",
+            "inherits": "default-release",
+            "cacheVariables": {
+                "CMAKE_BUILD_TYPE": "Release",
+                "CMAKE_INSTALL_PREFIX": ".",
+                "ENABLE_DEBUG_CONSOLE": "OFF",
+                "ENABLE_EDITOR": "OFF",
+                "ENABLE_GITVERSION": "OFF",
+                "ENABLE_LAUNCHER": "OFF",
+                "ENABLE_SERVER": "OFF",
+                "ENABLE_TRANSLATIONS": "OFF",
+                "FORCE_BUNDLED_FL": "ON",
+                "ENABLE_GOLDMASTER": "ON",
+                "VCMI_PORTMASTER": "ON"
+            }
         }
     ],
     "buildPresets": [
@@ -447,6 +466,12 @@
             "name": "android-daily-release",
             "configurePreset": "android-daily-release",
             "inherits": "android-conan-ninja-release"
+        },
+        {
+            "name": "portmaster-release",
+            "configurePreset": "portmaster-release",
+            "inherits": "default-release",
+            "configuration": "Release"
         }
     ],
     "testPresets": [

+ 75 - 0
ChangeLog.md

@@ -1,5 +1,80 @@
 # VCMI Project Changelog
 
+## 1.6.5 -> 1.6.6
+
+### General
+
+* Game no longer requires local network connection for single player games
+* Reduced size of obstacle-filled junction zones in Coldshadow Fantasy template
+* Upscaling filter xbrz x2 is now enabled by default on mobile systems
+* Fixes failure to import Chronicles on Windows with non-ascii characters in username
+* Added support for importing Chronicles using old All-in-One installer from gog.com
+* It is now possible to enable portrait mode on mobile systems.
+* Fixed grey bar at top of screen when returning to app while in game on Android
+
+### Stability
+
+* Fixed possible crash on opening unit description with unavailable upgrades
+* Fixed crash on winning game after last player loses the game due to not controlling a town for 7 days
+
+### Interface
+
+* Pressing Q during hero exchange will now swap both army and artifacts and will no longer trigger a quest log
+* Spellbook search is no longer enabled by default, allowing standard h3 shortcuts to work. Search can now be activated by pressing Tab
+* Ctrl/Shift + click on arrow buttons below creature slots during hero exchange now works in the familiar way from hd mod
+* On mobile systems, clicking on a blocked tile of a visitable object on the adventure map will now build a path to it
+* It is now possible to activate the adventure map overlay on the mobile system using the two-finger tap gesture
+* Fixed incorrect pinch event calculation that caused problems when zooming with touchscreen gestures
+* Game now displays both total cost in movement points and estimated time to arrive in turns when hovering over an accessible location
+* Artifact sort buttons in the Hero Backpack window now have correct text describing the sort order
+* Fixed non-standard color handling for shadows under selection highlight in creature animations from mods such as HotA's Iron Golem
+* Effects such as Bloodlust, Clone, and Petrify will now display correctly when xbrz is in use
+* Fixed broken Chronicles campaign screen available with new main menu themes mod
+* Fixed empty bonus shown in unit info window when unit is in Necropolis with Cover of Darkness built
+* Right-clicking on the difficulty button will now display the difficulty description popup
+* Fixed regression causing two minus signs in Fountain of Fortune description
+* Added option to upgrade all creatures in the radial menu when in town
+* Added option to display remaining unit health in the form of a health bar
+* Fixed regression that caused unavailable tiles to be displayed on the left and right sides of the battlefield when hovering with the mouse
+* Fixed regression that caused all spells to be displayed as having a duration of 16 rounds
+* Scrolling in the lobby window now only happens when hovering over the appropriate item, instead of scrolling all scrollable widgets at once
+* Fixed regression that caused black pixels on some hero portraits in mods that use 8-bit palette images
+* Fixed memory leak when upscaling images with xbrz filter
+* Fixed creature windows text align and buttons background
+
+### Mechanics
+
+* It is no longer possible to attack heroes standing on a visitable object from blocked tiles or from water when the attacker uses Fly
+* Fixed regression from 1.6 that caused multiple taverns in towns of the same faction to not be counted towards the level of information available for the thieves' guild
+* Fixed regression that caused Cove towns placed on map to be replaced with Castles on HotA maps
+* The amount of gold a player can receive from a bonfire is now always equal to the amount of rare resources received multiplied by 100
+* Disabled default victory conditions on all Elixir of Life campaign maps that require an artifact to be found, in line with H3
+
+### Nullkiller AI
+
+* Improved scoring of town buildings by the AI
+* AI will now prefer to give faster units to its scout heroes to optimize their movement points in future turns
+* Fixed AI not constructing prerequisites for town buildings in some cases, like not building Stables when attempting to build Training Grounds
+* AI will now avoid recruiting heroes if AI is low on gold or if the town is threatened by an enemy hero
+* AI will no longer attempt to use more than one hero to defend a town
+* AI will now devalue non-flying units when attacking towns with fortifications to prevent suicides against castles
+* Increased the priority of building unupgraded dwellings, as they provide units that can be hired immediately, rather than next week like citadels and castles
+* When multiple cities are threatened, the AI will now prefer to defend the one that takes the least number of turns to reach
+* Fixed AI attempting to restore mana points in town without a mage guild built
+* Reduced AI prioritization of army merging to the same level as general gathering
+* AI will now prioritize army merging before attacking enemies
+* Increased AI defense prioritization
+* AI will no longer leave the defense of a threatened town in order to bring the army to another hero
+* AI will no longer send heroes to die outside of towns that already have a garrisoning hero inside, if there's a stronger enemy hero lurking around the town
+* AI will no longer focus excessively on reaching Keymaster tents
+* AI will no longer rush towns that don't have a citadel or better if there is a strong enemy hero in the area
+* AI will no longer try to maximize defenses by using the strongest defender possible, but will instead try to use the most appropriate defender
+* Heroes that are currently threatened will be braver and not worry about attacking things that are also threatened if nothing safe is in range
+
+### Launcher
+
+* Added context menu for mod lists that allows disabling, enabling, installing, uninstalling, updating, opening installed mod location, and opening mod repository
+
 ## 1.6.4 -> 1.6.5
 
 ### General

binární
Mods/vcmi/Content/Sprites/radialMenu/upgradeCreatures.png


binární
Mods/vcmi/Content/Sprites/stackWindow/button-panel.png


+ 3 - 5
Mods/vcmi/Content/config/chinese.json

@@ -23,8 +23,6 @@
 	"vcmi.adventureMap.noTownWithTavern"       : "没有酒馆可供查看。",
 	"vcmi.adventureMap.spellUnknownProblem"    : "无此魔法的信息。",
 	"vcmi.adventureMap.playerAttacked"         : "玩家遭受攻击: %s",
-	"vcmi.adventureMap.moveCostDetails"        : "移动点数 - 花费: %TURNS 轮 + %POINTS 点移动力, 剩余移动力: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns" : "移动点数 - 花费: %POINTS 点移动力, 剩余移动力: %REMAINING",
 	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(移动点数: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "抱歉,重放对手行动功能目前暂未实现!",
 
@@ -422,11 +420,11 @@
 	"vcmi.heroWindow.openBackpack.hover" : "开启宝物背包界面",
 	"vcmi.heroWindow.openBackpack.help"  : "用更大的界面显示所有获得的宝物",
 	"vcmi.heroWindow.sortBackpackByCost.hover"  : "按价格排序",
-	"vcmi.heroWindow.sortBackpackByCost.help"  : "将行囊里的宝物按价格排序。",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "{按价格排序}\n\n将行囊里的宝物按价格排序。",
 	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "按装备槽排序",
-	"vcmi.heroWindow.sortBackpackBySlot.help"  : "将行囊里的宝物按装备槽排序。",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "{按装备槽排序}\n\n将行囊里的宝物按装备槽排序。",
 	"vcmi.heroWindow.sortBackpackByClass.hover"  : "按类型排序",
-	"vcmi.heroWindow.sortBackpackByClass.help"  : "将行囊里的宝物按装备槽排序:低级宝物、中级宝物、高级宝物、圣物。",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "{按类型排序}\n\n将行囊里的宝物按装备槽排序:低级宝物、中级宝物、高级宝物、圣物。",
 	"vcmi.heroWindow.fusingArtifact.fusing" : "你已拥有融合%s所需的全部组件,想现在进行融合吗?{所有组件在融合后将被消耗。}",
 
 	"vcmi.tavernWindow.inviteHero"  : "邀请英雄",

+ 23 - 17
Mods/vcmi/Content/config/czech.json

@@ -23,9 +23,9 @@
 	"vcmi.adventureMap.noTownWithTavern"       : "Nejsou dostupná žádná města s putykou!",
 	"vcmi.adventureMap.spellUnknownProblem"    : "Neznámý problém s tímto kouzlem! Další informace nejsou k dispozici.",
 	"vcmi.adventureMap.playerAttacked"         : "Hráč byl napaden: %s",
-	"vcmi.adventureMap.moveCostDetails"        : "Body pohybu - Cena: %TURNS tahů + %POINTS bodů, zbylé body: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Body pohybu - Cena: %POINTS bodů, zbylé body: %REMAINING",
-	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(Body pohybu: %REMAINING / %POINTS)",
+	"vcmi.adventureMap.moveCostDetails"                  : "Přesun sem tě bude stát {%TOTAL} bodů (za {%TURNS} tahů a {%POINTS} bodů). Po přesunu ti zbyde {%REMAINING} bodů.",
+	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Přesun sem tě bude stát {%POINTS} bodů. Po přesunu ti zbyde {%REMAINING} bodů.",
+	"vcmi.adventureMap.movementPointsHeroInfo"           : "(Body pohybu: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Omlouváme se, přehrání tahu soupeře ještě není implementováno!",
 
 	"vcmi.bonusSource.artifact" : "Artefakt",
@@ -68,6 +68,7 @@
 	"vcmi.radialWheel.heroGetArtifacts" : "Získat artefakty od jiného hrdiny",
 	"vcmi.radialWheel.heroSwapArtifacts" : "Vyměnit artefakty s jiným hrdinou",
 	"vcmi.radialWheel.heroDismiss" : "Propustit hrdinu",
+	"vcmi.radialWheel.upgradeCreatures" : "Vylepšit všechny jednotky",
 
 	"vcmi.radialWheel.moveTop" : "Přesunout nahoru",
 	"vcmi.radialWheel.moveUp" : "Posunout výše",
@@ -86,9 +87,9 @@
 
 	"vcmi.spellBook.search" : "Hledat",
 
-	"vcmi.spellResearch.canNotAfford" : "Nemáte dostatek prostředků k nahrazení {%SPELL1} za {%SPELL2}. Stále však můžete toto kouzlo zrušit a pokračovat ve výzkumu dalších kouzel.",
-	"vcmi.spellResearch.comeAgain" : "Výzkum už byl dnes proveden. Vraťte se zítra.",
-	"vcmi.spellResearch.pay" : "Chcete nahradit {%SPELL1} za {%SPELL2}? Nebo zrušit toto kouzlo a pokračovat ve výzkumu dalších kouzel?",
+	"vcmi.spellResearch.canNotAfford" : "Nemáš dostatek prostředků na výměnu kouzla {%SPELL1} za {%SPELL2}. Můžeš ho však odstranit a pokračovat ve výzkumu.",
+	"vcmi.spellResearch.comeAgain" : "Výzkum už byl dnes proveden. Vrať se zítra.",
+	"vcmi.spellResearch.pay" : "Chceš nahradit {%SPELL1} za {%SPELL2}? Nebo zrušit toto kouzlo a pokračovat ve výzkumu dalších kouzel?",
 	"vcmi.spellResearch.research" : "Prozkoumat toto kouzlo",
 	"vcmi.spellResearch.skip" : "Přeskočit toto kouzlo",
 	"vcmi.spellResearch.abort" : "Přerušit",
@@ -168,10 +169,10 @@
 	"vcmi.lobby.login.as" : "Přihlásit se jako %s",
 	"vcmi.lobby.login.spectator" : "Divák",
 	"vcmi.lobby.header.rooms" : "Herní místnosti - %d",
-	"vcmi.lobby.header.channels" : "Kanály konverzace",
-	"vcmi.lobby.header.chat.global" : "Globální konverzace hry - %s", // %s -> language name
-	"vcmi.lobby.header.chat.match" : "Konverzace předchozí hry %s", // %s -> game start date & time
-	"vcmi.lobby.header.chat.player" : "Soukromá konverzace s %s", // %s -> nickname of another player
+	"vcmi.lobby.header.channels" : "Kanály chatu",
+	"vcmi.lobby.header.chat.global" : "Globální chat hry - %s", // %s -> language name
+	"vcmi.lobby.header.chat.match" : "Chat předchozí hry %s", // %s -> game start date & time
+	"vcmi.lobby.header.chat.player" : "Soukromý chat s %s", // %s -> nickname of another player
 	"vcmi.lobby.header.history" : "Vaše předchozí hry",
 	"vcmi.lobby.header.players" : "Online hráči - %d",
 	"vcmi.lobby.match.solo" : "Hra jednoho hráče",
@@ -185,7 +186,7 @@
 	"vcmi.lobby.room.description.load" : "Pro start hry načtěte uloženou hru.",
 	"vcmi.lobby.room.description.limit" : "Až %d hráčů se může připojit do vaší místnosti (včetně vás).",
 	"vcmi.lobby.invite.header" : "Pozvat hráče",
-	"vcmi.lobby.invite.notification" : "Pozval vás hráč do jejich soukromé místnosti. Nyní se do ní můžete připojit.",
+	"vcmi.lobby.invite.notification" : "Hráč vás pozval do své soukromé místnosti. Nyní se k ní můžete připojit.",
 	"vcmi.lobby.preview.title" : "Připojit se do herní místnosti",
 	"vcmi.lobby.preview.subtitle" : "Hra na %s, pořádána %s", //TL Note: 1) name of map or RMG template 2) nickname of game host
 	"vcmi.lobby.preview.version" : "Verze hry:",
@@ -216,9 +217,9 @@
 	"vcmi.lobby.pvp.coin.hover" : "Mince",
 	"vcmi.lobby.pvp.coin.help" : "Hodí mincí",
 	"vcmi.lobby.pvp.randomTown.hover" : "Náhodné město",
-	"vcmi.lobby.pvp.randomTown.help" : "Napsat náhodné město do konvezace",
+	"vcmi.lobby.pvp.randomTown.help" : "Napsat náhodné město do chatu",
 	"vcmi.lobby.pvp.randomTownVs.hover" : "Náhodné město vs.",
-	"vcmi.lobby.pvp.randomTownVs.help" : "Napsat 2 náhodná města do konvezace",
+	"vcmi.lobby.pvp.randomTownVs.help" : "Napsat 2 náhodná města do chatu",
 	"vcmi.lobby.pvp.versus" : "vs.",
 
 	"vcmi.client.errors.invalidMap" : "{Neplatná mapa nebo kampaň}\n\nChyba při startu hry! Vybraná mapa nebo kampaň může být neplatná nebo poškozená. Důvod:\n%s",
@@ -363,6 +364,8 @@
 	"vcmi.battleOptions.endWithAutocombat.help" : "{Přeskočit bitvu}\n\nAutomatický boj okamžitě dohraje bitvu do konce.",
 	"vcmi.battleOptions.showQuickSpell.hover" : "Zobrazit rychlý panel kouzel",
 	"vcmi.battleOptions.showQuickSpell.help" : "{Zobrazit rychlý panel kouzel}\n\nZobrazí panel pro rychlý výběr kouzel.",
+	"vcmi.battleOptions.showHealthBar.hover": "Zobrazit ukazatel zdraví",
+	"vcmi.battleOptions.showHealthBar.help": "{Zobrazit ukazatel zdraví}\n\nZobrazí ukazatel, který znázorňuje, kolik zdraví zbývá, než jednotka zemře.",
 
 	"vcmi.adventureMap.revisitObject.hover" : "Znovu navštívit objekt",
 	"vcmi.adventureMap.revisitObject.help" : "{Znovu navštívit objekt}\n\nPokud hrdina právě stojí na objektu na mapě, může toto místo znovu navštívit.",
@@ -415,6 +418,9 @@
 	"vcmi.townStructure.bank.borrow" : "Vstupujete do banky. Bankéř vás spatří a říká: \"Máme pro vás speciální nabídku. Můžete si vzít půjčku 2500 zlata na 5 dní. Každý den budete muset splácet 500 zlata.\"",
 	"vcmi.townStructure.bank.payBack" : "Vstupujete do banky. Bankéř vás spatří a říká: \"Již jste si vzali půjčku. Nejprve ji splaťte, než si vezmete další.\"",
 
+	"vcmi.townWindow.upgradeAll.notAllUpgradable" : "Nemáte dostatek surovin na vylepšení všech jednotek. Chcete vylepšit následující jednotky?",
+	"vcmi.townWindow.upgradeAll.notUpgradable" : "Nemáte dostatek surovin na vylepšení žádné z jednotek.",
+
 	"vcmi.logicalExpressions.anyOf"  : "Nějaké z následujících:",
 	"vcmi.logicalExpressions.allOf"  : "Všechny následující:",
 	"vcmi.logicalExpressions.noneOf" : "Žádné z následujících:",
@@ -424,11 +430,11 @@
 	"vcmi.heroWindow.openBackpack.hover" : "Otevřít okno s artefakty",
 	"vcmi.heroWindow.openBackpack.help"  : "Otevře okno, které umožňuje snadnější správu artefaktů v batohu.",
 	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Seřadit podle ceny",
-	"vcmi.heroWindow.sortBackpackByCost.help"  : "Seřadí artefakty v batohu podle ceny.",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "{Seřadit podle ceny}\n\nSeřadí artefakty v batohu podle ceny.",
 	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Seřadit podle slotu",
-	"vcmi.heroWindow.sortBackpackBySlot.help"  : "Seřadí artefakty v batohu podle přiřazeného slotu.",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "{Seřadit podle slotu}\n\nSeřadí artefakty v batohu podle přiřazeného slotu.",
 	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Seřadit podle třídy",
-	"vcmi.heroWindow.sortBackpackByClass.help"  : "Seřadí artefakty v batohu podle třídy artefaktu. Poklad, Menší, Větší, Relikvie.",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "{Seřadit podle třídy}\n\nSeřadí artefakty v batohu podle třídy artefaktu. Poklad, Menší, Větší, Relikvie.",
 	"vcmi.heroWindow.fusingArtifact.fusing" : "Máte všechny potřebné části k vytvoření %s. Chcete provést sloučení? {Při sloučení budou použity všechny části.}",
 
 	"vcmi.tavernWindow.inviteHero"  : "Pozvat hrdinu",
@@ -807,4 +813,4 @@
 	"spell.core.strongholdMoatTrigger.name" : "Dřevěné bodce",
 	"spell.core.summonDemons.name" : "Přivolání démonů",
 	"spell.core.towerMoat.name" : "Pozemní mina"
-}
+}

+ 16 - 10
Mods/vcmi/Content/config/english.json

@@ -23,16 +23,16 @@
 	"vcmi.adventureMap.noTownWithTavern"                 : "There are no available towns with taverns!",
 	"vcmi.adventureMap.spellUnknownProblem"              : "There is an unknown problem with this spell! No more information is available.",
 	"vcmi.adventureMap.playerAttacked"                   : "Player has been attacked: %s",
-	"vcmi.adventureMap.moveCostDetails"                  : "Movement points - Cost: %TURNS turns + %POINTS points, Remaining points: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Movement points - Cost: %POINTS points, Remaining points: %REMAINING",
-	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(Movement points: %REMAINING / %POINTS)",
+	"vcmi.adventureMap.moveCostDetails"                  : "Moving here will cost {%TOTAL} points in total ({%TURNS} turns and {%POINTS} points). {%REMAINING} points will remain after moving.",
+	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Moving here will cost {%POINTS} points. {%REMAINING} points will remain after moving.",
+	"vcmi.adventureMap.movementPointsHeroInfo"           : "(Movement points: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Sorry, replay opponent turn is not implemented yet!",
 
 	"vcmi.bonusSource.artifact" : "Artifact",
 	"vcmi.bonusSource.creature" : "Ability",
 	"vcmi.bonusSource.spell" : "Spell",
 	"vcmi.bonusSource.hero" : "Hero",
-	"vcmi.bonusSource.commander" : "Commander",
+	"vcmi.bonusSource.commander" : "Command.",
 	"vcmi.bonusSource.other" : "Other",
 
 	"vcmi.capitalColors.0" : "Red",
@@ -68,6 +68,7 @@
 	"vcmi.radialWheel.heroGetArtifacts" : "Get artifacts from other hero",
 	"vcmi.radialWheel.heroSwapArtifacts" : "Swap artifacts with other hero",
 	"vcmi.radialWheel.heroDismiss" : "Dismiss hero",
+	"vcmi.radialWheel.upgradeCreatures" : "Upgrade all creatures",
 
 	"vcmi.radialWheel.moveTop" : "Move to top",
 	"vcmi.radialWheel.moveUp" : "Move up",
@@ -363,6 +364,8 @@
 	"vcmi.battleOptions.endWithAutocombat.help": "{Ends battle}\n\nAuto-Combat plays battle to end instant",
 	"vcmi.battleOptions.showQuickSpell.hover": "Show Quickspell panel",
 	"vcmi.battleOptions.showQuickSpell.help": "{Show Quickspell panel}\n\nShow panel for quick selecting spells",
+	"vcmi.battleOptions.showHealthBar.hover": "Show health bar",
+	"vcmi.battleOptions.showHealthBar.help": "{Show health bar}\n\nShow health bar indicating remaining health before one unit dies.",	
 
 	"vcmi.adventureMap.revisitObject.hover" : "Revisit Object",
 	"vcmi.adventureMap.revisitObject.help" : "{Revisit Object}\n\nIf a hero currently stands on a Map Object, he can revisit the location.",
@@ -415,6 +418,9 @@
 	"vcmi.townStructure.bank.borrow" : "You enter the bank. A banker sees you and says: \"We have made a special offer for you. You can take a loan of 2500 gold from us for 5 days. You will have to repay 500 gold every day.\"",
 	"vcmi.townStructure.bank.payBack" : "You enter the bank. A banker sees you and says: \"You have already got your loan. Pay it back before taking a new one.\"",
 
+	"vcmi.townWindow.upgradeAll.notAllUpgradable" : "Not enough resources to upgrade all creatures. Do you want to upgrade following creatures?",
+	"vcmi.townWindow.upgradeAll.notUpgradable" : "Not enough resources to upgrade any creature.",
+
 	"vcmi.logicalExpressions.anyOf"  : "Any of the following:",
 	"vcmi.logicalExpressions.allOf"  : "All of the following:",
 	"vcmi.logicalExpressions.noneOf" : "None of the following:",
@@ -423,12 +429,12 @@
 	"vcmi.heroWindow.openCommander.help"  : "Shows details about the commander of this hero.",
 	"vcmi.heroWindow.openBackpack.hover" : "Open artifact backpack window",
 	"vcmi.heroWindow.openBackpack.help"  : "Opens window that allows easier artifact backpack management.",
-	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Sort by cost",
-	"vcmi.heroWindow.sortBackpackByCost.help"  : "Sort artifacts in backpack by cost.",
-	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Sort by slot",
-	"vcmi.heroWindow.sortBackpackBySlot.help"  : "Sort artifacts in backpack by equipped slot.",
-	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Sort by class",
-	"vcmi.heroWindow.sortBackpackByClass.help"  : "Sort artifacts in backpack by artifact class. Treasure, Minor, Major, Relic",
+	"vcmi.heroWindow.sortBackpackByCost.hover"  : "By value",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "{Sort by cost}\n\nSort artifacts in backpack by cost.",
+	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "By slot",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "{Sort by slot}\n\nSort artifacts in backpack by equipped slot.",
+	"vcmi.heroWindow.sortBackpackByClass.hover"  : "By class",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "{Sort by class}\n\nSort artifacts in backpack by artifact class. Treasure, Minor, Major, Relic",
 	"vcmi.heroWindow.fusingArtifact.fusing" : "You possess all of the components needed for the fusion of the %s. Do you wish to perform the fusion? {All components will be consumed upon fusion.}",
 
 	"vcmi.tavernWindow.inviteHero"  : "Invite hero",

+ 0 - 2
Mods/vcmi/Content/config/french.json

@@ -18,8 +18,6 @@
 	"vcmi.adventureMap.noTownWithTavern"       : "Il n'y a pas de villes disponibles avec des tavernes !",
 	"vcmi.adventureMap.spellUnknownProblem"    : "Il y a un problème inconnu avec ce sort ! Pas plus d'informations sont disponibles.",
 	"vcmi.adventureMap.playerAttacked"         : "Le joueur a été attaqué : %s",
-	"vcmi.adventureMap.moveCostDetails"        : "Points de mouvement - Coût : %TURNS tours + %POINTS points, Points restants : %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Points de mouvement - Coût : %POINTS points, Points restants : %REMAINING",
 
 	"vcmi.capitalColors.0" : "Rouge",
 	"vcmi.capitalColors.1" : "Bleu",

+ 47 - 19
Mods/vcmi/Content/config/german.json

@@ -23,16 +23,16 @@
 	"vcmi.adventureMap.noTownWithTavern"                 : "Keine Stadt mit Taverne verfügbar!",
 	"vcmi.adventureMap.spellUnknownProblem"              : "Unbekanntes Problem mit diesem Zauberspruch, keine weiteren Informationen verfügbar.",
 	"vcmi.adventureMap.playerAttacked"                   : "Spieler wurde attackiert: %s",
-	"vcmi.adventureMap.moveCostDetails"                  : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING",
-	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(Bewegungspunkte: %REMAINING / %POINTS)",
+	"vcmi.adventureMap.moveCostDetails"                  : "Eine Bewegung hierher kostet insgesamt {%TOTAL} Punkte ({%TURNS} Runden und {%POINTS} Punkte). Nach der Bewegung bleiben {%REMAINING} Punkte übrig.",
+	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Eine Bewegung hierher kostet {%POINTS} Punkte. Nach der Bewegung bleiben {%REMAINING} Punkte übrig.",
+	"vcmi.adventureMap.movementPointsHeroInfo"           : "(Bewegungspunkte: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Das Wiederholen des gegnerischen Zuges ist aktuell noch nicht implementiert!",
 
 	"vcmi.bonusSource.artifact" : "Artefakt",
 	"vcmi.bonusSource.creature" : "Fähigkeit",
 	"vcmi.bonusSource.spell" : "Zauber",
 	"vcmi.bonusSource.hero" : "Held",
-	"vcmi.bonusSource.commander" : "Commander",
+	"vcmi.bonusSource.commander" : "Command.",
 	"vcmi.bonusSource.other" : "Anderes",
 
 	"vcmi.capitalColors.0" : "Rot",
@@ -68,6 +68,7 @@
 	"vcmi.radialWheel.heroGetArtifacts" : "Artefakte von anderen Helden erhalten",
 	"vcmi.radialWheel.heroSwapArtifacts" : "Tausche Artefakte mit anderen Helden",
 	"vcmi.radialWheel.heroDismiss" : "Held entlassen",
+	"vcmi.radialWheel.upgradeCreatures" : "Alle Kreaturen aufrüsten",
 
 	"vcmi.radialWheel.moveTop" : "Ganz nach oben bewegen",
 	"vcmi.radialWheel.moveUp" : "Nach oben bewegen",
@@ -363,6 +364,8 @@
 	"vcmi.battleOptions.endWithAutocombat.help": "{Kampf beenden}\n\nAutokampf spielt den Kampf sofort zu Ende",
 	"vcmi.battleOptions.showQuickSpell.hover": "Schnellzauber-Panel anzeigen",
 	"vcmi.battleOptions.showQuickSpell.help": "{Schnellzauber-Panel anzeigen}\n\nZeigt ein Panel, auf dem schnell Zauber ausgewählt werden können",
+	"vcmi.battleOptions.showHealthBar.hover": "Gesundheits-Balken anzeigen",
+	"vcmi.battleOptions.showHealthBar.help": "{Gesundheits-Balken anzeigen}\n\nAnzeige eines Gesundheitsbalkens, der die verbleibende Gesundheit anzeigt, bevor eine Einheit stirbt.",	
 
 	"vcmi.adventureMap.revisitObject.hover" : "Objekt erneut besuchen",
 	"vcmi.adventureMap.revisitObject.help" : "{Objekt erneut besuchen}\n\nSteht ein Held gerade auf einem Kartenobjekt, kann er den Ort erneut aufsuchen.",
@@ -415,6 +418,9 @@
 	"vcmi.townStructure.bank.borrow" : "Ihr betretet die Bank. Ein Bankangestellter sieht Euch und sagt: \"Wir haben ein spezielles Angebot für Euch gemacht. Ihr könnt bei uns einen Kredit von 2500 Gold für 5 Tage aufnehmen. Ihr werdet jeden Tag 500 Gold zurückzahlen müssen.\"",
 	"vcmi.townStructure.bank.payBack" : "Ihr betretet die Bank. Ein Bankangestellter sieht Euch und sagt: \"Ihr habt Euren Kredit bereits erhalten. Zahlt Ihn ihn zurück, bevor Ihr einen neuen aufnehmt.\"",
 
+	"vcmi.townWindow.upgradeAll.notAllUpgradable" : "Nicht genügend Ressourcen um alle Kreaturen aufzurüsten. Folgende Kreaturen aufrüsten?",
+	"vcmi.townWindow.upgradeAll.notUpgradable" : "Nicht genügend Ressourcen um mindestens eine Kreatur aufzurüsten.",
+
 	"vcmi.logicalExpressions.anyOf"  : "Eines der folgenden:",
 	"vcmi.logicalExpressions.allOf"  : "Alles der folgenden:",
 	"vcmi.logicalExpressions.noneOf" : "Keines der folgenden:",
@@ -424,11 +430,11 @@
 	"vcmi.heroWindow.openBackpack.hover" : "Artefakt-Rucksack-Fenster öffnen",
 	"vcmi.heroWindow.openBackpack.help"  : "Öffnet ein Fenster, das die Verwaltung des Artefakt-Rucksacks erleichtert",
 	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Nach Kosten sortieren",
-	"vcmi.heroWindow.sortBackpackByCost.help"  : "Artefakte im Rucksack nach Kosten sortieren.",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "{Nach Kosten sortieren}\n\nArtefakte im Rucksack nach Kosten sortieren.",
 	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Nach Slot sortieren",
-	"vcmi.heroWindow.sortBackpackBySlot.help"  : "Artefakte im Rucksack nach Ausrüstungsslot sortieren.",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "{Nach Slot sortieren}\n\nArtefakte im Rucksack nach Ausrüstungsslot sortieren.",
 	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Nach Klasse sortieren",
-	"vcmi.heroWindow.sortBackpackByClass.help"  : "Artefakte im Rucksack nach Artefaktklasse sortieren. Schatz, Klein, Groß, Relikt",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "{Nach Klasse sortieren}\n\nArtefakte im Rucksack nach Artefaktklasse sortieren. Schatz, Klein, Groß, Relikt",
 	"vcmi.heroWindow.fusingArtifact.fusing" : "Ihr verfügt über alle Komponenten, die für die Fusion der %s benötigt werden. Möchtet Ihr die Verschmelzung durchführen? {Alle Komponenten werden bei der Fusion verbraucht.}",
 
 	"vcmi.tavernWindow.inviteHero"  : "Helden einladen",
@@ -608,7 +614,7 @@
 	"core.seerhut.quest.reachDate.visit.5" : "Geschlossen bis %s.",
 	
 	"mapObject.core.hillFort.object.description" : "Aufwertungen von Kreaturen. Die Stufen 1 - 4 sind billiger als in der zugehörigen Stadt.",
-
+	
 	"core.bonus.ADDITIONAL_ATTACK.name": "Doppelschlag",
 	"core.bonus.ADDITIONAL_ATTACK.description": "Greift zweimal an",
 	"core.bonus.ADDITIONAL_RETALIATION.name": "Zusätzliche Vergeltungsmaßnahmen",
@@ -763,16 +769,6 @@
 	"core.bonus.MECHANICAL.description": "Immunität gegen viele Effekte, reparierbar",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Prisma-Atem",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Prisma-Atem-Angriff (drei Richtungen)",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Zauber-Immunität",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "Immunität gegen alle Zauber-Schulen",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Luft-Immunität",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Feuer-Immunität",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Wasser-Immunität",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Erde-Immunität",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "Immunität gegen Zauber der Luft-Schule",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "Immunität gegen Zauber der Feuer-Schule",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "Immunität gegen Zauber der Wasser-Schule",
-	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "Immunität gegen Zauber der Erde-Schule",
 	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Zauberwiderstand",
 	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air": "Luft-Zauberwiderstand",
 	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire": "Feuer-Zauberwiderstand",
@@ -783,6 +779,38 @@
 	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire": "Schaden von Feuer-Zaubern um ${val}% reduziert.",
 	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water": "Schaden von Wasser-Zaubern um ${val}% reduziert.",
 	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth": "Schaden von Erde-Zaubern um ${val}% reduziert.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Zauber-Immunität",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Luft-Immunität",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Feuer-Immunität",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Wasser-Immunität",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Erde-Immunität",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "Immunität gegen alle Zauber-Schulen",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "Immunität gegen Zauber der Luft-Schule",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "Immunität gegen Zauber der Feuer-Schule",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "Immunität gegen Zauber der Wasser-Schule",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "Immunität gegen Zauber der Erde-Schule",
 	"core.bonus.OPENING_BATTLE_SPELL.name": "Startet mit Zauber",
-	"core.bonus.OPENING_BATTLE_SPELL.description": "Wirkt ${subtype.spell} beim Start des Kampfes"
+	"core.bonus.OPENING_BATTLE_SPELL.description": "Wirkt ${subtype.spell} beim Start des Kampfes",
+	
+	"spell.core.castleMoat.name" : "Graben",
+	"spell.core.castleMoatTrigger.name" : "Graben",
+	"spell.core.catapultShot.name" : "Katapultschuss",
+	"spell.core.cyclopsShot.name" : "Belagerungsschuss",
+	"spell.core.dungeonMoat.name" : "Siedeöl",
+	"spell.core.dungeonMoatTrigger.name" : "Siedeöl",
+	"spell.core.fireWallTrigger.name" : "Feuerwand",
+	"spell.core.firstAid.name" : "Erste Hilfe",
+	"spell.core.fortressMoat.name" : "Siedender Teer",
+	"spell.core.fortressMoatTrigger.name" : "Siedender Teer",
+	"spell.core.infernoMoat.name" : "Lava",
+	"spell.core.infernoMoatTrigger.name" : "Lava",
+	"spell.core.landMineTrigger.name" : "Landmine",
+	"spell.core.necropolisMoat.name" : "Knochenplatz",
+	"spell.core.necropolisMoatTrigger.name" : "Knochenplatz",
+	"spell.core.rampartMoat.name" : "Brombeeren",
+	"spell.core.rampartMoatTrigger.name" : "Brombeeren",
+	"spell.core.strongholdMoat.name" : "Holzspieße",
+	"spell.core.strongholdMoatTrigger.name" : "Holzspieße",
+	"spell.core.summonDemons.name" : "Dämonen beschwören",
+	"spell.core.towerMoat.name" : "Landmine"
 }

+ 3 - 5
Mods/vcmi/Content/config/hungarian.json

@@ -23,8 +23,6 @@
 	"vcmi.adventureMap.noTownWithTavern"                 : "Nincsenek elérhető kocsmával rendelkező városok!",
 	"vcmi.adventureMap.spellUnknownProblem"              : "Ismeretlen probléma van ezzel a varázslattal! További információ nem érhető el.",
 	"vcmi.adventureMap.playerAttacked"                   : "A játékost megtámadták: %s",
-	"vcmi.adventureMap.moveCostDetails"                  : "Mozgáspontok - Költség: %TURNS kör + %POINTS pont, Hátralévő pontok: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Mozgáspontok - Költség: %POINTS pont, Hátralévő pontok: %REMAINING",
 	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(Mozgáspontok: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Sajnáljuk, az ellenfél körének visszajátszása még nincs megvalósítva!",
 
@@ -424,11 +422,11 @@
 	"vcmi.heroWindow.openBackpack.hover" : "Műtárgy hátizsák ablak megnyitása",
 	"vcmi.heroWindow.openBackpack.help"  : "Az ablak megnyitása, amely megkönnyíti a műtárgy hátizsák kezelését.",
 	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Rendezés ár szerint",
-	"vcmi.heroWindow.sortBackpackByCost.help"  : "A műtárgyak ár szerinti rendezése a hátizsákban.",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "{Rendezés ár szerint}\n\nA műtárgyak ár szerinti rendezése a hátizsákban.",
 	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Rendezés nyílás szerint",
-	"vcmi.heroWindow.sortBackpackBySlot.help"  : "A műtárgyak nyílás szerinti rendezése a hátizsákban.",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "{Rendezés nyílás szerint}\n\nA műtárgyak nyílás szerinti rendezése a hátizsákban.",
 	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Rendezés osztály szerint",
-	"vcmi.heroWindow.sortBackpackByClass.help"  : "A műtárgyak osztály szerinti rendezése a hátizsákban. Kincs, Kisebb, Nagyobb, Relikvia",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "{Rendezés osztály szerint}\n\nA műtárgyak osztály szerinti rendezése a hátizsákban. Kincs, Kisebb, Nagyobb, Relikvia",
 	"vcmi.heroWindow.fusingArtifact.fusing" : "Ön birtokában van az összes szükséges komponensnek a(z) %s összeolvasztásához. Szeretné elvégezni az összeolvasztást? {Minden komponens elfogy az összeolvasztás során.}",
 
 	"vcmi.tavernWindow.inviteHero"  : "Hős meghívása",

+ 809 - 0
Mods/vcmi/Content/config/italian.json

@@ -0,0 +1,809 @@
+{
+	"vcmi.adventureMap.monsterThreat.title"     : "\n\nMinaccia: ",
+	"vcmi.adventureMap.monsterThreat.levels.0"  : "Facile",
+	"vcmi.adventureMap.monsterThreat.levels.1"  : "Molto Debole",
+	"vcmi.adventureMap.monsterThreat.levels.2"  : "Debole",
+	"vcmi.adventureMap.monsterThreat.levels.3"  : "Un po' più debole",
+	"vcmi.adventureMap.monsterThreat.levels.4"  : "Uguale",
+	"vcmi.adventureMap.monsterThreat.levels.5"  : "Un po' più forte",
+	"vcmi.adventureMap.monsterThreat.levels.6"  : "Forte",
+	"vcmi.adventureMap.monsterThreat.levels.7"  : "Molto Forte",
+	"vcmi.adventureMap.monsterThreat.levels.8"  : "Difficile",
+	"vcmi.adventureMap.monsterThreat.levels.9"  : "Schiacciante",
+	"vcmi.adventureMap.monsterThreat.levels.10" : "Mortale",
+	"vcmi.adventureMap.monsterThreat.levels.11" : "Impossibile",
+	"vcmi.adventureMap.monsterLevel"            : "\n\nUnità di livello %LEVEL %TOWN %ATTACK_TYPE",
+	"vcmi.adventureMap.monsterMeleeType"        : "corpo a corpo",
+	"vcmi.adventureMap.monsterRangedType"       : "a distanza",
+	"vcmi.adventureMap.search.hover"            : "Cerca oggetto sulla mappa",
+	"vcmi.adventureMap.search.help"             : "Seleziona un oggetto da cercare sulla mappa.",
+
+	"vcmi.adventureMap.confirmRestartGame"               : "Sei sicuro di voler riavviare il gioco?",
+	"vcmi.adventureMap.noTownWithMarket"                 : "Non ci sono mercati disponibili!",
+	"vcmi.adventureMap.noTownWithTavern"                 : "Non ci sono città disponibili con taverne!",
+	"vcmi.adventureMap.spellUnknownProblem"              : "C'è un problema sconosciuto con questo incantesimo! Nessuna informazione aggiuntiva disponibile.",
+	"vcmi.adventureMap.playerAttacked"                   : "Il giocatore è stato attaccato: %s",
+	"vcmi.adventureMap.moveCostDetails"                  : "Punti movimento - Costo: %TURNS turni + %POINTS punti, Punti rimanenti: %REMAINING",
+	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Punti movimento - Costo: %POINTS punti, Punti rimanenti: %REMAINING",
+	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(Punti movimento: %REMAINING / %POINTS)",
+	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Spiacente, la riproduzione del turno avversario non è ancora implementata!",
+
+	"vcmi.bonusSource.artifact" : "Artefatto",
+	"vcmi.bonusSource.creature" : "Abilità",
+	"vcmi.bonusSource.spell" : "Incantesimo",
+	"vcmi.bonusSource.hero" : "Eroe",
+	"vcmi.bonusSource.commander" : "Comandante",
+	"vcmi.bonusSource.other" : "Altro",
+
+	"vcmi.capitalColors.0" : "Rosso",
+	"vcmi.capitalColors.1" : "Blu",
+	"vcmi.capitalColors.2" : "Marrone",
+	"vcmi.capitalColors.3" : "Verde",
+	"vcmi.capitalColors.4" : "Arancione",
+	"vcmi.capitalColors.5" : "Viola",
+	"vcmi.capitalColors.6" : "Turchese",
+	"vcmi.capitalColors.7" : "Rosa",
+	
+	"vcmi.heroOverview.startingArmy" : "Unità Iniziali",
+	"vcmi.heroOverview.warMachine" : "Macchine da Guerra",
+	"vcmi.heroOverview.secondarySkills" : "Abilità Secondarie",
+	"vcmi.heroOverview.spells" : "Incantesimi",
+	
+	"vcmi.quickExchange.moveUnit" : "Sposta Unità",
+	"vcmi.quickExchange.moveAllUnits" : "Sposta Tutte le Unità",
+	"vcmi.quickExchange.swapAllUnits" : "Scambia Eserciti",
+	"vcmi.quickExchange.moveAllArtifacts" : "Sposta Tutti gli Artefatti",
+	"vcmi.quickExchange.swapAllArtifacts" : "Scambia Artefatti",
+	
+	"vcmi.radialWheel.mergeSameUnit" : "Unisci creature dello stesso tipo",
+	"vcmi.radialWheel.fillSingleUnit" : "Riempi con creature singole",
+	"vcmi.radialWheel.splitSingleUnit" : "Dividi una singola creatura",
+	"vcmi.radialWheel.splitUnitEqually" : "Dividi equamente le creature",
+	"vcmi.radialWheel.moveUnit" : "Sposta creature in un altro esercito",
+	"vcmi.radialWheel.splitUnit" : "Dividi creatura in un altro slot",
+	
+	"vcmi.radialWheel.heroGetArmy" : "Prendi l'esercito da un altro eroe",
+	"vcmi.radialWheel.heroSwapArmy" : "Scambia l'esercito con un altro eroe",
+	"vcmi.radialWheel.heroExchange" : "Apri scambio eroe",
+	"vcmi.radialWheel.heroGetArtifacts" : "Prendi artefatti da un altro eroe",
+	"vcmi.radialWheel.heroSwapArtifacts" : "Scambia artefatti con un altro eroe",
+	"vcmi.radialWheel.heroDismiss" : "Congeda l'eroe",
+
+	"vcmi.radialWheel.moveTop" : "Sposta in alto",
+	"vcmi.radialWheel.moveUp" : "Sposta su",
+	"vcmi.radialWheel.moveDown" : "Sposta giù",
+	"vcmi.radialWheel.moveBottom" : "Sposta in basso",
+	
+	"vcmi.randomMap.description" : "Mappa creata dal Generatore di Mappe Casuali.\nIl modello era %s, dimensione %dx%d, livelli %d, giocatori %d, computer %d, acqua %s, mostri %s, mappa VCMI",
+	"vcmi.randomMap.description.isHuman" : ", %s è un umano",
+	"vcmi.randomMap.description.townChoice" : ", la scelta della città di %s è %s",
+	"vcmi.randomMap.description.water.none" : "nessuna",
+	"vcmi.randomMap.description.water.normal" : "normale",
+	"vcmi.randomMap.description.water.islands" : "isole",
+	"vcmi.randomMap.description.monster.weak" : "debole",
+	"vcmi.randomMap.description.monster.normal" : "normale",
+	"vcmi.randomMap.description.monster.strong" : "forte",
+
+	"vcmi.spellBook.search" : "cerca...",
+
+	"vcmi.spellResearch.canNotAfford" : "Non puoi permetterti di sostituire {%SPELL1} con {%SPELL2}. Ma puoi comunque scartare questo incantesimo e continuare la ricerca.",
+	"vcmi.spellResearch.comeAgain" : "La ricerca è già stata fatta oggi. Torna domani.",
+	"vcmi.spellResearch.pay" : "Vuoi sostituire {%SPELL1} con {%SPELL2}? Oppure scartare questo incantesimo e continuare la ricerca?",
+	"vcmi.spellResearch.research" : "Ricerca questo Incantesimo",
+	"vcmi.spellResearch.skip" : "Salta questo Incantesimo",
+	"vcmi.spellResearch.abort" : "Annulla",
+	"vcmi.spellResearch.noMoreSpells" : "Non ci sono più incantesimi disponibili per la ricerca.",
+
+	"vcmi.mainMenu.serverConnecting" : "Connessione in corso...",
+	"vcmi.mainMenu.serverAddressEnter" : "Inserisci indirizzo:",
+	"vcmi.mainMenu.serverConnectionFailed" : "Connessione fallita",
+	"vcmi.mainMenu.serverClosing" : "Chiusura in corso...",
+	"vcmi.mainMenu.hostTCP" : "Ospita una partita TCP/IP",
+	"vcmi.mainMenu.joinTCP" : "Unisciti a una partita TCP/IP",
+
+	"vcmi.lobby.filepath" : "Percorso file",
+	"vcmi.lobby.creationDate" : "Data di creazione",
+	"vcmi.lobby.scenarioName" : "Nome dello scenario",
+	"vcmi.lobby.mapPreview" : "Anteprima mappa",
+	"vcmi.lobby.noPreview" : "nessuna anteprima",
+	"vcmi.lobby.noUnderground" : "nessun sotterraneo",
+	"vcmi.lobby.sortDate" : "Ordina le mappe per data di modifica",
+	"vcmi.lobby.backToLobby" : "Ritorna alla lobby",
+	"vcmi.lobby.author" : "Autore",
+	"vcmi.lobby.handicap" : "Handicap",
+	"vcmi.lobby.handicap.resource" : "Dà ai giocatori risorse appropriate per iniziare oltre a quelle normali. I valori negativi sono consentiti, ma limitati a 0 (il giocatore non inizia mai con risorse negative).",
+	"vcmi.lobby.handicap.income" : "Modifica le entrate del giocatore in percentuale. Arrotondato per eccesso.",
+	"vcmi.lobby.handicap.growth" : "Modifica la crescita delle creature nelle città possedute dal giocatore. Arrotondato per eccesso.",
+	"vcmi.lobby.deleteUnsupportedSave" : "{Salvataggi non supportati trovati}\n\nVCMI ha trovato %d salvataggi non più supportati, probabilmente a causa di differenze tra le versioni di VCMI.\n\nVuoi eliminarli?",
+	"vcmi.lobby.deleteSaveGameTitle" : "Seleziona un salvataggio da eliminare",
+	"vcmi.lobby.deleteMapTitle" : "Seleziona uno scenario da eliminare",
+	"vcmi.lobby.deleteFile" : "Vuoi eliminare il seguente file?",
+	"vcmi.lobby.deleteFolder" : "Vuoi eliminare la seguente cartella?",
+	"vcmi.lobby.deleteMode" : "Passa alla modalità elimina e torna indietro",
+
+	"vcmi.broadcast.failedLoadGame" : "Impossibile caricare la partita",
+	"vcmi.broadcast.command" : "Usa '!help' per elencare i comandi disponibili",
+	"vcmi.broadcast.simturn.end" : "I turni simultanei sono terminati",
+	"vcmi.broadcast.simturn.endBetween" : "I turni simultanei tra i giocatori %s e %s sono terminati",
+	"vcmi.broadcast.serverProblem" : "Il server ha riscontrato un problema",
+	"vcmi.broadcast.gameTerminated" : "la partita è stata terminata",
+	"vcmi.broadcast.gameSavedAs" : "partita salvata come",
+	"vcmi.broadcast.noCheater" : "Nessun baro registrato!",
+	"vcmi.broadcast.playerCheater" : "Il giocatore %s ha usato i trucchi!",
+	"vcmi.broadcast.statisticFile" : "I file delle statistiche possono essere trovati nella directory %s",
+	"vcmi.broadcast.help.commands" : "Comandi disponibili per l'host:",
+	"vcmi.broadcast.help.exit" : "'!exit' - termina immediatamente la partita in corso",
+	"vcmi.broadcast.help.kick" : "'!kick <player>' - espelle il giocatore specificato dalla partita",
+	"vcmi.broadcast.help.save" : "'!save <filename>' - salva la partita con il nome specificato",
+	"vcmi.broadcast.help.statistic" : "'!statistic' - salva le statistiche della partita come file CSV",
+	"vcmi.broadcast.help.commandsAll" : "Comandi disponibili per tutti i giocatori:",
+	"vcmi.broadcast.help.help" : "'!help' - mostra questo aiuto",
+	"vcmi.broadcast.help.cheaters" : "'!cheaters' - elenca i giocatori che hanno usato trucchi durante la partita",
+	"vcmi.broadcast.help.vote" : "'!vote' - consente di modificare alcune impostazioni di gioco se tutti i giocatori sono d'accordo",
+	"vcmi.broadcast.vote.allow" : "'!vote simturns allow X' - consente turni simultanei per un numero specificato di giorni, o fino al contatto",
+	"vcmi.broadcast.vote.force" : "'!vote simturns force X' - forza turni simultanei per un numero specificato di giorni, bloccando i contatti tra giocatori",
+	"vcmi.broadcast.vote.abort" : "'!vote simturns abort' - interrompe i turni simultanei alla fine di questo turno",
+	"vcmi.broadcast.vote.timer" : "'!vote timer prolong X' - prolunga il timer di base per tutti i giocatori del numero specificato di secondi",
+	"vcmi.broadcast.vote.noActive" : "Nessuna votazione attiva!",
+	"vcmi.broadcast.vote.yes" : "sì",
+	"vcmi.broadcast.vote.no" : "no",
+	"vcmi.broadcast.vote.notRecognized" : "Comando di votazione non riconosciuto!",
+	"vcmi.broadcast.vote.success.untilContacts" : "Votazione riuscita. I turni simultanei dureranno ancora %s giorni, o fino al contatto",
+	"vcmi.broadcast.vote.success.contactsBlocked" : "Votazione riuscita. I turni simultanei dureranno ancora %s giorni. I contatti sono bloccati",
+	"vcmi.broadcast.vote.success.nextDay" : "Votazione riuscita. I turni simultanei termineranno il giorno successivo",
+	"vcmi.broadcast.vote.success.timer" : "Votazione riuscita. Il timer per tutti i giocatori è stato prolungato di %s secondi",
+	"vcmi.broadcast.vote.aborted" : "Un giocatore ha votato contro la modifica. Votazione annullata",
+	"vcmi.broadcast.vote.start.untilContacts" : "Votazione avviata per consentire turni simultanei per altri %s giorni",
+	"vcmi.broadcast.vote.start.contactsBlocked" : "Votazione avviata per forzare turni simultanei per altri %s giorni",
+	"vcmi.broadcast.vote.start.nextDay" : "Votazione avviata per terminare i turni simultanei dal giorno successivo",
+	"vcmi.broadcast.vote.start.timer" : "Votazione avviata per prolungare il timer per tutti i giocatori di %s secondi",
+	"vcmi.broadcast.vote.hint" : "Digita '!vote yes' per accettare la modifica o '!vote no' per rifiutarla",
+		
+	"vcmi.lobby.login.title" : "VCMI Lobby Online",
+	"vcmi.lobby.login.username" : "Nome utente:",
+	"vcmi.lobby.login.connecting" : "Connessione in corso...",
+	"vcmi.lobby.login.error" : "Errore di connessione: %s",
+	"vcmi.lobby.login.create" : "Nuovo Account",
+	"vcmi.lobby.login.login" : "Accedi",
+	"vcmi.lobby.login.as" : "Accedi come %s",
+	"vcmi.lobby.login.spectator" : "Spettatore",
+	"vcmi.lobby.header.rooms" : "Stanze di gioco - %d",
+	"vcmi.lobby.header.channels" : "Canali chat",
+	"vcmi.lobby.header.chat.global" : "Chat globale - %s", // %s -> language name
+	"vcmi.lobby.header.chat.match" : "Chat dalla partita precedente su %s", // %s -> game start date & time
+	"vcmi.lobby.header.chat.player" : "Chat privata con %s", // %s -> nickname of another player
+	"vcmi.lobby.header.history" : "Le tue partite precedenti",
+	"vcmi.lobby.header.players" : "Giocatori online - %d",
+	"vcmi.lobby.match.solo" : "Partita in singolo",
+	"vcmi.lobby.match.duel" : "Partita con %s", // %s -> nickname of another player
+	"vcmi.lobby.match.multi" : "%d giocatori",
+	"vcmi.lobby.room.create" : "Crea nuova stanza",
+	"vcmi.lobby.room.players.limit" : "Limite giocatori",
+	"vcmi.lobby.room.description.public" : "Qualsiasi giocatore può entrare in una stanza pubblica.",
+	"vcmi.lobby.room.description.private" : "Solo i giocatori invitati possono entrare in una stanza privata.",
+	"vcmi.lobby.room.description.new" : "Per avviare la partita, seleziona uno scenario o imposta una mappa casuale.",
+	"vcmi.lobby.room.description.load" : "Per avviare la partita, usa uno dei tuoi salvataggi.",
+	"vcmi.lobby.room.description.limit" : "Fino a %d giocatori possono entrare nella tua stanza, incluso te.",
+	"vcmi.lobby.invite.header" : "Invita giocatori",
+	"vcmi.lobby.invite.notification" : "Un giocatore ti ha invitato nella sua stanza di gioco. Ora puoi unirti alla sua stanza privata.",
+	"vcmi.lobby.preview.title" : "Unisciti alla stanza di gioco",
+	"vcmi.lobby.preview.subtitle" : "Partita su %s, ospitata da %s", //TL Note: 1) name of map or RMG template 2) nickname of game host
+	"vcmi.lobby.preview.version" : "Versione di gioco:",
+	"vcmi.lobby.preview.players" : "Giocatori:",
+	"vcmi.lobby.preview.mods" : "Mod utilizzate:",
+	"vcmi.lobby.preview.allowed" : "Vuoi unirti alla stanza di gioco?",
+	"vcmi.lobby.preview.error.header" : "Impossibile unirsi a questa stanza.",
+	"vcmi.lobby.preview.error.playing" : "Devi prima uscire dalla tua partita attuale.",
+	"vcmi.lobby.preview.error.full" : "La stanza è già piena.",
+	"vcmi.lobby.preview.error.busy" : "La stanza non accetta più nuovi giocatori.",
+	"vcmi.lobby.preview.error.invite" : "Non sei stato invitato a questa stanza.",
+	"vcmi.lobby.preview.error.mods" : "Stai usando un set di mod diverso.",
+	"vcmi.lobby.preview.error.version" : "Stai usando una versione diversa di VCMI.",
+	"vcmi.lobby.room.new" : "Nuova partita",
+	"vcmi.lobby.room.load" : "Carica partita",
+	"vcmi.lobby.room.type" : "Tipo di stanza",
+	"vcmi.lobby.room.mode" : "Modalità di gioco",
+	"vcmi.lobby.room.state.public" : "Pubblica",
+	"vcmi.lobby.room.state.private" : "Privata",
+	"vcmi.lobby.room.state.busy" : "In gioco",
+	"vcmi.lobby.room.state.invited" : "Invitato",
+	"vcmi.lobby.mod.state.compatible" : "Compatibile",
+	"vcmi.lobby.mod.state.disabled" : "Deve essere attivato",
+	"vcmi.lobby.mod.state.version" : "Versione incompatibile",
+	"vcmi.lobby.mod.state.excessive" : "Deve essere disattivato",
+	"vcmi.lobby.mod.state.missing" : "Non installato",
+	"vcmi.lobby.pvp.coin.hover" : "Lancia la moneta",
+	"vcmi.lobby.pvp.coin.help" : "Lancia una moneta",
+	"vcmi.lobby.pvp.randomTown.hover" : "Città casuale",
+	"vcmi.lobby.pvp.randomTown.help" : "Scrivi una città casuale in chat",
+	"vcmi.lobby.pvp.randomTownVs.hover" : "Città casuale vs.",
+	"vcmi.lobby.pvp.randomTownVs.help" : "Scrivi due città casuali in chat",
+	"vcmi.lobby.pvp.versus" : "vs.",
+
+	"vcmi.client.errors.invalidMap" : "{Mappa o campagna non valida}\n\nImpossibile avviare la partita! La mappa o la campagna selezionata potrebbe essere non valida o corrotta. Motivo:\n%s",
+	"vcmi.client.errors.missingCampaigns" : "{File dati mancanti}\n\nI file dati delle campagne non sono stati trovati! Potresti avere file dati incompleti o corrotti di Heroes 3. Reinstalla i dati del gioco.",
+	"vcmi.client.errors.modLoadingFailure" : "{Errore di caricamento delle mod}\n\nSono stati rilevati problemi critici durante il caricamento delle mod! Il gioco potrebbe non funzionare correttamente o bloccarsi! Aggiorna o disattiva le seguenti mod:\n\n",
+	"vcmi.server.errors.disconnected" : "{Errore di rete}\n\nLa connessione al server di gioco è stata persa!",
+	"vcmi.server.errors.playerLeft" : "{Giocatore disconnesso}\n\nIl giocatore %s si è disconnesso dalla partita!", //%s -> player color
+	"vcmi.server.errors.existingProcess" : "Un altro processo del server VCMI è in esecuzione. Terminarlo prima di avviare una nuova partita.",
+	"vcmi.server.errors.modsToEnable"    : "{Le seguenti mod sono richieste}",
+	"vcmi.server.errors.modsToDisable"   : "{Le seguenti mod devono essere disattivate}",
+	"vcmi.server.errors.unknownEntity" : "Impossibile caricare il salvataggio! Entità sconosciuta '%s' trovata nel salvataggio! Il salvataggio potrebbe non essere compatibile con la versione attualmente installata delle mod!",
+	"vcmi.server.errors.wrongIdentified"   : "Sei stato identificato come giocatore %s mentre ci si aspettava %s",
+	"vcmi.server.errors.notAllowed"   : "Non ti è permesso eseguire questa azione!",
+	
+	"vcmi.dimensionDoor.seaToLandError" : "Non è possibile teletrasportarsi dal mare alla terraferma o viceversa con la Porta Dimensionale.",
+
+	"vcmi.settingsMainWindow.generalTab.hover" : "Generale",
+	"vcmi.settingsMainWindow.generalTab.help"     : "Passa alla scheda Opzioni generali, che contiene le impostazioni relative al comportamento generale del client di gioco.",
+	"vcmi.settingsMainWindow.battleTab.hover" : "Battaglia",
+	"vcmi.settingsMainWindow.battleTab.help"     : "Passa alla scheda Opzioni di battaglia, che consente di configurare il comportamento del gioco durante le battaglie.",
+	"vcmi.settingsMainWindow.adventureTab.hover" : "Mappa Avventura",
+	"vcmi.settingsMainWindow.adventureTab.help"  : "Passa alla scheda Opzioni Mappa Avventura (la mappa dell'avventura è la sezione del gioco in cui i giocatori controllano i movimenti degli eroi).",
+
+	"vcmi.systemOptions.videoGroup" : "Impostazioni video",
+	"vcmi.systemOptions.audioGroup" : "Impostazioni audio",
+	"vcmi.systemOptions.otherGroup" : "Altre impostazioni", // unused right now
+	"vcmi.systemOptions.townsGroup" : "Schermata della città",
+
+	"vcmi.statisticWindow.statistics" : "Statistiche",
+	"vcmi.statisticWindow.tsvCopy" : "Dati negli appunti",
+	"vcmi.statisticWindow.selectView" : "Seleziona vista",
+	"vcmi.statisticWindow.value" : "Valore",
+	"vcmi.statisticWindow.title.overview" : "Panoramica",
+	"vcmi.statisticWindow.title.resources" : "Risorse",
+	"vcmi.statisticWindow.title.income" : "Entrate",
+	"vcmi.statisticWindow.title.numberOfHeroes" : "Numero di eroi",
+	"vcmi.statisticWindow.title.numberOfTowns" : "Numero di città",
+	"vcmi.statisticWindow.title.numberOfArtifacts" : "Numero di artefatti",
+	"vcmi.statisticWindow.title.numberOfDwellings" : "Numero di dimore",
+	"vcmi.statisticWindow.title.numberOfMines" : "Numero di miniere",
+	"vcmi.statisticWindow.title.armyStrength" : "Forza dell'esercito",
+	"vcmi.statisticWindow.title.experience" : "Esperienza",
+	"vcmi.statisticWindow.title.resourcesSpentArmy" : "Costi dell'esercito",
+	"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Costi degli edifici",
+	"vcmi.statisticWindow.title.mapExplored" : "Percentuale di mappa esplorata",
+	"vcmi.statisticWindow.param.playerName" : "Nome giocatore",
+	"vcmi.statisticWindow.param.daysSurvived" : "Giorni sopravvissuti",
+	"vcmi.statisticWindow.param.maxHeroLevel" : "Livello massimo dell'eroe",
+	"vcmi.statisticWindow.param.battleWinRatioHero" : "Tasso di vittoria (vs. eroe)",
+	"vcmi.statisticWindow.param.battleWinRatioNeutral" : "Tasso di vittoria (vs. neutrali)",
+	"vcmi.statisticWindow.param.battlesHero" : "Battaglie (vs. eroe)",
+	"vcmi.statisticWindow.param.battlesNeutral" : "Battaglie (vs. neutrali)",
+	"vcmi.statisticWindow.param.maxArmyStrength" : "Massima forza dell'esercito totale",
+	"vcmi.statisticWindow.param.tradeVolume" : "Volume di scambio",
+	"vcmi.statisticWindow.param.obeliskVisited" : "Obelisco visitato",
+	"vcmi.statisticWindow.icon.townCaptured" : "Città conquistate",
+	"vcmi.statisticWindow.icon.strongestHeroDefeated" : "Eroe più forte dell'avversario sconfitto",
+	"vcmi.statisticWindow.icon.grailFound" : "Graal trovato",
+	"vcmi.statisticWindow.icon.defeated" : "Sconfitto",
+
+	"vcmi.systemOptions.fullscreenBorderless.hover" : "Schermo intero (senza bordi)",
+	"vcmi.systemOptions.fullscreenBorderless.help"  : "{Schermo intero senza bordi}\n\nSe selezionato, VCMI verrà eseguito in modalità schermo intero senza bordi. In questa modalità, il gioco utilizzerà sempre la stessa risoluzione del desktop, ignorando la risoluzione selezionata.",
+	"vcmi.systemOptions.fullscreenExclusive.hover"  : "Schermo intero (esclusivo)",
+	"vcmi.systemOptions.fullscreenExclusive.help"   : "{Schermo intero}\n\nSe selezionato, VCMI verrà eseguito in modalità schermo intero esclusiva. In questa modalità, il gioco cambierà la risoluzione del monitor con quella selezionata.",
+	"vcmi.systemOptions.resolutionButton.hover" : "Risoluzione: %wx%h",
+	"vcmi.systemOptions.resolutionButton.help"  : "{Seleziona risoluzione}\n\nModifica la risoluzione dello schermo di gioco.",
+	"vcmi.systemOptions.resolutionMenu.hover"   : "Seleziona risoluzione",
+	"vcmi.systemOptions.resolutionMenu.help"    : "Modifica la risoluzione dello schermo di gioco.",
+	"vcmi.systemOptions.scalingButton.hover"   : "Scala interfaccia: %p%",
+	"vcmi.systemOptions.scalingButton.help"    : "{Scala interfaccia}\n\nModifica la scala dell'interfaccia di gioco.",
+	"vcmi.systemOptions.scalingMenu.hover"     : "Seleziona scala interfaccia",
+	"vcmi.systemOptions.scalingMenu.help"      : "Modifica la scala dell'interfaccia di gioco.",
+	"vcmi.systemOptions.longTouchButton.hover"   : "Intervallo di tocco lungo: %d ms",
+	"vcmi.systemOptions.longTouchButton.help"    : "{Intervallo di tocco lungo}\n\nQuando si utilizza il touchscreen, le finestre popup appariranno dopo aver toccato lo schermo per la durata specificata in millisecondi.",
+	"vcmi.systemOptions.longTouchMenu.hover"     : "Seleziona intervallo di tocco lungo",
+	"vcmi.systemOptions.longTouchMenu.help"      : "Modifica la durata dell'intervallo di tocco lungo.",
+	"vcmi.systemOptions.longTouchMenu.entry"     : "%d millisecondi",
+	"vcmi.systemOptions.framerateButton.hover"  : "Mostra FPS",
+	"vcmi.systemOptions.framerateButton.help"   : "{Mostra FPS}\n\nAttiva o disattiva la visibilità del contatore dei fotogrammi al secondo nell'angolo della finestra di gioco.",
+	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Feedback aptico",
+	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Feedback aptico}\n\nAttiva o disattiva il feedback aptico sugli input tattili.",
+	"vcmi.systemOptions.enableUiEnhancementsButton.hover"  : "Miglioramenti interfaccia",
+	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Miglioramenti interfaccia}\n\nAttiva vari miglioramenti della qualità della vita nell'interfaccia. Ad esempio, un pulsante per lo zaino, ecc. Disabilitalo per un'esperienza più classica.",
+	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "Libro degli incantesimi grande",
+	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Libro degli incantesimi grande}\n\nAbilita un libro degli incantesimi più grande che mostra più incantesimi per pagina. L'animazione del cambio pagina non funziona con questa impostazione attivata.",
+	"vcmi.systemOptions.audioMuteFocus.hover"  : "Silenzioso quando inattivo",
+	"vcmi.systemOptions.audioMuteFocus.help"   : "{Silenzioso quando inattivo}\n\nDisattiva l'audio quando la finestra del gioco non è attiva. Fanno eccezione i messaggi di gioco e il suono del nuovo turno.",
+
+	"vcmi.adventureOptions.infoBarPick.hover" : "Mostra messaggi nel pannello informazioni",
+	"vcmi.adventureOptions.infoBarPick.help" : "{Mostra messaggi nel pannello informazioni}\n\nQuando possibile, i messaggi del gioco provenienti dagli oggetti sulla mappa saranno mostrati nel pannello informazioni, invece che comparire in una finestra separata.",
+	"vcmi.adventureOptions.numericQuantities.hover" : "Quantità di creature numeriche",
+	"vcmi.adventureOptions.numericQuantities.help" : "{Quantità di creature numeriche}\n\nMostra la quantità approssimativa di creature nemiche nel formato numerico A-B.",
+	"vcmi.adventureOptions.forceMovementInfo.hover" : "Mostra sempre il costo del movimento",
+	"vcmi.adventureOptions.forceMovementInfo.help" : "{Mostra sempre il costo del movimento}\n\nMostra sempre i dati dei punti movimento nella barra di stato (invece di visualizzarli solo quando si tiene premuto il tasto ALT).",
+	"vcmi.adventureOptions.showGrid.hover" : "Mostra griglia",
+	"vcmi.adventureOptions.showGrid.help" : "{Mostra griglia}\n\nMostra la griglia di sovrapposizione, evidenziando i confini tra le caselle della mappa avventura.",
+	"vcmi.adventureOptions.borderScroll.hover" : "Scorrimento ai bordi",
+	"vcmi.adventureOptions.borderScroll.help" : "{Scorrimento ai bordi}\n\nScorri la mappa avventura quando il cursore è vicino al bordo della finestra. Può essere disattivato tenendo premuto il tasto CTRL.",
+	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Gestione creature nel pannello informazioni",
+	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Gestione creature nel pannello informazioni}\n\nConsente di riordinare le creature nel pannello informazioni invece di alternarle tra i componenti predefiniti.",
+	"vcmi.adventureOptions.leftButtonDrag.hover" : "Trascinamento con clic sinistro",
+	"vcmi.adventureOptions.leftButtonDrag.help" : "{Trascinamento con clic sinistro}\n\nQuando attivato, spostando il mouse con il tasto sinistro premuto si trascina la visuale della mappa avventura.",
+	"vcmi.adventureOptions.rightButtonDrag.hover" : "Trascinamento con clic destro",
+	"vcmi.adventureOptions.rightButtonDrag.help" : "{Trascinamento con clic destro}\n\nQuando attivato, spostando il mouse con il tasto destro premuto si trascina la visuale della mappa avventura.",
+	"vcmi.adventureOptions.smoothDragging.hover" : "Trascinamento della mappa fluido",
+	"vcmi.adventureOptions.smoothDragging.help" : "{Trascinamento della mappa fluido}\n\nQuando attivato, il trascinamento della mappa ha un effetto di scorrimento fluido moderno.",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Salta effetti di dissolvenza",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Salta effetti di dissolvenza}\n\nQuando attivato, salta la dissolvenza degli oggetti e altri effetti simili (raccolta risorse, imbarco su navi, ecc.). Rende l'interfaccia più reattiva in alcuni casi, a scapito dell'estetica. Utile soprattutto nei giochi PvP. Per una velocità di movimento massima, la dissolvenza è sempre saltata indipendentemente da questa impostazione.",
+	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed1.help": "Imposta la velocità di scorrimento della mappa su molto lenta.",
+	"vcmi.adventureOptions.mapScrollSpeed5.help": "Imposta la velocità di scorrimento della mappa su molto veloce.",
+	"vcmi.adventureOptions.mapScrollSpeed6.help": "Imposta la velocità di scorrimento della mappa su istantanea.",
+	"vcmi.adventureOptions.hideBackground.hover" : "Nascondi Sfondo",
+	"vcmi.adventureOptions.hideBackground.help" : "{Nascondi Sfondo}\n\nNasconde la mappa dell'avventura nello sfondo e mostra una texture al suo posto.",
+
+	"vcmi.battleOptions.queueSizeLabel.hover": "Mostra coda dell'ordine di turno",
+	"vcmi.battleOptions.queueSizeNoneButton.hover": "SPENTO",
+	"vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO",
+	"vcmi.battleOptions.queueSizeSmallButton.hover": "PICCOLO",
+	"vcmi.battleOptions.queueSizeBigButton.hover": "GRANDE",
+	"vcmi.battleOptions.queueSizeNoneButton.help": "Non visualizzare la coda dell'ordine di turno.",
+	"vcmi.battleOptions.queueSizeAutoButton.help": "Regola automaticamente la dimensione della coda dell'ordine di turno in base alla risoluzione del gioco (PICCOLO viene utilizzato se l'altezza della risoluzione è inferiore a 700 pixel, GRANDE viene usato altrimenti).",
+	"vcmi.battleOptions.queueSizeSmallButton.help": "Imposta la dimensione della coda dell'ordine di turno su PICCOLO.",
+	"vcmi.battleOptions.queueSizeBigButton.help": "Imposta la dimensione della coda dell'ordine di turno su GRANDE (non supportato se l'altezza della risoluzione del gioco è inferiore a 700 pixel).",
+	"vcmi.battleOptions.animationsSpeed1.hover": "",
+	"vcmi.battleOptions.animationsSpeed5.hover": "",
+	"vcmi.battleOptions.animationsSpeed6.hover": "",
+	"vcmi.battleOptions.animationsSpeed1.help": "Imposta la velocità dell'animazione su molto lenta.",
+	"vcmi.battleOptions.animationsSpeed5.help": "Imposta la velocità dell'animazione su molto veloce.",
+	"vcmi.battleOptions.animationsSpeed6.help": "Imposta la velocità dell'animazione su istantanea.",
+	"vcmi.battleOptions.movementHighlightOnHover.hover": "Evidenzia il movimento al passaggio del mouse",
+	"vcmi.battleOptions.movementHighlightOnHover.help": "{Evidenzia il movimento al passaggio del mouse}\n\nEvidenzia il raggio di movimento dell'unità quando ci passi sopra con il cursore.",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Mostra limiti di gittata per tiratori",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Mostra limiti di gittata per tiratori}\n\nMostra i limiti di gittata del tiratore quando ci passi sopra con il cursore.",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Mostra finestre statistiche degli eroi",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Mostra finestre statistiche degli eroi}\n\nAttiva in modo permanente le finestre statistiche degli eroi che mostrano le statistiche primarie e i punti incantesimo.",
+	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Salta musica introduttiva",
+	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Salta musica introduttiva}\n\nPermette di compiere azioni durante la musica introduttiva che viene riprodotta all'inizio di ogni battaglia.",	
+	"vcmi.battleOptions.endWithAutocombat.hover": "Termina la battaglia",
+	"vcmi.battleOptions.endWithAutocombat.help": "{Termina la battaglia}\n\nL'Auto-Combat gioca la battaglia fino alla fine immediatamente.",
+	"vcmi.battleOptions.showQuickSpell.hover": "Mostra pannello incantesimi rapidi",
+	"vcmi.battleOptions.showQuickSpell.help": "{Mostra pannello incantesimi rapidi}\n\nMostra il pannello per la selezione rapida degli incantesimi.",
+
+	"vcmi.adventureMap.revisitObject.hover" : "Rivisita oggetto",
+	"vcmi.adventureMap.revisitObject.help" : "{Rivisita oggetto}\n\nSe un eroe si trova su un oggetto della mappa, può rivisitare la posizione.",
+
+	"vcmi.battleWindow.pressKeyToSkipIntro" : "Premi un tasto qualsiasi per iniziare immediatamente la battaglia",
+	"vcmi.battleWindow.damageEstimation.melee" : "Attacca %CREATURE (%DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.meleeKills" : "Attacca %CREATURE (%DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.ranged" : "Spara a %CREATURE (%SHOTS, %DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.rangedKills" : "Spara a %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.shots" : "%d colpi rimasti",
+	"vcmi.battleWindow.damageEstimation.shots.1" : "%d colpo rimasto",
+	"vcmi.battleWindow.damageEstimation.damage" : "%d danni",
+	"vcmi.battleWindow.damageEstimation.damage.1" : "%d danni",
+	"vcmi.battleWindow.damageEstimation.kills" : "%d periranno",
+	"vcmi.battleWindow.damageEstimation.kills.1" : "%d perirà",
+	
+	"vcmi.battleWindow.damageRetaliation.will" : "Contrattaccherà",
+	"vcmi.battleWindow.damageRetaliation.may" : "Potrebbe contrattaccare",
+	"vcmi.battleWindow.damageRetaliation.never" : "Non contrattaccherà.",
+	"vcmi.battleWindow.damageRetaliation.damage" : "(%DAMAGE).",
+	"vcmi.battleWindow.damageRetaliation.damageKills" : "(%DAMAGE, %KILLS).",
+	
+	"vcmi.battleWindow.killed" : "Ucciso",
+	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s sono stati uccisi da colpi precisi!",
+	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s è stato ucciso con un colpo preciso!",
+	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s sono stati uccisi da colpi precisi!",
+	"vcmi.battleWindow.endWithAutocombat" : "Sei sicuro di voler terminare la battaglia con il combattimento automatico?",
+
+	"vcmi.battleResultsWindow.applyResultsLabel" : "Accettare il risultato della battaglia?",
+
+	"vcmi.tutorialWindow.title" : "Introduzione touchscreen",
+	"vcmi.tutorialWindow.decription.RightClick" : "Tocca e tieni premuto l'elemento su cui desideri fare clic destro. Tocca un'area libera per chiudere.",
+	"vcmi.tutorialWindow.decription.MapPanning" : "Tocca e trascina con un dito per spostare la mappa.",
+	"vcmi.tutorialWindow.decription.MapZooming" : "Pizzica con due dita per modificare lo zoom della mappa.",
+	"vcmi.tutorialWindow.decription.RadialWheel" : "Scorrendo si apre la ruota radiale per varie azioni, come la gestione delle creature/eroi e l'ordinamento delle città.",
+	"vcmi.tutorialWindow.decription.BattleDirection" : "Per attaccare da una direzione specifica, scorri nella direzione da cui deve essere effettuato l'attacco.",
+	"vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Il gesto di attacco direzionale può essere annullato se il dito è sufficientemente lontano.",
+	"vcmi.tutorialWindow.decription.AbortSpell" : "Tocca e tieni premuto per annullare un incantesimo.",
+
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Mostra creature disponibili",
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Mostra creature disponibili}\n\nMostra il numero di creature disponibili per l'acquisto invece della loro crescita nel riepilogo della città (angolo in basso a sinistra della schermata della città).",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Mostra crescita settimanale delle creature",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Mostra crescita settimanale delle creature}\n\nMostra la crescita settimanale delle creature invece della quantità disponibile nel riepilogo della città (angolo in basso a sinistra della schermata della città).",
+	"vcmi.otherOptions.compactTownCreatureInfo.hover": "Info compatta delle creature",
+	"vcmi.otherOptions.compactTownCreatureInfo.help": "{Info compatta delle creature}\n\nMostra informazioni più piccole per le creature della città nel riepilogo della città (angolo in basso a sinistra della schermata della città).",
+
+	"vcmi.townHall.missingBase"             : "L'edificio base %s deve essere costruito prima",
+	"vcmi.townHall.noCreaturesToRecruit"    : "Non ci sono creature da reclutare!",
+
+	"vcmi.townStructure.bank.borrow" : "Entri in banca. Un banchiere ti vede e dice: \"Abbiamo preparato un'offerta speciale per te. Puoi prendere un prestito di 2500 oro per 5 giorni. Dovrai restituire 500 oro ogni giorno.\"",
+	"vcmi.townStructure.bank.payBack" : "Entri in banca. Un banchiere ti vede e dice: \"Hai già ottenuto un prestito. Rimborsalo prima di prenderne un altro.\"",
+
+	"vcmi.logicalExpressions.anyOf"  : "Qualsiasi dei seguenti:",
+	"vcmi.logicalExpressions.allOf"  : "Tutti i seguenti:",
+	"vcmi.logicalExpressions.noneOf" : "Nessuno dei seguenti:",
+
+	"vcmi.heroWindow.openCommander.hover" : "Apri finestra informazioni comandante",
+	"vcmi.heroWindow.openCommander.help"  : "Mostra i dettagli sul comandante di questo eroe.",
+	"vcmi.heroWindow.openBackpack.hover" : "Apri finestra zaino artefatti",
+	"vcmi.heroWindow.openBackpack.help"  : "Apre una finestra che consente una gestione più semplice dello zaino artefatti.",
+	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Ordina per costo",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "Ordina gli artefatti nello zaino in base al costo.",
+	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Ordina per slot",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "Ordina gli artefatti nello zaino in base allo slot equipaggiato.",
+	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Ordina per classe",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "Ordina gli artefatti nello zaino in base alla classe dell'artefatto. Tesoro, Minore, Maggiore, Reliquia",
+	"vcmi.heroWindow.fusingArtifact.fusing" : "Possiedi tutti i componenti necessari per la fusione del %s. Vuoi eseguire la fusione? {Tutti i componenti saranno consumati durante la fusione.}",
+
+	"vcmi.tavernWindow.inviteHero"  : "Invita eroe",
+
+	"vcmi.commanderWindow.artifactMessage" : "Vuoi restituire questo artefatto all'eroe?",
+
+	"vcmi.creatureWindow.showBonuses.hover"    : "Passa alla vista bonus",
+	"vcmi.creatureWindow.showBonuses.help"     : "Mostra tutti i bonus attivi del comandante.",
+	"vcmi.creatureWindow.showSkills.hover"     : "Passa alla vista abilità",
+	"vcmi.creatureWindow.showSkills.help"      : "Mostra tutte le abilità apprese del comandante.",
+	"vcmi.creatureWindow.returnArtifact.hover" : "Restituisci artefatto",
+	"vcmi.creatureWindow.returnArtifact.help"  : "Fai clic su questo pulsante per restituire l'artefatto allo zaino dell'eroe.",
+
+	"vcmi.questLog.hideComplete.hover" : "Nascondi missioni completate",
+	"vcmi.questLog.hideComplete.help"  : "Nasconde tutte le missioni completate.",
+
+	"vcmi.randomMapTab.widgets.randomTemplate"      : "(Casuale)",
+	"vcmi.randomMapTab.widgets.templateLabel"        : "Modello",
+	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Imposta...",
+	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Allineamenti squadra",
+	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Tipi di strade",
+
+	"vcmi.optionsTab.turnOptions.hover" : "Opzioni turno",
+	"vcmi.optionsTab.turnOptions.help" : "Seleziona opzioni timer turno e turni simultanei",
+
+	"vcmi.optionsTab.chessFieldBase.hover" : "Timer base",
+	"vcmi.optionsTab.chessFieldTurn.hover" : "Timer turno",
+	"vcmi.optionsTab.chessFieldBattle.hover" : "Timer battaglia",
+	"vcmi.optionsTab.chessFieldUnit.hover" : "Timer unità",
+	"vcmi.optionsTab.chessFieldBase.help" : "Utilizzato quando {Timer turno} raggiunge 0. Impostato una sola volta all'inizio del gioco. Al raggiungimento dello zero, il turno corrente termina. Qualsiasi combattimento in corso si concluderà con una sconfitta.",
+	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Utilizzato fuori dal combattimento o quando {Timer battaglia} scade. Resettato ogni turno. Il tempo residuo viene aggiunto a {Timer base} alla fine del turno.",
+	"vcmi.optionsTab.chessFieldTurnDiscard.help" : "Utilizzato fuori dal combattimento o quando {Timer battaglia} scade. Resettato ogni turno. Il tempo non speso viene perso.",
+	"vcmi.optionsTab.chessFieldBattle.help" : "Utilizzato nelle battaglie con IA o nei combattimenti PvP quando {Timer unità} scade. Resettato all'inizio di ogni combattimento.",
+	"vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Utilizzato quando si seleziona l'azione di un'unità nei combattimenti PvP. Il tempo residuo viene aggiunto a {Timer battaglia} alla fine del turno dell'unità.",
+	"vcmi.optionsTab.chessFieldUnitDiscard.help" : "Utilizzato quando si seleziona l'azione di un'unità nei combattimenti PvP. Resettato all'inizio del turno di ogni unità. Il tempo non speso viene perso.",
+
+	"vcmi.optionsTab.accumulate" : "Accumula",
+
+	"vcmi.optionsTab.simturnsTitle" : "Turni simultanei",
+	"vcmi.optionsTab.simturnsMin.hover" : "Almeno per",
+	"vcmi.optionsTab.simturnsMax.hover" : "Al massimo per",
+	"vcmi.optionsTab.simturnsAI.hover" : "(Sperimentale) Turni simultanei IA",
+	"vcmi.optionsTab.simturnsMin.help" : "Gioca simultaneamente per il numero di giorni specificato. I contatti tra i giocatori durante questo periodo sono bloccati.",
+	"vcmi.optionsTab.simturnsMax.help" : "Gioca simultaneamente per il numero di giorni specificato o fino al contatto con un altro giocatore.",
+	"vcmi.optionsTab.simturnsAI.help" : "{Turni simultanei IA}\nOpzione sperimentale. Consente ai giocatori IA di agire contemporaneamente ai giocatori umani quando i turni simultanei sono abilitati.",
+
+	"vcmi.optionsTab.turnTime.select"     : "Seleziona preset timer turno",
+	"vcmi.optionsTab.turnTime.unlimited"  : "Tempo di turno illimitato",
+	"vcmi.optionsTab.turnTime.classic.1"  : "Timer classico: 1 minuto",
+	"vcmi.optionsTab.turnTime.classic.2"  : "Timer classico: 2 minuti",
+	"vcmi.optionsTab.turnTime.classic.5"  : "Timer classico: 5 minuti",
+	"vcmi.optionsTab.turnTime.classic.10" : "Timer classico: 10 minuti",
+	"vcmi.optionsTab.turnTime.classic.20" : "Timer classico: 20 minuti",
+	"vcmi.optionsTab.turnTime.classic.30" : "Timer classico: 30 minuti",
+	"vcmi.optionsTab.turnTime.chess.20"   : "Scacchi: 20:00 + 10:00 + 02:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.16"   : "Scacchi: 16:00 + 08:00 + 01:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.8"    : "Scacchi: 08:00 + 04:00 + 01:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.4"    : "Scacchi: 04:00 + 02:00 + 00:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.2"    : "Scacchi: 02:00 + 01:00 + 00:15 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.1"    : "Scacchi: 01:00 + 01:00 + 00:00 + 00:00",
+
+	"vcmi.optionsTab.simturns.select"         : "Seleziona preset turni simultanei",
+	"vcmi.optionsTab.simturns.none"           : "Nessun turno simultaneo",
+	"vcmi.optionsTab.simturns.tillContactMax" : "Turni simultanei: fino al contatto",
+	"vcmi.optionsTab.simturns.tillContact1"   : "Turni simultanei: 1 settimana, interruzione al contatto",
+	"vcmi.optionsTab.simturns.tillContact2"   : "Turni simultanei: 2 settimane, interruzione al contatto",
+	"vcmi.optionsTab.simturns.tillContact4"   : "Turni simultanei: 1 mese, interruzione al contatto",
+	"vcmi.optionsTab.simturns.blocked1"       : "Turni simultanei: 1 settimana, contatti bloccati",
+	"vcmi.optionsTab.simturns.blocked2"       : "Turni simultanei: 2 settimane, contatti bloccati",
+	"vcmi.optionsTab.simturns.blocked4"       : "Turni simultanei: 1 mese, contatti bloccati",
+	
+	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
+	// Using this information, VCMI will automatically select correct plural form for every possible amount
+	"vcmi.optionsTab.simturns.days.0" : " %d giorni",
+	"vcmi.optionsTab.simturns.days.1" : " %d giorno",
+	"vcmi.optionsTab.simturns.days.2" : " %d giorni",
+	"vcmi.optionsTab.simturns.weeks.0" : " %d settimane",
+	"vcmi.optionsTab.simturns.weeks.1" : " %d settimana",
+	"vcmi.optionsTab.simturns.weeks.2" : " %d settimane",
+	"vcmi.optionsTab.simturns.months.0" : " %d mesi",
+	"vcmi.optionsTab.simturns.months.1" : " %d mese",
+	"vcmi.optionsTab.simturns.months.2" : " %d mesi",
+
+	"vcmi.optionsTab.extraOptions.hover" : "Opzioni extra",
+	"vcmi.optionsTab.extraOptions.help" : "Impostazioni aggiuntive per il gioco",
+
+	"vcmi.optionsTab.cheatAllowed.hover" : "Permetti trucchi",
+	"vcmi.optionsTab.unlimitedReplay.hover" : "Replay battaglia illimitato",
+	"vcmi.optionsTab.cheatAllowed.help" : "{Permetti trucchi}\nPermette l'inserimento di trucchi durante il gioco.",
+	"vcmi.optionsTab.unlimitedReplay.help" : "{Replay battaglia illimitato}\nNessun limite di riproduzione delle battaglie.",
+
+	// Custom victory conditions for H3 campaigns and HotA maps
+	"vcmi.map.victoryCondition.daysPassed.toOthers" : "Il nemico è riuscito a sopravvivere fino a questo giorno. La vittoria è sua!",
+	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Congratulazioni! Sei riuscito a sopravvivere. La vittoria è tua!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "Il nemico ha sconfitto tutti i mostri che infestavano questa terra e reclama la vittoria!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Congratulazioni! Hai sconfitto tutti i mostri che infestavano questa terra e puoi reclamare la vittoria!",
+	"vcmi.map.victoryCondition.collectArtifacts.message" : "Acquisisci tre artefatti",
+	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Congratulazioni! Hai sconfitto tutti i tuoi nemici e hai l'Alleanza Angelica! La vittoria è tua!",
+	"vcmi.map.victoryCondition.angelicAlliance.message" : "Sconfiggi tutti i nemici e crea l'Alleanza Angelica",
+	"vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Ahimè, hai perso parte dell'Alleanza Angelica. Tutto è perduto.",
+
+	// few strings from WoG used by vcmi
+	"vcmi.stackExperience.description" : "» Dettagli esperienza truppa «\n\nTipo di creatura ................... : %s\nGrado esperienza ................. : %s (%i)\nPunti esperienza ............... : %i\nPunti esperienza al livello successivo .. : %i\nEsperienza massima per battaglia ... : %i%% (%i)\nNumero di creature nello stack .... : %i\nNumero massimo di nuove reclute\n senza perdere il grado attuale .... : %i\nMoltiplicatore esperienza ........... : %.2f\nMoltiplicatore miglioramento .............. : %.2f\nEsperienza dopo il livello 10 ........ : %i\nNumero massimo di nuove reclute per mantenere il\n grado 10 se a esperienza massima : %i",
+	"vcmi.stackExperience.rank.0" : "Base",
+	"vcmi.stackExperience.rank.1" : "Principiante",
+	"vcmi.stackExperience.rank.2" : "Addestrato",
+	"vcmi.stackExperience.rank.3" : "Abile",
+	"vcmi.stackExperience.rank.4" : "Esperto",
+	"vcmi.stackExperience.rank.5" : "Veterano",
+	"vcmi.stackExperience.rank.6" : "Maestro",
+	"vcmi.stackExperience.rank.7" : "Esperto",
+	"vcmi.stackExperience.rank.8" : "Gran Maestro",
+	"vcmi.stackExperience.rank.9" : "Campione",
+	"vcmi.stackExperience.rank.10" : "Asso",
+	
+	// Strings for HotA Seer Hut / Quest Guards
+	"core.seerhut.quest.heroClass.complete.0" : "Ah, sei %s. Ecco un regalo per te. Lo accetti?",
+	"core.seerhut.quest.heroClass.complete.1" : "Ah, sei %s. Ecco un regalo per te. Lo accetti?",
+	"core.seerhut.quest.heroClass.complete.2" : "Ah, sei %s. Ecco un regalo per te. Lo accetti?",
+	"core.seerhut.quest.heroClass.complete.3" : "Le guardie notano che sei %s e ti offrono di passare. Accetti?",
+	"core.seerhut.quest.heroClass.complete.4" : "Le guardie notano che sei %s e ti offrono di passare. Accetti?",
+	"core.seerhut.quest.heroClass.complete.5" : "Le guardie notano che sei %s e ti offrono di passare. Accetti?",
+	"core.seerhut.quest.heroClass.description.0" : "Invia %s a %s",
+	"core.seerhut.quest.heroClass.description.1" : "Invia %s a %s",
+	"core.seerhut.quest.heroClass.description.2" : "Invia %s a %s",
+	"core.seerhut.quest.heroClass.description.3" : "Invia %s ad aprire il cancello",
+	"core.seerhut.quest.heroClass.description.4" : "Invia %s ad aprire il cancello",
+	"core.seerhut.quest.heroClass.description.5" : "Invia %s ad aprire il cancello",
+	"core.seerhut.quest.heroClass.hover.0" : "(cerca un eroe della classe %s)",
+	"core.seerhut.quest.heroClass.hover.1" : "(cerca un eroe della classe %s)",
+	"core.seerhut.quest.heroClass.hover.2" : "(cerca un eroe della classe %s)",
+	"core.seerhut.quest.heroClass.hover.3" : "(cerca un eroe della classe %s)",
+	"core.seerhut.quest.heroClass.hover.4" : "(cerca un eroe della classe %s)",
+	"core.seerhut.quest.heroClass.hover.5" : "(cerca un eroe della classe %s)",
+	"core.seerhut.quest.heroClass.receive.0" : "Ho un regalo per %s.",
+	"core.seerhut.quest.heroClass.receive.1" : "Ho un regalo per %s.",
+	"core.seerhut.quest.heroClass.receive.2" : "Ho un regalo per %s.",
+	"core.seerhut.quest.heroClass.receive.3" : "Le guardie qui dicono che lasceranno passare solo %s.",
+	"core.seerhut.quest.heroClass.receive.4" : "Le guardie qui dicono che lasceranno passare solo %s.",
+	"core.seerhut.quest.heroClass.receive.5" : "Le guardie qui dicono che lasceranno passare solo %s.",
+	"core.seerhut.quest.heroClass.visit.0" : "Non sei %s. Non ho niente per te. Vattene!",
+	"core.seerhut.quest.heroClass.visit.1" : "Non sei %s. Non ho niente per te. Vattene!",
+	"core.seerhut.quest.heroClass.visit.2" : "Non sei %s. Non ho niente per te. Vattene!",
+	"core.seerhut.quest.heroClass.visit.3" : "Le guardie qui lasceranno passare solo %s.",
+	"core.seerhut.quest.heroClass.visit.4" : "Le guardie qui lasceranno passare solo %s.",
+	"core.seerhut.quest.heroClass.visit.5" : "Le guardie qui lasceranno passare solo %s.",
+	
+	"core.seerhut.quest.reachDate.complete.0" : "Ora sono libero. Ecco cosa ho per te. Lo accetti?",
+	"core.seerhut.quest.reachDate.complete.1" : "Ora sono libero. Ecco cosa ho per te. Lo accetti?",
+	"core.seerhut.quest.reachDate.complete.2" : "Ora sono libero. Ecco cosa ho per te. Lo accetti?",
+	"core.seerhut.quest.reachDate.complete.3" : "Ora puoi passare. Vuoi attraversare?",
+	"core.seerhut.quest.reachDate.complete.4" : "Ora puoi passare. Vuoi attraversare?",
+	"core.seerhut.quest.reachDate.complete.5" : "Ora puoi passare. Vuoi attraversare?",
+	"core.seerhut.quest.reachDate.description.0" : "Aspetta fino a %s per %s",
+	"core.seerhut.quest.reachDate.description.1" : "Aspetta fino a %s per %s",
+	"core.seerhut.quest.reachDate.description.2" : "Aspetta fino a %s per %s",
+	"core.seerhut.quest.reachDate.description.3" : "Aspetta fino a %s per aprire il cancello",
+	"core.seerhut.quest.reachDate.description.4" : "Aspetta fino a %s per aprire il cancello",
+	"core.seerhut.quest.reachDate.description.5" : "Aspetta fino a %s per aprire il cancello",
+	"core.seerhut.quest.reachDate.hover.0" : "(Non tornare prima di %s)",
+	"core.seerhut.quest.reachDate.hover.1" : "(Non tornare prima di %s)",
+	"core.seerhut.quest.reachDate.hover.2" : "(Non tornare prima di %s)",
+	"core.seerhut.quest.reachDate.hover.3" : "(Non tornare prima di %s)",
+	"core.seerhut.quest.reachDate.hover.4" : "(Non tornare prima di %s)",
+	"core.seerhut.quest.reachDate.hover.5" : "(Non tornare prima di %s)",
+	"core.seerhut.quest.reachDate.receive.0" : "Sono occupato. Torna dopo il %s",
+	"core.seerhut.quest.reachDate.receive.1" : "Sono occupato. Torna dopo il %s",
+	"core.seerhut.quest.reachDate.receive.2" : "Sono occupato. Torna dopo il %s",
+	"core.seerhut.quest.reachDate.receive.3" : "Chiuso fino al %s.",
+	"core.seerhut.quest.reachDate.receive.4" : "Chiuso fino al %s.",
+	"core.seerhut.quest.reachDate.receive.5" : "Chiuso fino al %s.",
+	"core.seerhut.quest.reachDate.visit.0" : "Sono occupato. Torna dopo il %s.",
+	"core.seerhut.quest.reachDate.visit.1" : "Sono occupato. Torna dopo il %s.",
+	"core.seerhut.quest.reachDate.visit.2" : "Sono occupato. Torna dopo il %s.",
+	"core.seerhut.quest.reachDate.visit.3" : "Chiuso fino al %s.",
+	"core.seerhut.quest.reachDate.visit.4" : "Chiuso fino al %s.",
+	"core.seerhut.quest.reachDate.visit.5" : "Chiuso fino al %s.",
+	
+	"mapObject.core.hillFort.object.description" : "Aggiorna le creature. I livelli 1 - 4 sono meno costosi rispetto alla città associata.",
+	
+	"core.bonus.ADDITIONAL_ATTACK.name": "Doppio colpo",
+	"core.bonus.ADDITIONAL_ATTACK.description": "Attacca due volte",
+	"core.bonus.ADDITIONAL_RETALIATION.name": "Ritorsioni aggiuntive",
+	"core.bonus.ADDITIONAL_RETALIATION.description": "Può contrattaccare ${val} volte in più",
+	"core.bonus.AIR_IMMUNITY.name": "Immunità all'aria",
+	"core.bonus.AIR_IMMUNITY.description": "Immune a tutti gli incantesimi della scuola di magia dell'Aria",
+	"core.bonus.ATTACKS_ALL_ADJACENT.name": "Attacco a 360°",
+	"core.bonus.ATTACKS_ALL_ADJACENT.description": "Attacca tutti i nemici adiacenti",
+	"core.bonus.BLOCKS_RETALIATION.name": "Nessuna ritorsione",
+	"core.bonus.BLOCKS_RETALIATION.description": "Il nemico non può contrattaccare",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.name": "Nessuna ritorsione a distanza",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.description": "Il nemico non può contrattaccare con un attacco a distanza",
+	"core.bonus.CATAPULT.name": "Catapulta",
+	"core.bonus.CATAPULT.description": "Attacca le mura d'assedio",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Riduce il costo del lancio (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Riduce il costo del lancio degli incantesimi dell'eroe di ${val}",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Resistenza magica (${val}%)",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Aumenta il costo del lancio degli incantesimi nemici di ${val}",
+	"core.bonus.CHARGE_IMMUNITY.name": "Immunità alla carica",
+	"core.bonus.CHARGE_IMMUNITY.description": "Immune alla carica di Cavalieri e Campioni",
+	"core.bonus.DARKNESS.name": "Oscurità",
+	"core.bonus.DARKNESS.description": "Crea un velo d'oscurità con raggio ${val}",
+	"core.bonus.DEATH_STARE.name": "Sguardo della morte (${val}%)",
+	"core.bonus.DEATH_STARE.description": "Ha una probabilità del ${val}% di uccidere un'unità singola",
+	"core.bonus.DEFENSIVE_STANCE.name": "Bonus di difesa",
+	"core.bonus.DEFENSIVE_STANCE.description": "+${val} Difesa quando è in posizione difensiva",
+	"core.bonus.DESTRUCTION.name": "Distruzione",
+	"core.bonus.DESTRUCTION.description": "Ha una probabilità del ${val}% di uccidere unità extra dopo l'attacco",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Colpo della morte",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Ha una probabilità del ${val}% di infliggere il doppio dei danni base quando attacca",
+	"core.bonus.DRAGON_NATURE.name": "Drago",
+	"core.bonus.DRAGON_NATURE.description": "Creatura con Natura del Drago",
+	"core.bonus.EARTH_IMMUNITY.name": "Immunità alla terra",
+	"core.bonus.EARTH_IMMUNITY.description": "Immune a tutti gli incantesimi della scuola di magia della Terra",
+	"core.bonus.ENCHANTER.name": "Incantatore",
+	"core.bonus.ENCHANTER.description": "Può lanciare l'incantesimo ${subtype.spell} ogni turno",
+	"core.bonus.ENCHANTED.name": "Incantato",
+	"core.bonus.ENCHANTED.description": "Sotto effetto permanente di ${subtype.spell}",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.name": "Ignora attacco (${val}%)",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.description": "Quando viene attaccata, ignora il ${val}% dell'attacco dell'avversario",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignora difesa (${val}%)",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Quando attacca, ignora il ${val}% della difesa dell'avversario",
+	"core.bonus.FIRE_IMMUNITY.name": "Immunità al fuoco",
+	"core.bonus.FIRE_IMMUNITY.description": "Immune a tutti gli incantesimi della scuola di magia del Fuoco",
+	"core.bonus.FIRE_SHIELD.name": "Scudo di fuoco (${val}%)",
+	"core.bonus.FIRE_SHIELD.description": "Riflette una parte dei danni da mischia",
+	"core.bonus.FIRST_STRIKE.name": "Primo colpo",
+	"core.bonus.FIRST_STRIKE.description": "Questa creatura contrattacca prima di essere attaccata",
+	"core.bonus.FEAR.name": "Paura",
+	"core.bonus.FEAR.description": "Provoca paura su una pila nemica",
+	"core.bonus.FEARLESS.name": "Impavido",
+	"core.bonus.FEARLESS.description": "Immune all'abilità Paura",
+	"core.bonus.FEROCITY.name": "Ferocia",
+	"core.bonus.FEROCITY.description": "Attacca ${val} volte aggiuntive se uccide qualcuno",
+	"core.bonus.FLYING.name": "Volare",
+	"core.bonus.FLYING.description": "Si muove volando (ignora gli ostacoli)",
+	"core.bonus.FREE_SHOOTING.name": "Colpo ravvicinato",
+	"core.bonus.FREE_SHOOTING.description": "Può usare attacchi a distanza anche in mischia",
+	"core.bonus.GARGOYLE.name": "Gargoyle",
+	"core.bonus.GARGOYLE.description": "Non può essere rianimato o curato",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Riduzione danno (${val}%)",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Riduce il danno fisico da attacchi a distanza o corpo a corpo",
+	"core.bonus.HATE.name": "Odia ${subtype.creature}",
+	"core.bonus.HATE.description": "Infligge ${val}% di danni in più a ${subtype.creature}",
+	"core.bonus.HEALER.name": "Guaritore",
+	"core.bonus.HEALER.description": "Cura le unità alleate",
+	"core.bonus.HP_REGENERATION.name": "Rigenerazione",
+	"core.bonus.HP_REGENERATION.description": "Cura ${val} punti ferita ogni turno",
+	"core.bonus.JOUSTING.name": "Carica del Campione",
+	"core.bonus.JOUSTING.description": "+${val}% danno per ogni esagono percorso",
+	"core.bonus.KING.name": "Re",
+	"core.bonus.KING.description": "Vulnerabile a SLAUGHTER di livello ${val} o superiore",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Immunità agli incantesimi 1-${val}",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immunità agli incantesimi di livello 1-${val}",
+	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Portata limitata",
+	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Impossibile attaccare unità oltre ${val} esagoni",
+	"core.bonus.LIFE_DRAIN.name": "Assorbimento vitale (${val}%)",
+	"core.bonus.LIFE_DRAIN.description": "Drena ${val}% del danno inflitto",
+	"core.bonus.MANA_CHANNELING.name": "Canale Magico ${val}%",
+	"core.bonus.MANA_CHANNELING.description": "Fornisce al tuo eroe ${val}% del mana speso dal nemico",
+	"core.bonus.MANA_DRAIN.name": "Drenaggio di mana",
+	"core.bonus.MANA_DRAIN.description": "Drena ${val} mana ogni turno",
+	"core.bonus.MAGIC_MIRROR.name": "Specchio Magico (${val}%)",
+	"core.bonus.MAGIC_MIRROR.description": "Ha una probabilità del ${val}% di reindirizzare un incantesimo offensivo su un'unità nemica",
+	"core.bonus.MAGIC_RESISTANCE.name": "Resistenza Magica (${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description": "Ha una probabilità del ${val}% di resistere a un incantesimo nemico",
+	"core.bonus.MIND_IMMUNITY.name": "Immunità agli incantesimi mentali",
+	"core.bonus.MIND_IMMUNITY.description": "Immune agli incantesimi di tipo mentale",
+	"core.bonus.NO_DISTANCE_PENALTY.name": "Nessuna penalità a distanza",
+	"core.bonus.NO_DISTANCE_PENALTY.description": "Infligge il massimo danno a qualsiasi distanza",
+	"core.bonus.NO_MELEE_PENALTY.name": "Nessuna penalità in mischia",
+	"core.bonus.NO_MELEE_PENALTY.description": "L'unità non subisce penalità in mischia",
+	"core.bonus.NO_MORALE.name": "Morale neutrale",
+	"core.bonus.NO_MORALE.description": "L'unità è immune agli effetti del morale",
+	"core.bonus.NO_WALL_PENALTY.name": "Nessuna penalità per le mura",
+	"core.bonus.NO_WALL_PENALTY.description": "Danno pieno durante l'assedio",
+	"core.bonus.NON_LIVING.name": "Non vivente",
+	"core.bonus.NON_LIVING.description": "Immunità a molti effetti",
+	"core.bonus.RANDOM_SPELLCASTER.name": "Random spellcaster",
+	"core.bonus.RANDOM_SPELLCASTER.description": "Può lanciare un incantesimo casuale",
+	"core.bonus.RANGED_RETALIATION.name": "Ritorsione a distanza",
+	"core.bonus.RANGED_RETALIATION.description": "Può effettuare un contrattacco a distanza",
+	"core.bonus.RECEPTIVE.name": "Ricettivo",
+	"core.bonus.RECEPTIVE.description": "Nessuna immunità agli incantesimi amichevoli",
+	"core.bonus.REBIRTH.name": "Rinascita (${val}%)",
+	"core.bonus.REBIRTH.description": "${val}% della pila risorgerà dopo la morte",
+	"core.bonus.RETURN_AFTER_STRIKE.name": "Attacco e Ritorno",
+	"core.bonus.RETURN_AFTER_STRIKE.description": "Ritorna dopo un attacco in mischia",
+	"core.bonus.REVENGE.name": "Vendetta",
+	"core.bonus.REVENGE.description": "Infligge danni extra in base alla salute persa dell'attaccante in battaglia",
+	"core.bonus.SHOOTER.name": "A distanza",
+	"core.bonus.SHOOTER.description": "L'unità può attaccare a distanza",
+	"core.bonus.SHOOTS_ALL_ADJACENT.name": "Tiro a raggio totale",
+	"core.bonus.SHOOTS_ALL_ADJACENT.description": "Gli attacchi a distanza di questa unità colpiscono tutti i bersagli in una piccola area",
+	"core.bonus.SOUL_STEAL.name": "Furto d'anima",
+	"core.bonus.SOUL_STEAL.description": "Ottiene ${val} nuove creature per ogni nemico ucciso",
+	"core.bonus.SPELLCASTER.name": "Incantatore",
+	"core.bonus.SPELLCASTER.description": "Può lanciare ${subtype.spell}",
+	"core.bonus.SPELL_AFTER_ATTACK.name": "Lancia Dopo l'Attacco",
+	"core.bonus.SPELL_AFTER_ATTACK.description": "Ha una probabilità del ${val}% di lanciare ${subtype.spell} dopo l'attacco",
+	"core.bonus.SPELL_BEFORE_ATTACK.name": "Lancia Prima dell'Attacco",
+	"core.bonus.SPELL_BEFORE_ATTACK.description": "Ha una probabilità del ${val}% di lanciare ${subtype.spell} prima dell'attacco",
+	"core.bonus.SPELL_IMMUNITY.name": "Immunità agli incantesimi",
+	"core.bonus.SPELL_IMMUNITY.description": "Immune a ${subtype.spell}",
+	"core.bonus.SPELL_LIKE_ATTACK.name": "Attacco simile a un incantesimo",
+	"core.bonus.SPELL_LIKE_ATTACK.description": "Attacca con ${subtype.spell}",
+	"core.bonus.SPELL_RESISTANCE_AURA.name": "Aura di Resistenza",
+	"core.bonus.SPELL_RESISTANCE_AURA.description": "Gli stack vicini ottengono ${val}% di resistenza magica",
+	"core.bonus.SUMMON_GUARDIANS.name": "Evoca guardiani",
+	"core.bonus.SUMMON_GUARDIANS.description": "All'inizio della battaglia evoca ${subtype.creature} (${val}%)",
+	"core.bonus.SYNERGY_TARGET.name": "Sinergizzabile",
+	"core.bonus.SYNERGY_TARGET.description": "Questa creatura è vulnerabile all'effetto sinergico",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.name": "Soffio",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.description": "Attacco a soffio (raggio di 2 esagoni)",
+	"core.bonus.THREE_HEADED_ATTACK.name": "Attacco a tre teste",
+	"core.bonus.THREE_HEADED_ATTACK.description": "Attacca tre unità adiacenti",
+	"core.bonus.TRANSMUTATION.name": "Trasmutazione",
+	"core.bonus.TRANSMUTATION.description": "${val}% di possibilità di trasformare l'unità attaccata in un altro tipo",
+	"core.bonus.UNDEAD.name": "Non Morto",
+	"core.bonus.UNDEAD.description": "L'unità è Non Morta",
+	"core.bonus.UNLIMITED_RETALIATIONS.name": "Ritorsioni illimitate",
+	"core.bonus.UNLIMITED_RETALIATIONS.description": "Può contrattaccare un numero illimitato di attacchi",
+	"core.bonus.WATER_IMMUNITY.name": "Immunità all'acqua",
+	"core.bonus.WATER_IMMUNITY.description": "Immune a tutti gli incantesimi della scuola di magia dell'Acqua",
+	"core.bonus.WIDE_BREATH.name": "Soffio ampio",
+	"core.bonus.WIDE_BREATH.description": "Attacco a soffio ampio (più esagoni)",
+	"core.bonus.DISINTEGRATE.name": "Disintegrazione",
+	"core.bonus.DISINTEGRATE.description": "Nessun cadavere rimane dopo la morte",
+	"core.bonus.INVINCIBLE.name": "Invincibile",
+	"core.bonus.INVINCIBLE.description": "Non può essere influenzato da nulla",
+	"core.bonus.MECHANICAL.name": "Meccanico",
+	"core.bonus.MECHANICAL.description": "Immunità a molti effetti, riparabile",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Soffio Prisma",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Attacco Soffio Prisma (tre direzioni)",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Resistenza agli incantesimi",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.air": "Resistenza agli incantesimi dell'Aria",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.fire": "Resistenza agli incantesimi di fuoco",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.water": "Resistenza agli incantesimi dell'Acqua",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name.earth": "Resistenza agli incantesimi della Terra",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Danno da tutti gli incantesimi ridotto del ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.air": "Danno da tutti gli incantesimi dell'Aria ridotto del ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.fire": "Danno da tutti gli incantesimi del Fuoco ridotto del ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.water": "Danno da tutti gli incantesimi dell'Acqua ridotto del ${val}%.",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description.earth": "Danno da tutti gli incantesimi della Terra ridotto del ${val}%.",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name": "Immunità agli incantesimi",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.air": "Immunità all'aria",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.fire": "Immunità al fuoco",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.water": "Immunità all'acqua",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.name.earth": "Immunità alla terra",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description": "Questa unità è immune a tutti gli incantesimi",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.air": "Questa unità è immune a tutti gli incantesimi della scuola dell'Aria",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.fire": "Questa unità è immune a tutti gli incantesimi della scuola del Fuoco",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "Questa unità è immune a tutti gli incantesimi della scuola dell'Acqua",
+	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "Questa unità è immune a tutti gli incantesimi della scuola della Terra",
+	"core.bonus.OPENING_BATTLE_SPELL.name": "Inizia con incantesimo",
+	"core.bonus.OPENING_BATTLE_SPELL.description": "Lancia ${subtype.spell} all'inizio della battaglia",
+	
+	"spell.core.castleMoat.name" : "Fossato",
+	"spell.core.castleMoatTrigger.name" : "Fossato",
+	"spell.core.catapultShot.name" : "Colpo di Catapulta",
+	"spell.core.cyclopsShot.name" : "Colpo d'assedio",
+	"spell.core.dungeonMoat.name" : "Olio Bollente",
+	"spell.core.dungeonMoatTrigger.name" : "Olio Bollente",
+	"spell.core.fireWallTrigger.name" : "Muro di Fuoco",
+	"spell.core.firstAid.name" : "Pronto Soccorso",
+	"spell.core.fortressMoat.name" : "Catrame Bollente",
+	"spell.core.fortressMoatTrigger.name" : "Catrame Bollente",
+	"spell.core.infernoMoat.name" : "Lava",
+	"spell.core.infernoMoatTrigger.name" : "Lava",
+	"spell.core.landMineTrigger.name" : "Mina Terrestre",
+	"spell.core.necropolisMoat.name" : "Cimitero d'ossa",
+	"spell.core.necropolisMoatTrigger.name" : "Cimitero di ossa",
+	"spell.core.rampartMoat.name" : "Cimitero di ossa",
+	"spell.core.rampartMoatTrigger.name" : "Rovi",
+	"spell.core.strongholdMoat.name" : "Rovi",
+	"spell.core.strongholdMoatTrigger.name" : "Spuntoni di legno",
+	"spell.core.summonDemons.name" : "Spuntoni di legno",
+	"spell.core.towerMoat.name" : "Mina terrestre"
+}

+ 9 - 8
Mods/vcmi/Content/config/polish.json

@@ -23,8 +23,8 @@
 	"vcmi.adventureMap.noTownWithTavern"       : "Brak dostępnego miasta z karczmą!",
 	"vcmi.adventureMap.spellUnknownProblem"    : "Nieznany problem z zaklęciem, brak dodatkowych informacji.",
 	"vcmi.adventureMap.playerAttacked"         : "Gracz został zaatakowany: %s",
-	"vcmi.adventureMap.moveCostDetails"        : "Punkty ruchu - Koszt: %TURNS tury + %POINTS punktów, Pozostanie: %REMAINING punktów",
-	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Punkty ruchu - Koszt: %POINTS punktów, Pozostanie: %REMAINING punktów",
+	"vcmi.adventureMap.moveCostDetails"        : "Ruch tutaj będzie kosztował łącznie {%TOTAL} punktów ({%TURNS} tur i {%POINTS} punktów). {%REMAINING} punktów zostanie po ruchu.",
+	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Ruch tutaj będzie kosztował {%POINTS} punktów. {%REMAINING} punktów zostanie po ruchu.",
 	"vcmi.adventureMap.movementPointsHeroInfo" : "(Punkty ruchu: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Wybacz, powtórka ruchu wroga nie została jeszcze zaimplementowana!",
 
@@ -106,6 +106,7 @@
 	"vcmi.radialWheel.heroGetArtifacts" : "Weź artefakty z innego bohatera",
 	"vcmi.radialWheel.heroSwapArtifacts" : "Zamień artefakty z innym bohaterem",
 	"vcmi.radialWheel.heroDismiss" : "Dymisja bohatera",
+	"vcmi.radialWheel.upgradeCreatures" : "Ulepsz wszystkie stworzenia",
 
 	"vcmi.radialWheel.moveTop" : "Przenieś na początek",
 	"vcmi.radialWheel.moveUp" : "Przenieś w górę",
@@ -212,12 +213,12 @@
 	"vcmi.lobby.pvp.randomTownVs.hover" : "Wylosuj 2 miasta",
 	"vcmi.lobby.pvp.randomTownVs.help" : "Wyświetli nazwę 2 wylosowanych miast na czacie, które nie zostały zablokowane na liście",
 	"vcmi.lobby.pvp.versus" : "vs.",
-	"vcmi.lobby.deleteFile" : "Czy chcesz usunąć ten plik ?",
-	"vcmi.lobby.deleteFolder" : "Czy chcesz usunąć ten folder ?",
+	"vcmi.lobby.deleteFile" : "Czy chcesz usunąć ten plik?",
+	"vcmi.lobby.deleteFolder" : "Czy chcesz usunąć ten folder?",
 	"vcmi.lobby.deleteMapTitle" : "Wskaż tytuł, który chcesz usunąć",
 	"vcmi.lobby.deleteMode" : "Przełącza tryb na usuwanie i spowrotem",
 	"vcmi.lobby.deleteSaveGameTitle" : "Wskaż zapis gry do usunięcia",
-	"vcmi.lobby.deleteUnsupportedSave" : "{Znaleziono niekompatybilne zapisy gry}\n\nVCMI wykrył %d zapisów gry, które nie są już wspierane. Prawdopodobnie ze względu na różne wersje gry.\n\nCzy chcesz je usunąć ?",
+	"vcmi.lobby.deleteUnsupportedSave" : "{Znaleziono niekompatybilne zapisy gry}\n\nVCMI wykrył %d zapisów gry, które nie są już wspierane. Prawdopodobnie ze względu na różne wersje gry.\n\nCzy chcesz je usunąć?",
 
 	"vcmi.client.errors.invalidMap" : "{Błędna mapa lub kampania}\n\nNie udało się stworzyć gry! Wybrana mapa lub kampania jest niepoprawna lub uszkodzona. Powód:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Brakujące pliki gry}\n\nPliki kampanii nie zostały znalezione! Możliwe że używasz niekompletnych lub uszkodzonych plików Heroes 3. Spróbuj ponownej instalacji plików gry.",
@@ -422,11 +423,11 @@
 	"vcmi.heroWindow.openBackpack.hover" : "Otwórz okno sakwy",
 	"vcmi.heroWindow.openBackpack.help"  : "Otwiera okno pozwalające łatwiej zarządzać artefaktami w sakwie",
 	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Sortuj wg. wartości",
-	"vcmi.heroWindow.sortBackpackByCost.help"  : "Sortuj artefakty w sakwie według wartości",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "{Sortuj wg. wartości}\n\nSortuj artefakty w sakwie według wartości",
 	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Sortuj wg. miejsc",
-	"vcmi.heroWindow.sortBackpackBySlot.help"  : "Sortuj artefakty w sakwie według umiejscowienia na ciele",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "{Sortuj wg. miejsc}\n\nSortuj artefakty w sakwie według umiejscowienia na ciele",
 	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Sortuj wg. jakości",
-	"vcmi.heroWindow.sortBackpackByClass.help"  : "Sortuj artefakty w sakwie według jakości: Skarb, Pomniejszy, Potężny, Relikt",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "{Sortuj wg. jakości}\n\nSortuj artefakty w sakwie według jakości: Skarb, Pomniejszy, Potężny, Relikt",
 	"vcmi.heroWindow.fusingArtifact.fusing" : "Posiadasz wszystkie niezbędne komponenty do stworzenia %s. Czy chcesz wykonać fuzję? {Wszystkie komponenty zostaną użyte}",
 
 	"vcmi.tavernWindow.inviteHero"  : "Zaproś bohatera",

+ 3 - 5
Mods/vcmi/Content/config/portuguese.json

@@ -23,8 +23,6 @@
 	"vcmi.adventureMap.noTownWithTavern"                 : "Não há cidades disponíveis com tavernas!",
 	"vcmi.adventureMap.spellUnknownProblem"              : "Há um problema desconhecido com este feitiço! Não há mais informações disponíveis.",
 	"vcmi.adventureMap.playerAttacked"                   : "O jogador foi atacado: %s",
-	"vcmi.adventureMap.moveCostDetails"                  : "Pontos de movimento - Custo: %TURNS turnos + %POINTS pontos, Pontos restantes: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Pontos de movimento - Custo: %POINTS pontos, Pontos restantes: %REMAINING",
 	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(Pontos de movimento: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Desculpe, a repetição do turno do oponente ainda não está implementada!",
 
@@ -424,11 +422,11 @@
 	"vcmi.heroWindow.openBackpack.hover" : "Abrir janela da mochila de artefatos",
 	"vcmi.heroWindow.openBackpack.help" : "Abre a janela que facilita o gerenciamento da mochila de artefatos.",
 	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Ordenar por custo",
-	"vcmi.heroWindow.sortBackpackByCost.help"   : "Ordena artefatos na mochila por custo.",
+	"vcmi.heroWindow.sortBackpackByCost.help"   : "{Ordenar por custo}\n\nOrdena artefatos na mochila por custo.",
 	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Ordenar por espaço",
-	"vcmi.heroWindow.sortBackpackBySlot.help"   : "Ordena artefatos na mochila por espaço equipado.",
+	"vcmi.heroWindow.sortBackpackBySlot.help"   : "{Ordenar por espaço}\n\nOrdena artefatos na mochila por espaço equipado.",
 	"vcmi.heroWindow.sortBackpackByClass.hover" : "Ordenar por classe",
-	"vcmi.heroWindow.sortBackpackByClass.help"  : "Ordena artefatos na mochila por classe de artefato. Tesouro, Menor, Maior, Relíquia.",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "{Ordenar por classe}\n\nOrdena artefatos na mochila por classe de artefato. Tesouro, Menor, Maior, Relíquia.",
 	"vcmi.heroWindow.fusingArtifact.fusing" : "Você possui todos os componentes necessários para a fusão de %s. Deseja realizar a fusão? {Todos os componentes serão consumidos após a fusão.}",
 
 	"vcmi.tavernWindow.inviteHero" : "Convidar herói",

+ 4 - 4
Mods/vcmi/Content/config/rmg/hdmod/coldshadowsFantasy.json

@@ -159,7 +159,7 @@
 			},
 			"17":
 			{
-				"type" : "junction", "size" : 30,
+				"type" : "junction", "size" : 15,
 				"terrainTypeLikeZone" : 9,
 				"allowedTowns" : ["neutral"],
 				"monsters" : "strong",
@@ -172,7 +172,7 @@
 			},
 			"18":
 			{
-				"type" : "junction", "size" : 30,
+				"type" : "junction", "size" : 15,
 				"terrainTypeLikeZone" : 9,
 				"allowedTowns" : ["neutral"],
 				"monsters" : "strong",
@@ -181,7 +181,7 @@
 			},
 			"19":
 			{
-				"type" : "junction", "size" : 30,
+				"type" : "junction", "size" : 15,
 				"terrainTypeLikeZone" : 9,
 				"allowedTowns" : ["neutral"],
 				"monsters" : "strong",
@@ -190,7 +190,7 @@
 			},
 			"20":
 			{
-				"type" : "junction", "size" : 30,
+				"type" : "junction", "size" : 15,
 				"terrainTypeLikeZone" : 9,
 				"allowedTowns" : ["neutral"],
 				"monsters" : "strong",

+ 0 - 2
Mods/vcmi/Content/config/russian.json

@@ -18,8 +18,6 @@
 	"vcmi.adventureMap.noTownWithTavern"       : "Нет союзных городов с тавернами!",
 	"vcmi.adventureMap.spellUnknownProblem"    : "Неизвестная проблема с заклинанием, дополнительная информация недоступна.",
 	"vcmi.adventureMap.playerAttacked"         : "Игрок атакован: %s",
-	"vcmi.adventureMap.moveCostDetails"        : "Очки движения - Стоимость: %TURNS ходов + %POINTS очков, Останется: %REMAINING очков",
-	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Очки движения - Стоимость: %POINTS очков, Останется: %REMAINING очков",
 
 	"vcmi.capitalColors.0" : "Красный",
 	"vcmi.capitalColors.1" : "Синий",

+ 0 - 2
Mods/vcmi/Content/config/spanish.json

@@ -18,8 +18,6 @@
 	"vcmi.adventureMap.noTownWithTavern"       : "¡No hay pueblo disponible con taberna!",
 	"vcmi.adventureMap.spellUnknownProblem"    : "Problema desconocido con este hechizo, no hay más información disponible.",
 	"vcmi.adventureMap.playerAttacked"         : "El jugador ha sido atacado: %s",
-	"vcmi.adventureMap.moveCostDetails"        : "Puntos de movimiento - Coste: %TURNS turnos + %POINTS puntos, Puntos restantes: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Puntos de movimiento - Coste: %POINTS puntos, Puntos restantes: %REMAINING",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Disculpe, la repetición del turno del oponente aún no está implementada.",
 
 	"vcmi.capitalColors.0" : "Rojo",

+ 39 - 11
Mods/vcmi/Content/config/ukrainian.json

@@ -23,8 +23,8 @@
 	"vcmi.adventureMap.noTownWithTavern"    : "Немає доступного міста з таверною!",
 	"vcmi.adventureMap.spellUnknownProblem" : "Невідома проблема з цим заклинанням, більше інформації немає.",
 	"vcmi.adventureMap.playerAttacked"      : "Гравця атаковано: %s",
-	"vcmi.adventureMap.moveCostDetails" : "Очки руху - Вартість: %TURNS ходів + %POINTS очок. Залишок очок: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Очки руху - Вартість: %POINTS очок, Залишок очок: %REMAINING",
+	"vcmi.adventureMap.moveCostDetails"     : "Переміщення сюди коштуватиме {%TOTAL} очок загалом ({%TURNS} ходів і {%POINTS} очок). Після переміщення залишиться {%REMAINING} очок.",
+	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Переміщення сюди коштуватиме {%TOTAL} очок. Після переміщення залишиться {%REMAINING} очок.",
 	"vcmi.adventureMap.movementPointsHeroInfo" : "(Очки руху: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Вибачте, функція повтору ходу суперника ще не реалізована!",
 
@@ -68,6 +68,7 @@
 	"vcmi.radialWheel.heroGetArtifacts" : "Отримати артефакти іншого героя",
 	"vcmi.radialWheel.heroSwapArtifacts" : "Обміняти артефакти героїв",
 	"vcmi.radialWheel.heroDismiss" : "Звільнити цього героя",
+	"vcmi.radialWheel.upgradeCreatures" : "Покращити усіх істот",
 
 	"vcmi.radialWheel.moveTop" : "Перемістити на початок",
 	"vcmi.radialWheel.moveUp" : "Перемістити вгору",
@@ -363,6 +364,8 @@
 	"vcmi.battleOptions.endWithAutocombat.help": "{Завершує бій}\n\nАвто-бій миттєво завершує бій",
 	"vcmi.battleOptions.showQuickSpell.hover": "Панель швидкого чарування",
 	"vcmi.battleOptions.showQuickSpell.help": "{Панель швидкого чарування}\n\nПоказати панель для швидкого вибору заклять.",
+	"vcmi.battleOptions.showHealthBar.hover": "Показувати шкалу здоров'я",
+	"vcmi.battleOptions.showHealthBar.help": "{Показувати шкалу здоров'я}\n\nПоказувати шкалу здоров'я, яка вказує на решту здоров'я до смерті однієї істоти.",
 
 	"vcmi.adventureMap.revisitObject.hover" : "Відвідати Об'єкт",
 	"vcmi.adventureMap.revisitObject.help" : "{Відвідати Об'єкт}\n\nЯкщо герой в даний момент стоїть на об'єкті мапи, він може знову відвідати цю локацію.",
@@ -415,6 +418,9 @@
 	"vcmi.townStructure.bank.borrow" : "Ви заходите в банк. Вас бачить банкір і каже: 'Ми зробили для вас спеціальну пропозицію. Ви можете взяти у нас позику в розмірі 2500 золотих на 5 днів. Але щодня ви повинні будете повертати по 500 золотих'.",
 	"vcmi.townStructure.bank.payBack" : "Ви заходите в банк. Банкір бачить вас і каже: 'Ви вже отримали позику. Погасіть її, перш ніж брати нову позику'.",
 
+	"vcmi.townWindow.upgradeAll.notAllUpgradable" : "Недостатньо коштів, щоб покращити всіх істот. Чи бажаєте ви покращити наступних істот?",
+	"vcmi.townWindow.upgradeAll.notUpgradable" : "Недостатньо коштів, щоб покращити будь-яку істоту.",
+
 	"vcmi.logicalExpressions.anyOf"  : "Будь-що з перерахованого:",
 	"vcmi.logicalExpressions.allOf"  : "Все з перерахованого:",
 	"vcmi.logicalExpressions.noneOf" : "Нічого з перерахованого:",
@@ -423,12 +429,12 @@
 	"vcmi.heroWindow.openCommander.help"  : "Показує інформацію про командира героя",
 	"vcmi.heroWindow.openBackpack.hover" : "Відкрити вікно рюкзака з артефактами",
 	"vcmi.heroWindow.openBackpack.help"  : "Відкриває вікно, що дозволяє легше керувати рюкзаком артефактів",
-	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Сортувати за вартістю",
-	"vcmi.heroWindow.sortBackpackByCost.help"  : "Сортувати артефакти в рюкзаку за вартістю.",
-	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Сортувати за типом",
-	"vcmi.heroWindow.sortBackpackBySlot.help"  : "Сортувати артефакти в рюкзаку за слотом, в який цей артефакт може бути екіпірований",
-	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Сортування за рідкістю",
-	"vcmi.heroWindow.sortBackpackByClass.help"  : "Сортувати артефакти в рюкзаку за класом рідкісності артефакту. Скарб, Малий, Великий, Реліквія",
+	"vcmi.heroWindow.sortBackpackByCost.hover"  : "За вартістю",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "{Сортування за вартістю}\n\nСортувати артефакти в рюкзаку за вартістю.",
+	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "За слотом",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "{Сортування за слотом}\n\nСортувати артефакти в рюкзаку за слотом, в який цей артефакт може бути екіпірований",
+	"vcmi.heroWindow.sortBackpackByClass.hover"  : "За рідкістю",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "{Сортування за рідкістю}\n\nСортувати артефакти в рюкзаку за класом рідкісності артефакту. Скарб, Малий, Великий, Реліквія",
 	"vcmi.heroWindow.fusingArtifact.fusing" : "Ви володієте всіма компонентами, необхідними для злиття %s. Ви бажаєте виконати злиття? {Всі компоненти буде спожито під час злиття.}",
 
 	"vcmi.tavernWindow.inviteHero"  : "Запросити героя",
@@ -464,7 +470,7 @@
 	"vcmi.optionsTab.chessFieldBattle.help" : "Використовується у боях з ШІ чи у боях з гравцями якщо {таймер загону} вичерпується. Встановлюється на початку кожного бою.",
 	"vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Використовується при обираннія дії загону у боях з гравцями. Встановлюється на початку дії. Залишок додається до {таймеру битви}",
 	"vcmi.optionsTab.chessFieldUnitDiscard.help" : "Використовується при обираннія дії загону у боях з гравцями. Встановлюється на початку дії. Залишок часу буде втрачено.",
-	
+
 	"vcmi.optionsTab.accumulate" : "Накопичувати",
 
 	"vcmi.optionsTab.simturnsTitle" : "Одночасні ходи",
@@ -740,7 +746,7 @@
 	"core.bonus.TRANSMUTATION.name" : "Трансмутація",
 	"core.bonus.TRANSMUTATION.description" : "${val}% шанс перетворити атакованого юніта в інший тип",
 	"core.bonus.UNDEAD.name" : "Нежить",
-	"core.bonus.UNDEAD.description" : "Істота є нежить",
+	"core.bonus.UNDEAD.description" : "Істота є нежиттю",
 	"core.bonus.UNLIMITED_RETALIATIONS.name" : "Необмежена кількість ударів у відповідь",
 	"core.bonus.UNLIMITED_RETALIATIONS.description" : "Відбиває будь-яку кількість атак",
 	"core.bonus.WATER_IMMUNITY.name" : "Імунітет до води",
@@ -782,5 +788,27 @@
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.water": "На цей загін не діють жодні закляття школи Води",
 	"core.bonus.SPELL_SCHOOL_IMMUNITY.description.earth": "На цей загін не діють жодні закляття школи Землі",
 	"core.bonus.REVENGE.description" : "Завдає додаткової шкоди залежно від втраченого здоров'я в бою",
-	"core.bonus.REVENGE.name" : "Помста"
+	"core.bonus.REVENGE.name" : "Помста",
+	
+	"spell.core.castleMoat.name" : "Рів",
+	"spell.core.castleMoatTrigger.name" : "Рів",
+	"spell.core.catapultShot.name" : "Постріл з катапульти",
+	"spell.core.cyclopsShot.name" : "Постріл по стінам",
+	"spell.core.dungeonMoat.name" : "Кипляча нафта",
+	"spell.core.dungeonMoatTrigger.name" : "Кипляча нафта",
+	"spell.core.fireWallTrigger.name" : "Вогняна стіна",
+	"spell.core.firstAid.name" : "Перша допомога",
+	"spell.core.fortressMoat.name" : "Киплячий дьоготь",
+	"spell.core.fortressMoatTrigger.name" : "Киплячий дьоготь",
+	"spell.core.infernoMoat.name" : "Лава",
+	"spell.core.infernoMoatTrigger.name" : "Лава",
+	"spell.core.landMineTrigger.name" : "Наземна міна",
+	"spell.core.necropolisMoat.name" : "Могильник",
+	"spell.core.necropolisMoatTrigger.name" : "Могильник",
+	"spell.core.rampartMoat.name" : "Чагарник",
+	"spell.core.rampartMoatTrigger.name" : "Чагарник",
+	"spell.core.strongholdMoat.name" : "Дерев'яні піки",
+	"spell.core.strongholdMoatTrigger.name" : "Дерев'яні піки",
+	"spell.core.summonDemons.name" : "Виклик демонів",
+	"spell.core.towerMoat.name" : "Наземна міна"
 }

+ 3 - 5
Mods/vcmi/Content/config/vietnamese.json

@@ -23,8 +23,6 @@
 	"vcmi.adventureMap.noTownWithTavern": "Thành không có sẵn quán rượu!",
 	"vcmi.adventureMap.spellUnknownProblem": "Phép này có lỗi! Không có thông tin nào khác.",
 	"vcmi.adventureMap.playerAttacked": "Người chơi bị tấn công: %s",
-	"vcmi.adventureMap.moveCostDetails": "Điểm di chuyển - Cần: %TURNS lượt + %POINTS điểm, Còn lại: %REMAINING",
-	"vcmi.adventureMap.moveCostDetailsNoTurns": "Điểm di chuyển - Cần: %POINTS điểm, Còn lại: %REMAINING",
 	"vcmi.adventureMap.movementPointsHeroInfo": "(Điểm di chuyển: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented": "Xin lỗi, lượt chơi lại của đối thủ vẫn chưa được triển khai!",
 
@@ -423,11 +421,11 @@
 	"vcmi.heroWindow.openBackpack.hover" : "Mở cửa sổ ba lô báu vật",
 	"vcmi.heroWindow.openBackpack.help"  : "Mở cửa sổ để quản lý ba lô báu vật dễ dàng hơn.",
 	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Sắp xếp theo giá",
-	"vcmi.heroWindow.sortBackpackByCost.help"  : "Sắp xếp các báu vật trong ba lô theo giá.",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "{Sắp xếp theo giá}\n\nSắp xếp các báu vật trong ba lô theo giá.",
 	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Sắp xếp theo vị trí",
-	"vcmi.heroWindow.sortBackpackBySlot.help"  : "Sắp xếp báu vật trong ba lô theo ô được trang bị.",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "{Sắp xếp theo vị trí}\n\nSắp xếp báu vật trong ba lô theo ô được trang bị.",
 	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Sắp xếp theo loại",
-	"vcmi.heroWindow.sortBackpackByClass.help"  : "Sắp xếp các báu vật trong ba lô theo loại: Chính, Phụ, Cổ đại và Quý hiếm.",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "{Sắp xếp theo loại}\n\nSắp xếp các báu vật trong ba lô theo loại: Chính, Phụ, Cổ đại và Quý hiếm.",
 	"vcmi.heroWindow.fusingArtifact.fusing" : "Bạn đã sở hữu tất cả các món đồ cần thiết để hợp nhất %s. Bạn có muốn hợp nhất không? {Tất cả các món đồ sẽ được sử dụng khi hợp nhất.}",
 
 	"vcmi.tavernWindow.inviteHero"  : "Mới thêm tướng",

+ 11 - 0
Mods/vcmi/mod.json

@@ -52,6 +52,17 @@
 		]
 	},
 
+	"italian" : {
+    "name" : "VCMI - File di base",
+    "description" : "File di base necessari per il corretto funzionamento di VCMI",
+    "author" : "Team VCMI",
+
+    "skipValidation" : true,
+    "translations" : [
+        "config/italian.json"
+    ]
+},
+
 	"polish" : {
 		"name" : "Podstawowe pliki VCMI",
 		"description" : "Dodatkowe pliki wymagane do prawidłowego działania VCMI",

+ 1 - 1
android/AndroidManifest.xml

@@ -81,7 +81,7 @@
             android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density"
             android:label="@string/app_name"
             android:launchMode="singleTop"
-            android:screenOrientation="sensorLandscape" />
+            android:screenOrientation="fullSensor" />
 
         <service
             android:name=".ServerService"

+ 2 - 0
android/vcmi-app/src/main/java/eu/vcmi/vcmi/VcmiSDLActivity.java

@@ -107,6 +107,8 @@ public class VcmiSDLActivity extends SDLActivity
         mLayout = layout;
 
         setContentView(outerLayout);
+
+        VcmiSDLActivity.this.setWindowStyle(true); // set fullscreen
     }
 
     @Override

+ 4 - 0
client/CMakeLists.txt

@@ -493,5 +493,9 @@ if (ffmpeg_INCLUDE_DIRS)
 	)
 endif()
 
+if(VCMI_PORTMASTER)
+	target_compile_definitions(vcmiclientcommon PRIVATE VCMI_PORTMASTER)
+endif()
+
 vcmi_set_output_dir(vcmiclientcommon "")
 enable_pch(vcmiclientcommon)

+ 2 - 0
client/CPlayerInterface.cpp

@@ -491,6 +491,7 @@ void CPlayerInterface::heroSecondarySkillChanged(const CGHeroInstance * hero, in
 		cuw->updateSecondarySkills();
 
 	localState->verifyPath(hero);
+	adventureInt->onHeroChanged(hero);// secondary skill can change primary skill / mana limit
 }
 
 void CPlayerInterface::heroManaPointsChanged(const CGHeroInstance * hero)
@@ -505,6 +506,7 @@ void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero)
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	if (makingTurn && hero->tempOwner == playerID)
 		adventureInt->onHeroChanged(hero);
+	invalidatePaths();
 }
 void CPlayerInterface::receivedResource()
 {

+ 9 - 5
client/CServerHandler.cpp

@@ -186,9 +186,9 @@ void CServerHandler::startLocalServerAndConnect(bool connectToLobby)
 	si->difficulty = lastDifficulty.Integer();
 
 	logNetwork->trace("\tStarting local server");
-	uint16_t srvport = serverRunner->start(getLocalPort(), connectToLobby, si);
+	serverRunner->start(loadMode == ELoadMode::MULTI, connectToLobby, si);
 	logNetwork->trace("\tConnecting to local server");
-	connectToServer(getLocalHostname(), srvport);
+	connectToServer(getLocalHostname(), getLocalPort());
 	logNetwork->trace("\tWaiting for connection");
 }
 
@@ -206,9 +206,13 @@ void CServerHandler::connectToServer(const std::string & addr, const ui16 port)
 
 		Settings remotePort = settings.write["server"]["remotePort"];
 		remotePort->Integer() = port;
-	}
 
-	networkHandler->connectToRemote(*this, addr, port);
+		networkHandler->connectToRemote(*this, addr, port);
+	}
+	else
+	{
+		serverRunner->connect(*networkHandler, *this);
+	}
 }
 
 void CServerHandler::onConnectionFailed(const std::string & errorMessage)
@@ -245,7 +249,7 @@ void CServerHandler::onTimer()
 	}
 
 	assert(isServerLocal());
-	networkHandler->connectToRemote(*this, getLocalHostname(), getLocalPort());
+	serverRunner->connect(*networkHandler, *this);
 }
 
 void CServerHandler::onConnectionEstablished(const NetworkConnectionPtr & netConnection)

+ 23 - 10
client/ServerRunner.cpp

@@ -13,6 +13,8 @@
 
 #include "../lib/VCMIDirs.h"
 #include "../lib/CThreadHelper.h"
+#include "../lib/network/NetworkInterface.h"
+#include "../lib/CConfigHandler.h"
 #include "../server/CVCMIServer.h"
 
 #ifdef ENABLE_SERVER_PROCESS
@@ -33,10 +35,11 @@
 ServerThreadRunner::ServerThreadRunner() = default;
 ServerThreadRunner::~ServerThreadRunner() = default;
 
-uint16_t ServerThreadRunner::start(uint16_t cfgport, bool connectToLobby, std::shared_ptr<StartInfo> startingInfo)
+void ServerThreadRunner::start(bool listenForConnections, bool connectToLobby, std::shared_ptr<StartInfo> startingInfo)
 {
 	// cfgport may be 0 -- the real port is returned after calling prepare()
-	server = std::make_unique<CVCMIServer>(cfgport, true);
+	uint16_t port = settings["server"]["localPort"].Integer();
+	server = std::make_unique<CVCMIServer>(port, true);
 
 	if (startingInfo)
 	{
@@ -45,18 +48,16 @@ uint16_t ServerThreadRunner::start(uint16_t cfgport, bool connectToLobby, std::s
 
 	std::promise<uint16_t> promise;
 
-	threadRunLocalServer = boost::thread([this, connectToLobby, &promise]{
+	threadRunLocalServer = boost::thread([this, connectToLobby, listenForConnections, &promise]{
 		setThreadName("runServer");
-		uint16_t port = server->prepare(connectToLobby);
+		uint16_t port = server->prepare(connectToLobby, listenForConnections);
 		promise.set_value(port);
 		server->run();
 	});
 
 	logNetwork->trace("Waiting for server port...");
-	auto srvport = promise.get_future().get();
-	logNetwork->debug("Server port: %d", srvport);
-
-	return srvport;
+	serverPort = promise.get_future().get();
+	logNetwork->debug("Server port: %d", serverPort);
 }
 
 void ServerThreadRunner::shutdown()
@@ -74,6 +75,11 @@ int ServerThreadRunner::exitCode()
 	return 0;
 }
 
+void ServerThreadRunner::connect(INetworkHandler & network, INetworkClientListener & listener)
+{
+	network.createInternalConnection(listener, server->getNetworkServer());
+}
+
 #ifdef ENABLE_SERVER_PROCESS
 
 ServerProcessRunner::ServerProcessRunner() = default;
@@ -94,8 +100,9 @@ int ServerProcessRunner::exitCode()
 	return child->exit_code();
 }
 
-uint16_t ServerProcessRunner::start(uint16_t port, bool connectToLobby, std::shared_ptr<StartInfo> startingInfo)
+void ServerProcessRunner::start(bool listenForConnections, bool connectToLobby, std::shared_ptr<StartInfo> startingInfo)
 {
+	uint16_t port = settings["server"]["localPort"].Integer();
 	boost::filesystem::path serverPath = VCMIDirs::get().serverPath();
 	boost::filesystem::path logPath = VCMIDirs::get().userLogsPath() / "server_log.txt";
 	std::vector<std::string> args;
@@ -109,8 +116,14 @@ uint16_t ServerProcessRunner::start(uint16_t port, bool connectToLobby, std::sha
 
 	if (ec)
 		throw std::runtime_error("Failed to start server! Reason: " + ec.message());
+}
+
+void ServerProcessRunner::connect(INetworkHandler & network, INetworkClientListener & listener)
+{
+	std::string host = settings["server"]["localHostname"].String();
+	uint16_t port = settings["server"]["localPort"].Integer();
 
-	return port;
+	network.connectToRemote(listener, host, port);
 }
 
 #endif

+ 15 - 5
client/ServerRunner.h

@@ -12,6 +12,8 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 struct StartInfo;
+class INetworkHandler;
+class INetworkClientListener;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -20,25 +22,31 @@ class CVCMIServer;
 class IServerRunner
 {
 public:
-	virtual uint16_t start(uint16_t port, bool connectToLobby, std::shared_ptr<StartInfo> startingInfo) = 0;
+	virtual void start(bool listenForConnections, bool connectToLobby, std::shared_ptr<StartInfo> startingInfo) = 0;
 	virtual void shutdown() = 0;
 	virtual void wait() = 0;
 	virtual int exitCode() = 0;
 
+	virtual void connect(INetworkHandler & network, INetworkClientListener & listener) = 0;
+
 	virtual ~IServerRunner() = default;
 };
 
 /// Class that runs server instance as a thread of client process
-class ServerThreadRunner : public IServerRunner, boost::noncopyable
+class ServerThreadRunner final : public IServerRunner, boost::noncopyable
 {
 	std::unique_ptr<CVCMIServer> server;
 	boost::thread threadRunLocalServer;
+	uint16_t serverPort = 0;
+
 public:
-	uint16_t start(uint16_t port, bool connectToLobby, std::shared_ptr<StartInfo> startingInfo) override;
+	void start(bool listenForConnections, bool connectToLobby, std::shared_ptr<StartInfo> startingInfo) override;
 	void shutdown() override;
 	void wait() override;
 	int exitCode() override;
 
+	void connect(INetworkHandler & network, INetworkClientListener & listener) override;
+
 	ServerThreadRunner();
 	~ServerThreadRunner();
 };
@@ -64,16 +72,18 @@ class child;
 
 /// Class that runs server instance as a child process
 /// Available only on desktop systems where process management is allowed
-class ServerProcessRunner : public IServerRunner, boost::noncopyable
+class ServerProcessRunner final : public IServerRunner, boost::noncopyable
 {
 	std::unique_ptr<boost::process::child> child;
 
 public:
-	uint16_t start(uint16_t port, bool connectToLobby, std::shared_ptr<StartInfo> startingInfo) override;
+	void start(bool listenForConnections, bool connectToLobby, std::shared_ptr<StartInfo> startingInfo) override;
 	void shutdown() override;
 	void wait() override;
 	int exitCode() override;
 
+	void connect(INetworkHandler & network, INetworkClientListener & listener) override;
+
 	ServerProcessRunner();
 	~ServerProcessRunner();
 };

+ 51 - 39
client/adventureMap/AdventureMapInterface.cpp

@@ -46,6 +46,7 @@
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/mapping/CMapDefines.h"
 #include "../../lib/pathfinder/CGPathNode.h"
+#include "../../lib/pathfinder/TurnInfo.h"
 #include "../../lib/spells/ISpellMechanics.h"
 #include "../../lib/spells/Problem.h"
 
@@ -527,7 +528,6 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
 	bool canSelect = topBlocking && topBlocking->ID == Obj::HERO && topBlocking->tempOwner == LOCPLINT->playerID;
 	canSelect |= topBlocking && topBlocking->ID == Obj::TOWN && LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, topBlocking->tempOwner) != PlayerRelations::ENEMIES;
 
-	bool isHero = false;
 	if(LOCPLINT->localState->getCurrentArmy()->ID != Obj::HERO) //hero is not selected (presumably town)
 	{
 		if(LOCPLINT->localState->getCurrentArmy() == topBlocking) //selected town clicked
@@ -537,9 +537,10 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
 	}
 	else if(const CGHeroInstance * currentHero = LOCPLINT->localState->getCurrentHero()) //hero is selected
 	{
-		isHero = true;
-
 		const CGPathNode *pn = LOCPLINT->getPathsInfo(currentHero)->getPathInfo(targetPosition);
+
+		const auto shipyard = dynamic_cast<const IShipyard *>(topBlocking);
+
 		if(currentHero == topBlocking) //clicked selected hero
 		{
 			LOCPLINT->openHeroWindow(currentHero);
@@ -550,10 +551,19 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
 			LOCPLINT->localState->setSelection(static_cast<const CArmedInstance*>(topBlocking));
 			return;
 		}
+		else if(shipyard != nullptr && pn->turns == 255 && LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, topBlocking->tempOwner) != PlayerRelations::ENEMIES)
+		{
+			LOCPLINT->showShipyardDialogOrProblemPopup(shipyard);
+		}
 		else //still here? we need to move hero if we clicked end of already selected path or calculate a new path otherwise
 		{
+			int3 destinationTile = targetPosition;
+
+			if(topBlocking && topBlocking->isVisitable() && !topBlocking->visitableAt(destinationTile) && settings["gameTweaks"]["simpleObjectSelection"].Bool())
+				destinationTile = topBlocking->visitablePos();
+
 			if(LOCPLINT->localState->hasPath(currentHero) &&
-			   LOCPLINT->localState->getPath(currentHero).endPos() == targetPosition &&
+			   LOCPLINT->localState->getPath(currentHero).endPos() == destinationTile &&
 			   !GH.isKeyboardShiftDown())//we'll be moving
 			{
 				assert(!CGI->mh->hasOngoingAnimations());
@@ -570,7 +580,7 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
 				}
 				else //remove old path and find a new one if we clicked on accessible tile
 				{
-					LOCPLINT->localState->setPath(currentHero, targetPosition);
+					LOCPLINT->localState->setPath(currentHero, destinationTile);
 					onHeroChanged(currentHero);
 				}
 			}
@@ -580,12 +590,6 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
 	{
 		throw std::runtime_error("Nothing is selected...");
 	}
-
-	const auto shipyard = ourInaccessibleShipyard(topBlocking);
-	if(isHero && shipyard != nullptr)
-	{
-		LOCPLINT->showShipyardDialogOrProblemPopup(shipyard);
-	}
 }
 
 void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
@@ -686,6 +690,28 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
 			showMoveDetailsInStatusbar(*hero, *pathNode);
 		}
 
+		if (objAtTile && pathNode->action == EPathNodeAction::UNKNOWN)
+		{
+			if(objAtTile->ID == Obj::TOWN && objRelations != PlayerRelations::ENEMIES)
+			{
+				CCS->curh->set(Cursor::Map::TOWN);
+				return;
+			}
+			else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER)
+			{
+				CCS->curh->set(Cursor::Map::HERO);
+				return;
+			}
+			else if (objAtTile->ID == Obj::SHIPYARD && objRelations != PlayerRelations::ENEMIES)
+			{
+				CCS->curh->set(Cursor::Map::T1_SAIL);
+				return;
+			}
+
+			if(objAtTile->isVisitable() && !objAtTile->visitableAt(targetPosition) && settings["gameTweaks"]["simpleObjectSelection"].Bool())
+				pathNode = LOCPLINT->getPathsInfo(hero)->getPathInfo(objAtTile->visitablePos());
+		}
+
 		int turns = pathNode->turns;
 		vstd::amin(turns, 3);
 		switch(pathNode->action)
@@ -737,38 +763,36 @@ void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
 			break;
 
 		default:
-			if(objAtTile && objRelations != PlayerRelations::ENEMIES)
-			{
-				if(objAtTile->ID == Obj::TOWN)
-					CCS->curh->set(Cursor::Map::TOWN);
-				else if(objAtTile->ID == Obj::HERO && objRelations == PlayerRelations::SAME_PLAYER)
-					CCS->curh->set(Cursor::Map::HERO);
-				else
-					CCS->curh->set(Cursor::Map::POINTER);
-			}
-			else
 				CCS->curh->set(Cursor::Map::POINTER);
 			break;
 		}
 	}
-
-	if(ourInaccessibleShipyard(objAtTile))
-	{
-		CCS->curh->set(Cursor::Map::T1_SAIL);
-	}
 }
 
 void AdventureMapInterface::showMoveDetailsInStatusbar(const CGHeroInstance & hero, const CGPathNode & pathNode)
 {
 	const int maxMovementPointsAtStartOfLastTurn = pathNode.turns > 0 ? hero.movementPointsLimit(pathNode.layer == EPathfindingLayer::LAND) : hero.movementPointsRemaining();
 	const int movementPointsLastTurnCost = maxMovementPointsAtStartOfLastTurn - pathNode.moveRemains;
-	const int remainingPointsAfterMove = pathNode.turns == 0 ? pathNode.moveRemains : 0;
+	const int remainingPointsAfterMove = pathNode.moveRemains;
+
+	int totalMovementCost = 0;
+	for (int i = 0; i <= pathNode.turns; ++i)
+	{
+		auto turnInfo = hero.getTurnInfo(i);
+		if (pathNode.layer == EPathfindingLayer::SAIL)
+			totalMovementCost += turnInfo->getMovePointsLimitWater();
+		else
+			totalMovementCost += turnInfo->getMovePointsLimitLand();
+	}
+
+	totalMovementCost -= pathNode.moveRemains;
 
 	std::string result = VLC->generaltexth->translate("vcmi.adventureMap", pathNode.turns > 0 ? "moveCostDetails" : "moveCostDetailsNoTurns");
 
 	boost::replace_first(result, "%TURNS", std::to_string(pathNode.turns));
 	boost::replace_first(result, "%POINTS", std::to_string(movementPointsLastTurnCost));
 	boost::replace_first(result, "%REMAINING", std::to_string(remainingPointsAfterMove));
+	boost::replace_first(result, "%TOTAL", std::to_string(totalMovementCost));
 
 	GH.statusbar()->write(result);
 }
@@ -844,18 +868,6 @@ Rect AdventureMapInterface::terrainAreaPixels() const
 	return widget->getMapView()->pos;
 }
 
-const IShipyard * AdventureMapInterface::ourInaccessibleShipyard(const CGObjectInstance *obj) const
-{
-	const auto *ret = dynamic_cast<const IShipyard *>(obj);
-
-	if(!ret ||
-		obj->tempOwner != currentPlayerID ||
-		(CCS->curh->get<Cursor::Map>() != Cursor::Map::T1_SAIL && CCS->curh->get<Cursor::Map>() != Cursor::Map::POINTER))
-		return nullptr;
-
-	return ret;
-}
-
 void AdventureMapInterface::hotkeyExitWorldView()
 {
 	setState(EAdventureState::MAKING_TURN);

+ 0 - 3
client/adventureMap/AdventureMapInterface.h

@@ -75,9 +75,6 @@ private:
 	/// updates active state of game window whenever game state changes
 	void adjustActiveness();
 
-	/// checks if obj is our ashipyard and cursor is 0,0 -> returns shipyard or nullptr else
-	const IShipyard * ourInaccessibleShipyard(const CGObjectInstance *obj) const;
-
 	/// check and if necessary reacts on scrolling by moving cursor to screen edge
 	void handleMapScrollingUpdate(uint32_t msPassed);
 

+ 13 - 8
client/battle/BattleAnimationClasses.cpp

@@ -594,16 +594,19 @@ void ColorTransformAnimation::tick(uint32_t msPassed)
 	if (index == timePoints.size())
 	{
 		//end of animation. Apply ColorShifter using final values and die
-		const auto & shifter = steps[index - 1];
-		owner.stacksController->setStackColorFilter(shifter, stack, spell, false);
+		const auto & lastColor = effectColors[index - 1];
+		const auto & lastAlpha = transparency[index - 1];
+		owner.stacksController->setStackColorFilter(lastColor, lastAlpha, stack, spell, false);
 		delete this;
 		return;
 	}
 
 	assert(index != 0);
 
-	const auto & prevShifter = steps[index - 1];
-	const auto & nextShifter = steps[index];
+	const auto & prevColor = effectColors[index - 1];
+	const auto & nextColor = effectColors[index];
+	const auto & prevAlpha = transparency[index - 1];
+	const auto & nextAlpha = transparency[index];
 
 	float prevPoint = timePoints[index-1];
 	float nextPoint = timePoints[index];
@@ -611,9 +614,10 @@ void ColorTransformAnimation::tick(uint32_t msPassed)
 	float stepDuration = (nextPoint - prevPoint);
 	float factor = localProgress / stepDuration;
 
-	auto shifter = ColorFilter::genInterpolated(prevShifter, nextShifter, factor);
+	const auto & currColor = vstd::lerp(prevColor, nextColor, factor);
+	const auto & currAlpha = vstd::lerp(prevAlpha, nextAlpha, factor);
 
-	owner.stacksController->setStackColorFilter(shifter, stack, spell, true);
+	owner.stacksController->setStackColorFilter(currColor, currAlpha, stack, spell, true);
 }
 
 ColorTransformAnimation::ColorTransformAnimation(BattleInterface & owner, const CStack * _stack, const std::string & colorFilterName, const CSpell * spell):
@@ -622,10 +626,11 @@ ColorTransformAnimation::ColorTransformAnimation(BattleInterface & owner, const
 	totalProgress(0.f)
 {
 	auto effect = owner.effectsController->getMuxerEffect(colorFilterName);
-	steps = effect.filters;
+	effectColors = effect.effectColors;
+	transparency = effect.transparency;
 	timePoints = effect.timePoints;
 
-	assert(!steps.empty() && steps.size() == timePoints.size());
+	assert(!effectColors.empty() && effectColors.size() == timePoints.size());
 
 	logAnim->debug("Created ColorTransformAnimation for %s", stack->getName());
 }

+ 4 - 1
client/battle/BattleAnimationClasses.h

@@ -11,6 +11,7 @@
 
 #include "../../lib/battle/BattleHexArray.h"
 #include "../../lib/filesystem/ResourcePath.h"
+#include "../../lib/Color.h"
 #include "BattleConstants.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
@@ -113,8 +114,10 @@ public:
 
 class ColorTransformAnimation : public BattleStackAnimation
 {
-	std::vector<ColorFilter> steps;
+	std::vector<ColorRGBA> effectColors;
+	std::vector<float> transparency;
 	std::vector<float> timePoints;
+
 	const CSpell * spell;
 
 	float totalProgress;

+ 2 - 1
client/battle/BattleEffectsController.cpp

@@ -143,7 +143,8 @@ void BattleEffectsController::loadColorMuxers()
 		for (const JsonNode & entry : muxer.second.Vector() )
 		{
 			effect.timePoints.push_back(entry["time"].Float());
-			effect.filters.push_back(ColorFilter::genFromJson(entry));
+			effect.effectColors.push_back(ColorRGBA(255*entry["color"][0].Float(), 255*entry["color"][1].Float(), 255*entry["color"][2].Float(), 255*entry["color"][3].Float()));
+			effect.transparency.push_back(entry["alpha"].Float() * 255);
 		}
 		colorMuxerEffects[identifier] = effect;
 	}

+ 8 - 1
client/battle/BattleEffectsController.h

@@ -11,6 +11,7 @@
 
 #include "../../lib/battle/BattleHex.h"
 #include "../../lib/Point.h"
+#include "../../lib/Color.h"
 #include "../../lib/filesystem/ResourcePath.h"
 #include "BattleConstants.h"
 
@@ -21,13 +22,19 @@ struct BattleTriggerEffect;
 
 VCMI_LIB_NAMESPACE_END
 
-struct ColorMuxerEffect;
 class CAnimation;
 class Canvas;
 class BattleInterface;
 class BattleRenderer;
 class EffectAnimation;
 
+struct ColorMuxerEffect
+{
+	std::vector<ColorRGBA> effectColors;
+	std::vector<float> transparency;
+	std::vector<float> timePoints;
+};
+
 /// Struct for battle effect animation e.g. morale, prayer, armageddon, bless,...
 struct BattleEffect
 {

+ 1 - 1
client/battle/BattleFieldController.cpp

@@ -533,7 +533,7 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas)
 	BattleHexArray hoveredMoveHexes  = getHighlightedHexesForMovementTarget();
 
 	BattleHex hoveredHex = getHoveredHex();
-	BattleHexArray hoveredMouseHex = hoveredHex.isValid() ? BattleHexArray({ hoveredHex }) : BattleHexArray();
+	BattleHexArray hoveredMouseHex = hoveredHex.isAvailable() ? BattleHexArray({ hoveredHex }) : BattleHexArray();
 
 	const CStack * hoveredStack = getHoveredStack();
 	if(!hoveredStack && hoveredHex == BattleHex::INVALID)

+ 3 - 3
client/battle/BattleInterfaceClasses.cpp

@@ -636,7 +636,7 @@ void StackInfoBasicPanel::initializeData(const CStack * stack)
 	auto attack = std::to_string(CGI->creatures()->getByIndex(stack->creatureIndex())->getAttack(stack->isShooter())) + "(" + std::to_string(stack->getAttack(stack->isShooter())) + ")";
 	auto defense = std::to_string(CGI->creatures()->getByIndex(stack->creatureIndex())->getDefense(stack->isShooter())) + "(" + std::to_string(stack->getDefense(stack->isShooter())) + ")";
 	auto damage = std::to_string(CGI->creatures()->getByIndex(stack->creatureIndex())->getMinDamage(stack->isShooter())) + "-" + std::to_string(stack->getMaxDamage(stack->isShooter()));
-	auto health = CGI->creatures()->getByIndex(stack->creatureIndex())->getMaxHealth();
+	auto health = stack->getMaxHealth();
 	auto morale = stack->moraleVal();
 	auto luck = stack->luckVal();
 
@@ -691,7 +691,7 @@ void StackInfoBasicPanel::initializeData(const CStack * stack)
 			if (spellBonuses->empty())
 				throw std::runtime_error("Failed to find effects for spell " + effect.toSpell()->getJsonKey());
 
-			int duration = spellBonuses->front()->duration;
+			int duration = spellBonuses->front()->turnsRemain;
 
 			icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed));
 			if(settings["general"]["enableUiEnhancements"].Bool())
@@ -890,7 +890,7 @@ BattleResultResources BattleResultWindow::getResources(const BattleResult & br)
 		if (ourHero)
 		{
 			resources.resultText.appendTextID("core.genrltxt.305");
-			resources.resultText.replaceTextID(ourHero->getNameTranslated());
+			resources.resultText.replaceTextID(ourHero->getNameTextID());
 			resources.resultText.replaceNumber(br.exp[weAreAttacker ? BattleSide::ATTACKER : BattleSide::DEFENDER]);
 		}
 	}

+ 23 - 9
client/battle/BattleStacksController.cpp

@@ -38,6 +38,7 @@
 #include "../../lib/battle/BattleAction.h"
 #include "../../lib/battle/BattleHex.h"
 #include "../../lib/texts/TextOperations.h"
+#include "../../lib/CConfigHandler.h"
 #include "../../lib/CRandomGenerator.h"
 #include "../../lib/CStack.h"
 
@@ -204,8 +205,7 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 	if (!instant)
 	{
 		// immediately make stack transparent, giving correct shifter time to start
-		auto shifterFade = ColorFilter::genAlphaShifter(0);
-		setStackColorFilter(shifterFade, stack, nullptr, true);
+		setStackColorFilter(Colors::TRANSPARENCY, 0, stack, nullptr, true);
 
 		owner.addToAnimationStage(EAnimationEvents::HIT, [=]()
 		{
@@ -318,20 +318,33 @@ void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack *
 
 	Point textPosition = Point(amountBG->dimensions().x/2 + boxPosition.x, boxPosition.y + amountBG->dimensions().y/2);
 
+	if(settings["battle"]["showHealthBar"].Bool())
+	{
+		float health = stack->getMaxHealth();
+		float healthRemaining = std::max(stack->getAvailableHealth() - (stack->getCount() - 1) * health, .0f);
+		Rect r(boxPosition.x, boxPosition.y - 3, amountBG->width(), 4);
+		canvas.drawColor(r, Colors::RED);
+		canvas.drawColor(Rect(r.x, r.y, (r.w / health) * healthRemaining, r.h), Colors::GREEN);
+		canvas.drawBorder(r, Colors::YELLOW);
+	}
 	canvas.draw(amountBG, boxPosition);
 	canvas.drawText(textPosition, EFonts::FONT_TINY, Colors::WHITE, ETextAlignment::CENTER, TextOperations::formatMetric(stack->getCount(), 4));
 }
 
 void BattleStacksController::showStack(Canvas & canvas, const CStack * stack)
 {
-	ColorFilter fullFilter = ColorFilter::genEmptyShifter();
+	ColorRGBA effectColor = Colors::TRANSPARENCY;
+	uint8_t transparency = 255;
 	for(const auto & filter : stackFilterEffects)
 	{
 		if (filter.target == stack)
-			fullFilter = ColorFilter::genCombined(fullFilter, filter.effect);
+		{
+			effectColor = filter.effectColor;
+			transparency = static_cast<int>(filter.transparency) * transparency / 255;
+		}
 	}
 
-	stackAnimation[stack->unitId()]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit
+	stackAnimation[stack->unitId()]->nextFrame(canvas, effectColor, transparency, facingRight(stack)); // do actual blit
 }
 
 void BattleStacksController::tick(uint32_t msPassed)
@@ -769,18 +782,19 @@ Point BattleStacksController::getStackPositionAtHex(const BattleHex & hexNum, co
 	return ret;
 }
 
-void BattleStacksController::setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell * source, bool persistent)
+void BattleStacksController::setStackColorFilter(const ColorRGBA & effectColor, uint8_t transparency, const CStack * target, const CSpell * source, bool persistent)
 {
 	for (auto & filter : stackFilterEffects)
 	{
 		if (filter.target == target && filter.source == source)
 		{
-			filter.effect     = effect;
+			filter.effectColor = effectColor;
+			filter.transparency	= transparency;
 			filter.persistent = persistent;
 			return;
 		}
 	}
-	stackFilterEffects.push_back({ effect, target, source, persistent });
+	stackFilterEffects.push_back({ target, source, effectColor, transparency, persistent });
 }
 
 void BattleStacksController::removeExpiredColorFilters()
@@ -791,7 +805,7 @@ void BattleStacksController::removeExpiredColorFilters()
 		{
 			if (filter.source && !filter.target->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(filter.source->id)), Selector::all))
 				return true;
-			if (filter.effect == ColorFilter::genEmptyShifter())
+			if (filter.effectColor == Colors::TRANSPARENCY && filter.transparency == 255)
 				return true;
 		}
 		return false;

+ 4 - 3
client/battle/BattleStacksController.h

@@ -9,7 +9,7 @@
  */
 #pragma once
 
-#include "../render/ColorFilter.h"
+#include "../../lib/Color.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -37,9 +37,10 @@ class IImage;
 
 struct BattleStackFilterEffect
 {
-	ColorFilter effect;
 	const CStack * target;
 	const CSpell * source;
+	ColorRGBA effectColor;
+	uint8_t transparency;
 	bool persistent;
 };
 
@@ -134,7 +135,7 @@ public:
 	/// Adds new color filter effect targeting stack
 	/// Effect will last as long as stack is affected by specified spell (unless effect is persistent)
 	/// If effect from same (target, source) already exists, it will be updated
-	void setStackColorFilter(const ColorFilter & effect, const CStack * target, const CSpell *source, bool persistent);
+	void setStackColorFilter(const ColorRGBA & effect, uint8_t transparency, const CStack * target, const CSpell *source, bool persistent);
 	void addNewAnim(BattleAnimation *anim); //adds new anim to pendingAnims
 
 	const CStack* getActiveStack() const;

+ 12 - 13
client/battle/CreatureAnimation.cpp

@@ -24,11 +24,6 @@ static const ColorRGBA creatureBlueBorder = { 0, 255, 255, 255 };
 static const ColorRGBA creatureGoldBorder = { 255, 255, 0, 255 };
 static const ColorRGBA creatureNoBorder  =  { 0, 0, 0, 0 };
 
-static ColorRGBA genShadow(ui8 alpha)
-{
-	return ColorRGBA(0, 0, 0, alpha);
-}
-
 ColorRGBA AnimationControls::getBlueBorder()
 {
 	return creatureBlueBorder;
@@ -192,7 +187,6 @@ void CreatureAnimation::setType(ECreatureAnimType type)
 CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedController controller)
 	: name(name_),
 	  speed(0.1f),
-	  shadowAlpha(128),
 	  currentFrame(0),
 	  animationEnd(-1),
 	  elapsedTime(0),
@@ -200,8 +194,15 @@ CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedControll
 	  speedController(controller),
 	  once(false)
 {
-	forward = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_OVERLAY);
-	reverse = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_OVERLAY);
+
+	forward = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_SELECTION);
+	reverse = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_SELECTION);
+
+	if (forward->size(size_t(ECreatureAnimType::DEATH)) == 0)
+		throw std::runtime_error("Animation '" + name_.getOriginalName() + "' has empty death animation!");
+
+	if (forward->size(size_t(ECreatureAnimType::HOLDING)) == 0)
+		throw std::runtime_error("Animation '" + name_.getOriginalName() + "' has empty holding animation!");
 
 	// if necessary, add one frame into vcmi-only group DEAD
 	if(forward->size(size_t(ECreatureAnimType::DEAD)) == 0)
@@ -324,11 +325,8 @@ static ColorRGBA genBorderColor(ui8 alpha, const ColorRGBA & base)
 	return ColorRGBA(base.r, base.g, base.b, ui8(base.a * alpha / 256));
 }
 
-void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight)
+void CreatureAnimation::nextFrame(Canvas & canvas, const ColorRGBA & effectColor, uint8_t transparency, bool facingRight)
 {
-	ColorRGBA shadowTest = shifter.shiftColor(genShadow(128));
-	shadowAlpha = shadowTest.a;
-
 	size_t frame = static_cast<size_t>(floor(currentFrame));
 
 	std::shared_ptr<IImage> image;
@@ -345,7 +343,8 @@ void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter,
 		else
 			image->setOverlayColor(Colors::TRANSPARENCY);
 
-		image->adjustPalette(shifter, 0);
+		image->setEffectColor(effectColor);
+		image->setAlpha(transparency);
 
 		canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h));
 	}

+ 1 - 4
client/battle/CreatureAnimation.h

@@ -94,9 +94,6 @@ private:
 	///type of animation being displayed
 	ECreatureAnimType type;
 
-	/// current value of shadow transparency
-	uint8_t shadowAlpha;
-
 	/// border color, disabled if alpha = 0
 	ColorRGBA border;
 
@@ -127,7 +124,7 @@ public:
 	/// returns currently rendered type of animation
 	ECreatureAnimType getType() const;
 
-	void nextFrame(Canvas & canvas, const ColorFilter & shifter, bool facingRight);
+	void nextFrame(Canvas & canvas, const ColorRGBA & effectColor, uint8_t transparency, bool facingRight);
 
 	/// should be called every frame, return true when animation was reset to beginning
 	bool incrementFrame(float timePassed);

+ 15 - 0
client/eventsSDL/InputHandler.cpp

@@ -234,6 +234,14 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 				boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
 				GH.onScreenResize(false);
 			}
+#endif
+				break;
+			case SDL_WINDOWEVENT_SIZE_CHANGED:
+#ifdef VCMI_ANDROID
+			{
+				boost::mutex::scoped_lock interfaceLock(GH.interfaceMutex);
+				GH.onScreenResize(true);
+			}
 #endif
 				break;
 			case SDL_WINDOWEVENT_FOCUS_GAINED:
@@ -389,6 +397,13 @@ bool InputHandler::hasTouchInputDevice() const
 	return fingerHandler->hasTouchInputDevice();
 }
 
+int InputHandler::getNumTouchFingers() const
+{
+	if(currentInputMode != InputMode::TOUCH)
+		return 0;
+	return fingerHandler->getNumTouchFingers();
+}
+
 void InputHandler::dispatchMainThread(const std::function<void()> & functor)
 {
 	auto heapFunctor = new std::function<void()>(functor);

+ 3 - 0
client/eventsSDL/InputHandler.h

@@ -90,6 +90,9 @@ public:
 	/// returns true if system has active touchscreen
 	bool hasTouchInputDevice() const;
 
+	/// returns number of fingers on touchscreen
+	int getNumTouchFingers() const;
+
 	/// Calls provided functor in main thread on next execution frame
 	void dispatchMainThread(const std::function<void()> & functor);
 

+ 15 - 4
client/eventsSDL/InputSourceTouch.cpp

@@ -20,6 +20,7 @@
 #include "../gui/EventDispatcher.h"
 #include "../gui/MouseButton.h"
 #include "../gui/WindowHandler.h"
+#include "../render/IScreenHandler.h"
 #include "../CServerHandler.h"
 #include "../globalLobby/GlobalLobbyClient.h"
 
@@ -34,7 +35,7 @@
 #include <SDL_timer.h>
 
 InputSourceTouch::InputSourceTouch()
-	: lastTapTimeTicks(0), lastLeftClickTimeTicks(0)
+	: lastTapTimeTicks(0), lastLeftClickTimeTicks(0), numTouchFingers(0)
 {
 	params.useRelativeMode = settings["general"]["userRelativePointer"].Bool();
 	params.relativeModeSpeedFactor = settings["general"]["relativePointerSpeedMultiplier"].Float();
@@ -65,6 +66,7 @@ void InputSourceTouch::handleEventFingerMotion(const SDL_TouchFingerEvent & tfin
 		case TouchState::RELATIVE_MODE:
 		{
 			Point screenSize = GH.screenDimensions();
+			int scalingFactor = GH.screenHandler().getScalingFactor();
 
 			Point moveDistance {
 				static_cast<int>(screenSize.x * params.relativeModeSpeedFactor * tfinger.dx),
@@ -73,7 +75,7 @@ void InputSourceTouch::handleEventFingerMotion(const SDL_TouchFingerEvent & tfin
 
 			GH.input().moveCursorPosition(moveDistance);
 			if (CCS && CCS->curh)
-				CCS->curh->cursorMove(GH.getCursorPosition().x, GH.getCursorPosition().y);
+				CCS->curh->cursorMove(GH.getCursorPosition().x * scalingFactor, GH.getCursorPosition().y * scalingFactor);
 
 			break;
 		}
@@ -114,6 +116,8 @@ void InputSourceTouch::handleEventFingerMotion(const SDL_TouchFingerEvent & tfin
 
 void InputSourceTouch::handleEventFingerDown(const SDL_TouchFingerEvent & tfinger)
 {
+	numTouchFingers = SDL_GetNumTouchFingers(tfinger.touchId);
+
 	// FIXME: better place to update potentially changed settings?
 	params.longTouchTimeMilliseconds = settings["general"]["longTouchTimeMilliseconds"].Float();
 	params.hapticFeedbackEnabled = settings["general"]["hapticFeedback"].Bool();
@@ -172,6 +176,8 @@ void InputSourceTouch::handleEventFingerDown(const SDL_TouchFingerEvent & tfinge
 
 void InputSourceTouch::handleEventFingerUp(const SDL_TouchFingerEvent & tfinger)
 {
+	numTouchFingers = SDL_GetNumTouchFingers(tfinger.touchId);
+
 	switch(state)
 	{
 		case TouchState::RELATIVE_MODE:
@@ -280,6 +286,11 @@ bool InputSourceTouch::hasTouchInputDevice() const
 	return SDL_GetNumTouchDevices() > 0;
 }
 
+int InputSourceTouch::getNumTouchFingers() const
+{
+	return numTouchFingers;
+}
+
 void InputSourceTouch::emitPanningEvent(const SDL_TouchFingerEvent & tfinger)
 {
 	Point distance = convertTouchToMouse(-tfinger.dx, -tfinger.dy);
@@ -324,8 +335,8 @@ void InputSourceTouch::emitPinchEvent(const SDL_TouchFingerEvent & tfinger)
 	float newX = thisX - otherX;
 	float newY = thisY - otherY;
 
-	double distanceOld = std::sqrt(oldX * oldX + oldY + oldY);
-	double distanceNew = std::sqrt(newX * newX + newY + newY);
+	double distanceOld = std::sqrt(oldX * oldX + oldY * oldY);
+	double distanceNew = std::sqrt(newX * newX + newY * newY);
 
 	if (distanceOld > params.pinchSensitivityThreshold)
 		GH.events().dispatchGesturePinch(lastTapPosition, distanceNew / distanceOld);

+ 3 - 0
client/eventsSDL/InputSourceTouch.h

@@ -108,6 +108,7 @@ class InputSourceTouch
 
 	uint32_t lastLeftClickTimeTicks;
 	Point lastLeftClickPosition;
+	int numTouchFingers;
 
 	Point convertTouchToMouse(const SDL_TouchFingerEvent & current);
 	Point convertTouchToMouse(float x, float y);
@@ -127,4 +128,6 @@ public:
 	void handleUpdate();
 
 	bool hasTouchInputDevice() const;
+
+	int getNumTouchFingers() const;
 };

+ 10 - 0
client/globalLobby/GlobalLobbyWidget.cpp

@@ -28,6 +28,7 @@
 #include "../widgets/Images.h"
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/ObjectLists.h"
+#include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
 
 #include "../../lib/CConfigHandler.h"
@@ -126,6 +127,15 @@ std::shared_ptr<CIntObject> GlobalLobbyWidget::buildItemList(const JsonNode & co
 
 	auto result = std::make_shared<CListBox>(callback, position, itemOffset, visibleAmount, totalAmount, initialPos, sliderMode, Rect(sliderPosition, sliderSize));
 
+	if (result->getSlider())
+	{
+		Point scrollBoundsDimensions(sliderPosition.x + result->getSlider()->pos.w, result->getSlider()->pos.h);
+		Point scrollBoundsOffset = -sliderPosition;
+
+		result->getSlider()->setScrollBounds(Rect(scrollBoundsOffset, scrollBoundsDimensions));
+		result->getSlider()->setPanningStep(itemOffset.length());
+	}
+
 	result->setRedrawParent(true);
 	return result;
 }

+ 1 - 1
client/gui/CursorHandler.cpp

@@ -24,7 +24,7 @@
 
 std::unique_ptr<ICursor> CursorHandler::createCursor()
 {
-#if defined(VCMI_MOBILE)
+#if defined(VCMI_MOBILE) || defined(VCMI_PORTMASTER)
 	if (settings["general"]["userRelativePointer"].Bool())
 		return std::make_unique<CursorSoftware>();
 #endif

+ 1 - 0
client/gui/Shortcut.h

@@ -295,6 +295,7 @@ enum class EShortcut
 	// Spellbook screen
 	SPELLBOOK_TAB_ADVENTURE,
 	SPELLBOOK_TAB_COMBAT,
+	SPELLBOOK_SEARCH_FOCUS,
 
 	LIST_HERO_UP,
 	LIST_HERO_DOWN,

+ 1 - 0
client/gui/ShortcutHandler.cpp

@@ -277,6 +277,7 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"heroCostumeLoad9",         EShortcut::HERO_COSTUME_LOAD_9       },
 		{"spellbookTabAdventure",    EShortcut::SPELLBOOK_TAB_ADVENTURE   },
 		{"spellbookTabCombat",       EShortcut::SPELLBOOK_TAB_COMBAT      },
+		{"spellbookSearchFocus",     EShortcut::SPELLBOOK_SEARCH_FOCUS    },
 		{"listHeroUp",               EShortcut::LIST_HERO_UP              },
 		{"listHeroDown",             EShortcut::LIST_HERO_DOWN            },
 		{"listHeroTop",              EShortcut::LIST_HERO_TOP             },

+ 8 - 0
client/lobby/CBonusSelection.cpp

@@ -114,6 +114,7 @@ CBonusSelection::CBonusSelection()
 	for(size_t b = 0; b < difficultyIcons.size(); ++b)
 	{
 		difficultyIcons[b] = std::make_shared<CAnimImage>(AnimationPath::builtinTODO("GSPBUT" + std::to_string(b + 3) + ".DEF"), 0, 0, 709, settings["general"]["enableUiEnhancements"].Bool() ? 480 : 455);
+		difficultyIconAreas[b] = std::make_shared<LRClickableArea>(difficultyIcons[b]->pos - pos.topLeft(), nullptr, [b]() { CRClickPopup::createAndPush(CGI->generaltexth->zelp[24 + b].second); });
 	}
 
 	if(getCampaign()->playerSelectedDifficulty())
@@ -377,9 +378,16 @@ void CBonusSelection::updateAfterStateChange()
 	for(size_t i = 0; i < difficultyIcons.size(); i++)
 	{
 		if(i == CSH->si->difficulty)
+		{
 			difficultyIcons[i]->enable();
+			difficultyIconAreas[i]->enable();
+
+		}
 		else
+		{
 			difficultyIcons[i]->disable();
+			difficultyIconAreas[i]->disable();
+		}
 	}
 	flagbox->recreate();
 	createBonusesIcons();

+ 2 - 0
client/lobby/CBonusSelection.h

@@ -31,6 +31,7 @@ class ISelectionScreenInfo;
 class ExtraOptionsTab;
 class VideoWidgetOnce;
 class CBonusSelection;
+class LRClickableArea;
 
 
 /// Campaign screen where you can choose one out of three starting bonuses
@@ -93,6 +94,7 @@ public:
 	std::shared_ptr<CToggleGroup> groupBonuses;
 	std::shared_ptr<CLabel> labelDifficulty;
 	std::array<std::shared_ptr<CAnimImage>, 5> difficultyIcons;
+	std::array<std::shared_ptr<LRClickableArea>, 5> difficultyIconAreas;
 	std::shared_ptr<CButton> buttonDifficultyLeft;
 	std::shared_ptr<CButton> buttonDifficultyRight;
 	std::shared_ptr<CAnimImage> iconsMapSizes;

+ 1 - 1
client/lobby/SelectionTab.cpp

@@ -457,7 +457,7 @@ void SelectionTab::showPopupWindow(const Point & cursorPosition)
 		}
 
 		GH.windows().createAndPushWindow<CMapOverview>(
-			curItems[py]->getNameTranslated(),
+			curItems[py]->name,
 			curItems[py]->fullFileURI,
 			creationDateTime,
 			author,

+ 12 - 3
client/mainmenu/CHighScoreScreen.cpp

@@ -45,8 +45,6 @@ CHighScoreScreen::CHighScoreScreen(HighScorePage highscorepage, int highlighted)
 	OBJECT_CONSTRUCTION;
 	pos = center(Rect(0, 0, 800, 600));
 
-	backgroundAroundMenu = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y));
-
 	addHighScores();
 	addButtons();
 }
@@ -174,6 +172,12 @@ void CHighScoreScreen::buttonExitClick()
 	CMM->playMusic();
 }
 
+void CHighScoreScreen::showAll(Canvas & to)
+{
+	to.fillTexture(GH.renderHandler().loadImage(ImagePath::builtin("DiBoxBck"), EImageBlitMode::OPAQUE));
+	CWindowObject::showAll(to);
+}
+
 CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc, const StatisticDataSet & statistic)
 	: CWindowObject(BORDERED), won(won), calc(calc), stat(statistic)
 {
@@ -182,7 +186,6 @@ CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc
 	OBJECT_CONSTRUCTION;
 	pos = center(Rect(0, 0, 800, 600));
 
-	backgroundAroundMenu = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y));
 	background = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w, pos.h), Colors::BLACK);
 
 	if(won)
@@ -272,6 +275,12 @@ void CHighScoreInputScreen::show(Canvas & to)
 	CWindowObject::showAll(to);
 }
 
+void CHighScoreInputScreen::showAll(Canvas & to)
+{
+	to.fillTexture(GH.renderHandler().loadImage(ImagePath::builtin("DiBoxBck"), EImageBlitMode::OPAQUE));
+	CWindowObject::showAll(to);
+}
+
 void CHighScoreInputScreen::clickPressed(const Point & cursorPosition)
 {
 	if(statisticButton && statisticButton->pos.isInside(cursorPosition))

+ 2 - 2
client/mainmenu/CHighScoreScreen.h

@@ -39,11 +39,11 @@ private:
 	void buttonExitClick();
 
 	void showPopupWindow(const Point & cursorPosition) override;
+	void showAll(Canvas & to) override;
 
 	HighScorePage highscorepage;
 
 	std::shared_ptr<CPicture> background;
-	std::shared_ptr<CFilledTexture> backgroundAroundMenu;
 	std::vector<std::shared_ptr<CButton>> buttons;
 	std::vector<std::shared_ptr<CLabel>> texts;
 	std::vector<std::shared_ptr<CAnimImage>> images;
@@ -77,7 +77,6 @@ class CHighScoreInputScreen : public CWindowObject, public IVideoHolder
 	std::shared_ptr<CHighScoreInput> input;
 	std::shared_ptr<TransparentFilledRectangle> background;
 	std::shared_ptr<VideoWidgetBase> videoPlayer;
-	std::shared_ptr<CFilledTexture> backgroundAroundMenu;
 
 	std::shared_ptr<CButton> statisticButton;
 
@@ -95,4 +94,5 @@ public:
 	void clickPressed(const Point & cursorPosition) override;
 	void keyPressed(EShortcut key) override;
 	void show(Canvas & to) override;
+	void showAll(Canvas & to) override;
 };

+ 1 - 1
client/mapView/MapRenderer.cpp

@@ -407,7 +407,7 @@ std::shared_ptr<CAnimation> MapRendererObjects::getAnimation(const AnimationPath
 	if(it != animations.end())
 		return it->second;
 
-	auto ret = GH.renderHandler().loadAnimation(filename, enableOverlay ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::WITH_SHADOW);
+	auto ret = GH.renderHandler().loadAnimation(filename, enableOverlay ? EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR: EImageBlitMode::WITH_SHADOW);
 	animations[filename] = ret;
 
 	if(generateMovementGroups)

+ 3 - 0
client/mapView/MapRendererContext.cpp

@@ -278,6 +278,9 @@ std::string MapRendererAdventureContext::overlayText(const int3 & coordinates) c
 	if (!tile.visitable())
 		return {};
 
+	if ( tile.visitableObjects.back()->ID == Obj::EVENT)
+		return {};
+
 	return tile.visitableObjects.back()->getObjectName();
 }
 

+ 1 - 1
client/mapView/MapViewController.cpp

@@ -224,7 +224,7 @@ void MapViewController::updateState()
 		adventureContext->settingShowVisitable = settings["session"]["showVisitable"].Bool();
 		adventureContext->settingShowBlocked = settings["session"]["showBlocked"].Bool();
 		adventureContext->settingSpellRange = settings["session"]["showSpellRange"].Bool();
-		adventureContext->settingTextOverlay = GH.isKeyboardAltDown();
+		adventureContext->settingTextOverlay = GH.isKeyboardAltDown() || GH.input().getNumTouchFingers() == 2;
 	}
 }
 

+ 0 - 2
client/media/CMusicHandler.cpp

@@ -22,8 +22,6 @@
 #include "../../lib/TerrainHandler.h"
 #include "../../lib/filesystem/Filesystem.h"
 
-#include <SDL_mixer.h>
-
 void CMusicHandler::onVolumeChange(const JsonNode & volumeNode)
 {
 	setVolume(volumeNode.Integer());

+ 1 - 2
client/media/CMusicHandler.h

@@ -14,8 +14,7 @@
 
 #include "../lib/CConfigHandler.h"
 
-struct _Mix_Music;
-using Mix_Music = struct _Mix_Music;
+#include <SDL_mixer.h>
 
 class CMusicHandler;
 

+ 3 - 3
client/render/AssetGenerator.cpp

@@ -211,7 +211,7 @@ AssetGenerator::CanvasPtr AssetGenerator::createCampaignBackground()
 	auto locator = ImageLocator(ImagePath::builtin("CAMPBACK"), EImageBlitMode::OPAQUE);
 
 	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
-	auto image = GH.renderHandler().createImage(Point(200, 116), CanvasScalingPolicy::IGNORE);
+	auto image = GH.renderHandler().createImage(Point(800, 600), CanvasScalingPolicy::IGNORE);
 	Canvas canvas = image->getCanvas();
 
 	canvas.draw(img, Point(0, 0), Rect(0, 0, 800, 600));
@@ -247,11 +247,11 @@ AssetGenerator::CanvasPtr AssetGenerator::createCampaignBackground()
 
 AssetGenerator::CanvasPtr AssetGenerator::createChroniclesCampaignImages(int chronicle)
 {
-	auto imgPathBg = ImagePath::builtin("data/chronicles_" + std::to_string(chronicle) + "/GamSelBk");
+	auto imgPathBg = ImagePath::builtin("chronicles_" + std::to_string(chronicle) + "/GamSelBk");
 	auto locator = ImageLocator(imgPathBg, EImageBlitMode::OPAQUE);
 
 	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator);
-	auto image = GH.renderHandler().createImage(Point(800, 600), CanvasScalingPolicy::IGNORE);
+	auto image = GH.renderHandler().createImage(Point(200, 116), CanvasScalingPolicy::IGNORE);
 	Canvas canvas = image->getCanvas();
 
 	std::array sourceRect = {

+ 5 - 0
client/render/CanvasImage.cpp

@@ -25,6 +25,11 @@ CanvasImage::CanvasImage(const Point & size, CanvasScalingPolicy scalingPolicy)
 {
 }
 
+CanvasImage::~CanvasImage()
+{
+	SDL_FreeSurface(surface);
+}
+
 void CanvasImage::draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const
 {
 	if(src)

+ 2 - 0
client/render/CanvasImage.h

@@ -16,6 +16,7 @@ class CanvasImage : public IImage
 {
 public:
 	CanvasImage(const Point & size, CanvasScalingPolicy scalingPolicy);
+	~CanvasImage();
 
 	Canvas getCanvas();
 
@@ -31,6 +32,7 @@ public:
 	void setAlpha(uint8_t value) override{};
 	void playerColored(const PlayerColor & player) override{};
 	void setOverlayColor(const ColorRGBA & color) override{};
+	void setEffectColor(const ColorRGBA & color) override{};
 	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override{};
 	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override{};
 

+ 0 - 37
client/render/ColorFilter.cpp

@@ -129,40 +129,3 @@ ColorFilter ColorFilter::genCombined(const ColorFilter & left, const ColorFilter
 	float a = left.a * right.a;
 	return genMuxerShifter(r,g,b,a);
 }
-
-ColorFilter ColorFilter::genFromJson(const JsonNode & entry)
-{
-	ChannelMuxer r{ 1.f, 0.f, 0.f, 0.f };
-	ChannelMuxer g{ 0.f, 1.f, 0.f, 0.f };
-	ChannelMuxer b{ 0.f, 0.f, 1.f, 0.f };
-	float a{ 1.0};
-
-	if (!entry["red"].isNull())
-	{
-		r.r = entry["red"].Vector()[0].Float();
-		r.g = entry["red"].Vector()[1].Float();
-		r.b = entry["red"].Vector()[2].Float();
-		r.a = entry["red"].Vector()[3].Float();
-	}
-
-	if (!entry["green"].isNull())
-	{
-		g.r = entry["green"].Vector()[0].Float();
-		g.g = entry["green"].Vector()[1].Float();
-		g.b = entry["green"].Vector()[2].Float();
-		g.a = entry["green"].Vector()[3].Float();
-	}
-
-	if (!entry["blue"].isNull())
-	{
-		b.r = entry["blue"].Vector()[0].Float();
-		b.g = entry["blue"].Vector()[1].Float();
-		b.b = entry["blue"].Vector()[2].Float();
-		b.a = entry["blue"].Vector()[3].Float();
-	}
-
-	if (!entry["alpha"].isNull())
-		a = entry["alpha"].Float();
-
-	return genMuxerShifter(r,g,b,a);
-}

+ 0 - 9
client/render/ColorFilter.h

@@ -54,13 +54,4 @@ public:
 
 	/// Scales down strength of a shifter to a specified factor
 	static ColorFilter genInterpolated(const ColorFilter & left, const ColorFilter & right, float power);
-
-	/// Generates object using supplied Json config
-	static ColorFilter genFromJson(const JsonNode & entry);
-};
-
-struct ColorMuxerEffect
-{
-	std::vector<ColorFilter> filters;
-	std::vector<float> timePoints;
 };

+ 10 - 5
client/render/IImage.h

@@ -47,19 +47,24 @@ enum class EImageBlitMode : uint8_t
 	WITH_SHADOW,
 
 	/// RGBA, may consist from 3 separate parts: base, shadow, and overlay
-	WITH_SHADOW_AND_OVERLAY,
+	WITH_SHADOW_AND_SELECTION,
+	WITH_SHADOW_AND_FLAG_COLOR,
 
 	/// RGBA, contains only body, with shadow and overlay disabled
-	ONLY_BODY,
+	GRAYSCALE_BODY_HIDE_SELECTION,
+	ONLY_BODY_HIDE_SELECTION,
+	ONLY_BODY_HIDE_FLAG_COLOR,
 
 	/// RGBA, contains only body, with shadow disabled and overlay treated as part of body
 	ONLY_BODY_IGNORE_OVERLAY,
 
 	/// RGBA, contains only shadow
-	ONLY_SHADOW,
+	ONLY_SHADOW_HIDE_SELECTION,
+	ONLY_SHADOW_HIDE_FLAG_COLOR,
 
 	/// RGBA, contains only overlay
-	ONLY_OVERLAY,
+	ONLY_SELECTION,
+	ONLY_FLAG_COLOR,
 };
 
 enum class EScalingAlgorithm : int8_t
@@ -100,8 +105,8 @@ public:
 
 	virtual void setAlpha(uint8_t value) = 0;
 
-	//only indexed bitmaps with 7 special colors
 	virtual void setOverlayColor(const ColorRGBA & color) = 0;
+	virtual void setEffectColor(const ColorRGBA & color) = 0;
 
 	virtual ~IImage() = default;
 };

+ 5 - 2
client/renderSDL/FontChain.cpp

@@ -74,9 +74,12 @@ bool FontChain::bitmapFontsPrioritized(const std::string & bitmapFontName) const
 	return true; // else - use original bitmap fonts
 }
 
-void FontChain::addTrueTypeFont(const JsonNode & trueTypeConfig)
+void FontChain::addTrueTypeFont(const JsonNode & trueTypeConfig, bool begin)
 {
-	chain.insert(chain.begin(), std::make_unique<CTrueTypeFont>(trueTypeConfig));
+	if(begin)
+		chain.insert(chain.begin(), std::make_unique<CTrueTypeFont>(trueTypeConfig));
+	else
+		chain.push_back(std::make_unique<CTrueTypeFont>(trueTypeConfig));
 }
 
 void FontChain::addBitmapFont(const std::string & bitmapFilename)

+ 1 - 1
client/renderSDL/FontChain.h

@@ -33,7 +33,7 @@ class FontChain final : public IFont
 public:
 	FontChain() = default;
 
-	void addTrueTypeFont(const JsonNode & trueTypeConfig);
+	void addTrueTypeFont(const JsonNode & trueTypeConfig, bool begin);
 	void addBitmapFont(const std::string & bitmapFilename);
 
 	size_t getLineHeightScaled() const override;

+ 10 - 6
client/renderSDL/RenderHandler.cpp

@@ -230,6 +230,7 @@ std::shared_ptr<ISharedImage> RenderHandler::loadImageFromFileUncached(const Ima
 		if (generated)
 			return generated;
 
+		logGlobal->error("Failed to load image %s", locator.image->getOriginalName());
 		return std::make_shared<SDLImageShared>(ImagePath::builtin("DEFAULT"));
 	}
 
@@ -292,9 +293,9 @@ std::shared_ptr<SDLImageShared> RenderHandler::loadScaledImage(const ImageLocato
 
 	std::string imagePathString = pathToLoad.getName();
 
-	if(locator.layer == EImageBlitMode::ONLY_OVERLAY)
+	if(locator.layer == EImageBlitMode::ONLY_FLAG_COLOR || locator.layer == EImageBlitMode::ONLY_SELECTION)
 		imagePathString += "-OVERLAY";
-	if(locator.layer == EImageBlitMode::ONLY_SHADOW)
+	if(locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION || locator.layer == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR)
 		imagePathString += "-SHADOW";
 	if(locator.playerColored.isValidPlayer())
 		imagePathString += "-" + boost::to_upper_copy(GameConstants::PLAYER_COLOR_NAMES[locator.playerColored.getNum()]);
@@ -347,7 +348,10 @@ std::shared_ptr<IImage> RenderHandler::loadImage(const AnimationPath & path, int
 	if (!locator.empty())
 		return loadImage(locator);
 	else
+	{
+		logGlobal->error("Failed to load non-existing image");
 		return loadImage(ImageLocator(ImagePath::builtin("DEFAULT"), mode));
+	}
 }
 
 std::shared_ptr<IImage> RenderHandler::loadImage(const ImagePath & path, EImageBlitMode mode)
@@ -375,7 +379,7 @@ void RenderHandler::addImageListEntries(const EntityService * service)
 			if (imageName.empty())
 				return;
 
-			auto & layout = getAnimationLayout(AnimationPath::builtin("SPRITES/" + listName), 1, EImageBlitMode::SIMPLE);
+			auto & layout = getAnimationLayout(AnimationPath::builtin("SPRITES/" + listName), 1, EImageBlitMode::COLORKEY);
 
 			JsonNode entry;
 			entry["file"].String() = imageName;
@@ -413,8 +417,8 @@ static void detectOverlappingBuildings(RenderHandler * renderHandler, const Fact
 			if (left->pos.z != right->pos.z)
 				continue; // buildings already have different z-index and have well-defined overlap logic
 
-			auto leftImage = renderHandler->loadImage(left->defName, 0, 0, EImageBlitMode::SIMPLE);
-			auto rightImage = renderHandler->loadImage(right->defName, 0, 0, EImageBlitMode::SIMPLE);
+			auto leftImage = renderHandler->loadImage(left->defName, 0, 0, EImageBlitMode::COLORKEY);
+			auto rightImage = renderHandler->loadImage(right->defName, 0, 0, EImageBlitMode::COLORKEY);
 
 			Rect leftRect( left->pos.x, left->pos.y, leftImage->width(), leftImage->height());
 			Rect rightRect( right->pos.x, right->pos.y, rightImage->width(), rightImage->height());
@@ -490,7 +494,7 @@ std::shared_ptr<const IFont> RenderHandler::loadFont(EFonts font)
 
 		bitmapPath = bmpConf[index].String();
 		if (!ttfConf[bitmapPath].isNull())
-			loadedFont->addTrueTypeFont(ttfConf[bitmapPath]);
+			loadedFont->addTrueTypeFont(ttfConf[bitmapPath], !config["lowPriority"].Bool());
 	}
 	loadedFont->addBitmapFont(bitmapPath);
 

+ 119 - 33
client/renderSDL/ScalableImage.cpp

@@ -97,47 +97,67 @@ void ScalableImageParameters::preparePalette(const SDL_Palette * originalPalette
 {
 	switch(blitMode)
 	{
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::ONLY_OVERLAY:
+		case EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR:
+		case EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION:
+		case EImageBlitMode::ONLY_FLAG_COLOR:
+		case EImageBlitMode::ONLY_SELECTION:
 			adjustPalette(originalPalette, blitMode, ColorFilter::genAlphaShifter(0), 0);
 			break;
+		case EImageBlitMode::GRAYSCALE_BODY_HIDE_SELECTION:
+			adjustPalette(originalPalette, blitMode, ColorFilter::genMuxerShifter( { 0.299, 0.587, 0.114, 0}, { 0.299, 0.587, 0.114, 0}, { 0.299, 0.587, 0.114, 0}, 1), 0);
+			break;
+
 	}
 
 	switch(blitMode)
 	{
 		case EImageBlitMode::SIMPLE:
 		case EImageBlitMode::WITH_SHADOW:
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+		case EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR:
+		case EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION:
+		case EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR:
+		case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
 			setShadowTransparency(originalPalette, 1.0);
 			break;
-		case EImageBlitMode::ONLY_BODY:
+		case EImageBlitMode::ONLY_BODY_HIDE_SELECTION:
+		case EImageBlitMode::ONLY_BODY_HIDE_FLAG_COLOR:
 		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
-		case EImageBlitMode::ONLY_OVERLAY:
+		case EImageBlitMode::ONLY_FLAG_COLOR:
+		case EImageBlitMode::ONLY_SELECTION:
+		case EImageBlitMode::GRAYSCALE_BODY_HIDE_SELECTION:
 			setShadowTransparency(originalPalette, 0.0);
 			break;
 	}
 
 	switch(blitMode)
 	{
-		case EImageBlitMode::ONLY_OVERLAY:
-		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			setOverlayColor(originalPalette, Colors::WHITE_TRUE);
+		case EImageBlitMode::ONLY_FLAG_COLOR:
+		case EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR:
+			setOverlayColor(originalPalette, Colors::WHITE_TRUE, false);
+			break;
+		case EImageBlitMode::ONLY_SELECTION:
+		case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
+			setOverlayColor(originalPalette, Colors::WHITE_TRUE, true);
+			break;
+		case EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR:
+		case EImageBlitMode::ONLY_BODY_HIDE_FLAG_COLOR:
+			setOverlayColor(originalPalette, Colors::TRANSPARENCY, false);
 			break;
-		case EImageBlitMode::ONLY_SHADOW:
-		case EImageBlitMode::ONLY_BODY:
-			setOverlayColor(originalPalette, Colors::TRANSPARENCY);
+		case EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION:
+		case EImageBlitMode::ONLY_BODY_HIDE_SELECTION:
+		case EImageBlitMode::GRAYSCALE_BODY_HIDE_SELECTION:
+			setOverlayColor(originalPalette, Colors::TRANSPARENCY, true);
 			break;
 	}
 }
 
-void ScalableImageParameters::setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color)
+void ScalableImageParameters::setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color, bool includeShadow)
 {
 	palette->colors[5] = CSDL_Ext::toSDL(addColors(targetPalette[5], color));
 
-	for (int i : {6,7})
+	if (includeShadow)
 	{
-		if (colorsSimilar(originalPalette->colors[i], sourcePalette[i]))
+		for (int i : {6,7})
 			palette->colors[i] = CSDL_Ext::toSDL(addColors(targetPalette[i], color));
 	}
 }
@@ -183,7 +203,7 @@ void ScalableImageParameters::setShadowTransparency(const SDL_Palette * original
 void ScalableImageParameters::adjustPalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode, const ColorFilter & shifter, uint32_t colorsToSkipMask)
 {
 	// If shadow is enabled, following colors must be skipped unconditionally
-	if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_OVERLAY)
+	if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_SELECTION || blitMode == EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR)
 		colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4);
 
 	// Note: here we skip first colors in the palette that are predefined in H3 images
@@ -259,11 +279,15 @@ void ScalableImageShared::draw(SDL_Surface * where, const Point & dest, const Re
 	bool shadowLoading = scaled.at(scalingFactor).shadow.at(0) && scaled.at(scalingFactor).shadow.at(0)->isLoading();
 	bool bodyLoading = scaled.at(scalingFactor).body.at(0) && scaled.at(scalingFactor).body.at(0)->isLoading();
 	bool overlayLoading = scaled.at(scalingFactor).overlay.at(0) && scaled.at(scalingFactor).overlay.at(0)->isLoading();
+	bool grayscaleLoading = scaled.at(scalingFactor).bodyGrayscale.at(0) && scaled.at(scalingFactor).bodyGrayscale.at(0)->isLoading();
 	bool playerLoading = parameters.player != PlayerColor::CANNOT_DETERMINE && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum()) && scaled.at(scalingFactor).playerColored.at(1+parameters.player.getNum())->isLoading();
 
-	if (shadowLoading || bodyLoading || overlayLoading || playerLoading)
+	if (shadowLoading || bodyLoading || overlayLoading || playerLoading || grayscaleLoading)
 	{
 		getFlippedImage(scaled[1].body)->scaledDraw(where, parameters.palette, dimensions() * scalingFactor, dest, src, parameters.colorMultiplier, parameters.alphaValue, locator.layer);
+
+		if (parameters.effectColorMultiplier.a != ColorRGBA::ALPHA_TRANSPARENT)
+			getFlippedImage(scaled[1].body)->scaledDraw(where, parameters.palette, dimensions() * scalingFactor, dest, src, parameters.effectColorMultiplier, parameters.alphaValue, locator.layer);
 		return;
 	}
 
@@ -278,6 +302,9 @@ void ScalableImageShared::draw(SDL_Surface * where, const Point & dest, const Re
 	{
 		if (scaled.at(scalingFactor).body.at(0))
 			flipAndDraw(scaled.at(scalingFactor).body, parameters.colorMultiplier, parameters.alphaValue);
+
+		if (scaled.at(scalingFactor).bodyGrayscale.at(0) && parameters.effectColorMultiplier.a != ColorRGBA::ALPHA_TRANSPARENT)
+			flipAndDraw(scaled.at(scalingFactor).bodyGrayscale, parameters.effectColorMultiplier, parameters.alphaValue);
 	}
 
 	if (scaled.at(scalingFactor).overlay.at(0))
@@ -353,7 +380,25 @@ void ScalableImageInstance::setOverlayColor(const ColorRGBA & color)
 	parameters.ovelayColorMultiplier = color;
 
 	if (parameters.palette)
-		parameters.setOverlayColor(image->getPalette(), color);
+		parameters.setOverlayColor(image->getPalette(), color, blitMode == EImageBlitMode::WITH_SHADOW_AND_SELECTION);
+}
+
+void ScalableImageInstance::setEffectColor(const ColorRGBA & color)
+{
+	parameters.effectColorMultiplier = color;
+
+	if (parameters.palette)
+	{
+		const auto grayscaleFilter = ColorFilter::genMuxerShifter( { 0.299, 0.587, 0.114, 0}, { 0.299, 0.587, 0.114, 0}, { 0.299, 0.587, 0.114, 0}, 1);
+		const auto effectStrengthFilter = ColorFilter::genRangeShifter( 0, 0, 0, color.r / 255.f, color.g / 255.f, color.b / 255.f);
+		const auto effectFilter = ColorFilter::genCombined(grayscaleFilter, effectStrengthFilter);
+		const auto effectiveFilter = ColorFilter::genInterpolated(ColorFilter::genEmptyShifter(), effectFilter, color.a / 255.f);
+
+		parameters.adjustPalette(image->getPalette(), blitMode, effectiveFilter, 0);
+	}
+
+	if (color.a != ColorRGBA::ALPHA_TRANSPARENT)
+		image->prepareEffectImage();
 }
 
 void ScalableImageInstance::playerColored(const PlayerColor & player)
@@ -410,11 +455,19 @@ std::shared_ptr<const ISharedImage> ScalableImageShared::loadOrGenerateImage(EIm
 	if (loadedImage)
 		return loadedImage;
 
+	// optional images for 1x resolution - only try load them, don't attempt to generate
+	bool optionalImage =
+		mode == EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR ||
+		mode == EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION ||
+		mode == EImageBlitMode::ONLY_FLAG_COLOR ||
+		mode == EImageBlitMode::ONLY_SELECTION ||
+		mode == EImageBlitMode::GRAYSCALE_BODY_HIDE_SELECTION ||
+		color != PlayerColor::CANNOT_DETERMINE;
+
 	if (scalingFactor == 1)
 	{
-		// optional images for 1x resolution - only try load them, don't attempt to generate
 		// this block should never be called for 'body' layer - that image is loaded unconditionally before construction
-		assert(mode == EImageBlitMode::ONLY_SHADOW || mode == EImageBlitMode::ONLY_OVERLAY || color != PlayerColor::CANNOT_DETERMINE);
+		assert(optionalImage);
 		return nullptr;
 	}
 
@@ -427,7 +480,7 @@ std::shared_ptr<const ISharedImage> ScalableImageShared::loadOrGenerateImage(EIm
 		{
 			if (scaling == 1)
 			{
-				if (mode == EImageBlitMode::ONLY_SHADOW || mode == EImageBlitMode::ONLY_OVERLAY || color != PlayerColor::CANNOT_DETERMINE)
+				if (optionalImage)
 				{
 					ScalableImageParameters parameters(getPalette(), mode);
 					return loadedImage->scaleInteger(scalingFactor, parameters.palette, mode);
@@ -464,9 +517,13 @@ void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor col
 				scaled[scalingFactor].body[0] = loadOrGenerateImage(locator.layer, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
 				break;
 
-			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			case EImageBlitMode::ONLY_BODY:
-				scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
+			case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
+			case EImageBlitMode::ONLY_BODY_HIDE_SELECTION:
+				scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_HIDE_SELECTION, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
+				break;
+			case EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR:
+			case EImageBlitMode::ONLY_BODY_HIDE_FLAG_COLOR:
+				scaled[scalingFactor].body[0] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_HIDE_FLAG_COLOR, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].body[0]);
 				break;
 
 			case EImageBlitMode::WITH_SHADOW:
@@ -486,9 +543,13 @@ void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor col
 				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(locator.layer, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
 				break;
 
-			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-			case EImageBlitMode::ONLY_BODY:
-				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
+			case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
+			case EImageBlitMode::ONLY_BODY_HIDE_SELECTION:
+				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_HIDE_SELECTION, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
+				break;
+			case EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR:
+			case EImageBlitMode::ONLY_BODY_HIDE_FLAG_COLOR:
+				scaled[scalingFactor].playerColored[1+color.getNum()] = loadOrGenerateImage(EImageBlitMode::ONLY_BODY_HIDE_FLAG_COLOR, scalingFactor, color, scaled[1].playerColored[1+color.getNum()]);
 				break;
 
 			case EImageBlitMode::WITH_SHADOW:
@@ -503,9 +564,13 @@ void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor col
 		switch(locator.layer)
 		{
 			case EImageBlitMode::WITH_SHADOW:
-			case EImageBlitMode::ONLY_SHADOW:
-			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-				scaled[scalingFactor].shadow[0] = loadOrGenerateImage(EImageBlitMode::ONLY_SHADOW, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].shadow[0]);
+			case EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION:
+			case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
+				scaled[scalingFactor].shadow[0] = loadOrGenerateImage(EImageBlitMode::ONLY_SHADOW_HIDE_SELECTION, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].shadow[0]);
+				break;
+			case EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR:
+			case EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR:
+				scaled[scalingFactor].shadow[0] = loadOrGenerateImage(EImageBlitMode::ONLY_SHADOW_HIDE_FLAG_COLOR, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].shadow[0]);
 				break;
 			default:
 				break;
@@ -516,9 +581,13 @@ void ScalableImageShared::loadScaledImages(int8_t scalingFactor, PlayerColor col
 	{
 		switch(locator.layer)
 		{
-			case EImageBlitMode::ONLY_OVERLAY:
-			case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
-				scaled[scalingFactor].overlay[0] = loadOrGenerateImage(EImageBlitMode::ONLY_OVERLAY, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].overlay[0]);
+			case EImageBlitMode::ONLY_FLAG_COLOR:
+			case EImageBlitMode::WITH_SHADOW_AND_FLAG_COLOR:
+				scaled[scalingFactor].overlay[0] = loadOrGenerateImage(EImageBlitMode::ONLY_FLAG_COLOR, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].overlay[0]);
+				break;
+			case EImageBlitMode::ONLY_SELECTION:
+			case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
+				scaled[scalingFactor].overlay[0] = loadOrGenerateImage(EImageBlitMode::ONLY_SELECTION, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].overlay[0]);
 				break;
 			default:
 				break;
@@ -530,3 +599,20 @@ void ScalableImageShared::preparePlayerColoredImage(PlayerColor color)
 {
 	loadScaledImages(GH.screenHandler().getScalingFactor(), color);
 }
+
+void ScalableImageShared::prepareEffectImage()
+{
+	int scalingFactor = GH.screenHandler().getScalingFactor();
+
+	if (scaled[scalingFactor].bodyGrayscale[0] == nullptr)
+	{
+		switch(locator.layer)
+		{
+			case EImageBlitMode::WITH_SHADOW_AND_SELECTION:
+				scaled[scalingFactor].bodyGrayscale[0] = loadOrGenerateImage(EImageBlitMode::GRAYSCALE_BODY_HIDE_SELECTION, scalingFactor, PlayerColor::CANNOT_DETERMINE, scaled[1].bodyGrayscale[0]);
+				break;
+			default:
+				break;
+		}
+	}
+}

+ 7 - 1
client/renderSDL/ScalableImage.h

@@ -26,6 +26,7 @@ struct ScalableImageParameters : boost::noncopyable
 
 	ColorRGBA colorMultiplier = Colors::WHITE_TRUE;
 	ColorRGBA ovelayColorMultiplier = Colors::WHITE_TRUE;
+	ColorRGBA effectColorMultiplier = Colors::TRANSPARENCY;
 
 	PlayerColor player = PlayerColor::CANNOT_DETERMINE;
 	uint8_t alphaValue = 255;
@@ -39,7 +40,7 @@ struct ScalableImageParameters : boost::noncopyable
 	void setShadowTransparency(const SDL_Palette * originalPalette, float factor);
 	void shiftPalette(const SDL_Palette * originalPalette, uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove);
 	void playerColored(PlayerColor player);
-	void setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color);
+	void setOverlayColor(const SDL_Palette * originalPalette, const ColorRGBA & color, bool includeShadow);
 	void preparePalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode);
 	void adjustPalette(const SDL_Palette * originalPalette, EImageBlitMode blitMode, const ColorFilter & shifter, uint32_t colorsToSkipMask);
 };
@@ -64,6 +65,9 @@ class ScalableImageShared final : public std::enable_shared_from_this<ScalableIm
 		/// Upscaled overlay (player color, selection highlight) of our image, may be null
 		FlippedImages overlay;
 
+		/// Upscaled grayscale version of body, for special effects in combat (e.g clone / petrify / berserk)
+		FlippedImages bodyGrayscale;
+
 		/// player-colored images of this particular scale, mostly for UI. These are never flipped in h3
 		PlayerColoredImages playerColored;
 	};
@@ -91,6 +95,7 @@ public:
 
 	std::shared_ptr<ScalableImageInstance> createImageReference();
 
+	void prepareEffectImage();
 	void preparePlayerColoredImage(PlayerColor color);
 };
 
@@ -115,6 +120,7 @@ public:
 	void setAlpha(uint8_t value) override;
 	void draw(SDL_Surface * where, const Point & pos, const Rect * src, int scalingFactor) const override;
 	void setOverlayColor(const ColorRGBA & color) override;
+	void setEffectColor(const ColorRGBA & color) override;
 	void playerColored(const PlayerColor & player) override;
 	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override;
 	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;

+ 11 - 18
client/renderSDL/ScreenHandler.cpp

@@ -208,6 +208,10 @@ ScreenHandler::ScreenHandler()
 	// NOTE: requires SDL 2.24.
 	SDL_SetHint(SDL_HINT_WINDOWS_DPI_AWARENESS, "permonitor");
 #endif
+	if(settings["video"]["allowPortrait"].Bool())
+		SDL_SetHint(SDL_HINT_ORIENTATIONS, "Portrait PortraitUpsideDown LandscapeLeft LandscapeRight");
+	else
+		SDL_SetHint(SDL_HINT_ORIENTATIONS, "LandscapeLeft LandscapeRight");
 
 	if(SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER | SDL_INIT_AUDIO | SDL_INIT_GAMECONTROLLER))
 	{
@@ -354,34 +358,23 @@ EUpscalingFilter ScreenHandler::loadUpscalingFilter() const
 		return filter;
 
 	// else - autoselect
-#ifdef VCMI_MOBILE
-	// to help with performance - only if player explicitly enabled xbrz
-	return EUpscalingFilter::NONE;
-#else
 	Point outputResolution = getRenderResolution();
 	Point logicalResolution = getPreferredLogicalResolution();
 
 	float scaleX = static_cast<float>(outputResolution.x) / logicalResolution.x;
 	float scaleY = static_cast<float>(outputResolution.x) / logicalResolution.x;
 	float scaling = std::min(scaleX, scaleY);
+	int systemMemoryMb = SDL_GetSystemRAM();
 
 	if (scaling <= 1.001f)
 		return EUpscalingFilter::NONE; // running at original resolution or even lower than that - no need for xbrz
-	else
-		return EUpscalingFilter::XBRZ_2;
-#endif
 
-#if 0
-// Old version, most optimal, but rather performance-heavy
-	if (scaling <= 1.001f)
-		return EUpscalingFilter::NONE; // running at original resolution or even lower than that - no need for xbrz
-	if (scaling <= 2.001f)
-		return EUpscalingFilter::XBRZ_2; // resolutions below 1200p (including 1080p / FullHD)
-	if (scaling <= 3.001f)
-		return EUpscalingFilter::XBRZ_3; // resolutions below 2400p (including 1440p and 2160p / 4K)
+	if (systemMemoryMb < 2048)
+		return EUpscalingFilter::NONE; // xbrz2 may use ~1.0 - 1.5 Gb of RAM and has notable CPU cost - avoid on low-spec hardware
 
-	return EUpscalingFilter::XBRZ_4; // Only for massive displays, e.g. 8K
-#endif
+	// Only using xbrz2 for autoselection.
+	// Higher options may have high system requirements and should be only selected explicitly by player
+	return EUpscalingFilter::XBRZ_2;
 }
 
 void ScreenHandler::selectUpscalingFilter()
@@ -478,7 +471,7 @@ SDL_Window * ScreenHandler::createWindow()
 #endif
 
 #ifdef VCMI_ANDROID
-	return createWindowImpl(Point(), SDL_WINDOW_FULLSCREEN, false);
+	return createWindowImpl(Point(), SDL_WINDOW_RESIZABLE, false);
 #endif
 }
 

+ 16 - 0
client/widgets/CExchangeController.cpp

@@ -110,6 +110,22 @@ void CExchangeController::moveStack(bool leftToRight, SlotID sourceSlot)
 	}
 }
 
+void CExchangeController::moveSingleStackCreature(bool leftToRight, SlotID sourceSlot, bool forceEmptySlotTarget)
+{
+	const auto source = leftToRight ? left : right;
+	const auto target = leftToRight ? right : left;
+	auto creature = source->getCreature(sourceSlot);
+
+	if(creature == nullptr || source->stacksCount() == 1)
+		return;
+
+	SlotID targetSlot = forceEmptySlotTarget ? target->getFreeSlot() : target->getSlotFor(creature);
+	if(targetSlot.validSlot())
+	{
+		LOCPLINT->cb->splitStack(source, target, sourceSlot, targetSlot, target->getStackCount(targetSlot) + 1);
+	}
+}
+
 void CExchangeController::swapArtifacts(bool equipped, bool baclpack)
 {
 	LOCPLINT->cb->bulkMoveArtifacts(left->id, right->id, true, equipped, baclpack);

+ 1 - 0
client/widgets/CExchangeController.h

@@ -20,6 +20,7 @@ public:
 	void swapArmy();
 	void moveArmy(bool leftToRight, std::optional<SlotID> heldSlot);
 	void moveStack(bool leftToRight, SlotID sourceSlot);
+	void moveSingleStackCreature(bool leftToRight, SlotID sourceSlot, bool forceEmptySlotTarget);
 	void swapArtifacts(bool equipped, bool baclpack);
 	void moveArtifacts(bool leftToRight, bool equipped, bool baclpack);
 

+ 13 - 3
client/widgets/Images.cpp

@@ -32,6 +32,16 @@
 #include "../../lib/texts/CGeneralTextHandler.h" //for Unicode related stuff
 #include "../../lib/CRandomGenerator.h"
 
+static EImageBlitMode getModeForFlags( uint8_t flags)
+{
+	if (flags & CCreatureAnim::CREATURE_MODE)
+		return EImageBlitMode::WITH_SHADOW_AND_SELECTION;
+	if (flags & CCreatureAnim::MAP_OBJECT_MODE)
+		return EImageBlitMode::WITH_SHADOW;
+
+	return EImageBlitMode::COLORKEY;
+}
+
 CPicture::CPicture(std::shared_ptr<IImage> image, const Point & position)
 	: bg(image)
 	, needRefresh(false)
@@ -195,12 +205,12 @@ CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, size_t Group, i
 {
 	pos.x += x;
 	pos.y += y;
-	anim = GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY);
+	anim = GH.renderHandler().loadAnimation(name, getModeForFlags(Flags));
 	init();
 }
 
 CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, Rect targetPos, size_t Group, ui8 Flags):
-	anim(GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY)),
+	anim(GH.renderHandler().loadAnimation(name, getModeForFlags(Flags))),
 	frame(Frame),
 	group(Group),
 	flags(Flags),
@@ -318,7 +328,7 @@ bool CAnimImage::isPlayerColored() const
 }
 
 CShowableAnim::CShowableAnim(int x, int y, const AnimationPath & name, ui8 Flags, ui32 frameTime, size_t Group, uint8_t alpha):
-	anim(GH.renderHandler().loadAnimation(name, (Flags & CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY)),
+	anim(GH.renderHandler().loadAnimation(name, getModeForFlags(Flags))),
 	group(Group),
 	frame(0),
 	first(0),

+ 2 - 1
client/widgets/Images.h

@@ -151,7 +151,8 @@ public:
 		BASE=1,            //base frame will be blitted before current one
 		HORIZONTAL_FLIP=2, //TODO: will be displayed rotated
 		VERTICAL_FLIP=4,   //TODO: will be displayed rotated
-		CREATURE_MODE=8,   // use alpha channel for images with palette. Required for creatures in battle and map objects
+		CREATURE_MODE=8,   // use alpha channel for images with palette. Required for creatures in battle
+		MAP_OBJECT_MODE=16,   // use alpha channel for images with palette. Required for map objects
 		PLAY_ONCE=32       //play animation only once and stop at last frame
 	};
 protected:

+ 11 - 2
client/widgets/MiscWidgets.cpp

@@ -550,6 +550,15 @@ CreatureTooltip::CreatureTooltip(Point pos, const CGCreature * creature)
 	tooltipTextbox = std::make_shared<CTextBox>(textContent, Rect(15, 95, 230, 150), 0, FONT_SMALL, ETextAlignment::TOPCENTER, Colors::WHITE);
 }
 
+void CreatureTooltip::show(Canvas & to)
+{
+	// fixes scrolling of textbox (#5076)
+	setRedrawParent(true);
+	redraw();
+
+	CIntObject::show(to);
+}
+
 void MoraleLuckBox::set(const AFactionMember * node)
 {
 	OBJECT_CONSTRUCTION;
@@ -615,9 +624,9 @@ void MoraleLuckBox::set(const AFactionMember * node)
 		imageName = morale ? "IMRL42" : "ILCK42";
 
 	image = std::make_shared<CAnimImage>(AnimationPath::builtin(imageName), *component.value + 3);
-	image->moveBy(Point(pos.w/2 - image->pos.w/2, pos.h/2 - image->pos.h/2));//center icon
+	image->moveBy(Point(pos.w/2 - image->pos.w/2, pos.h/2 - image->pos.h/2)); //center icon
 	if(settings["general"]["enableUiEnhancements"].Bool())
-		label = std::make_shared<CLabel>(small ? 30 : 42, small ? 20 : 38, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(modifierList->totalValue()));
+		label = std::make_shared<CLabel>((image->pos.topLeft() - pos.topLeft()).x + (small ? 28 : 40), (image->pos.topLeft() - pos.topLeft()).y + (small ? 20 : 38), EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(modifierList->totalValue()));
 }
 
 MoraleLuckBox::MoraleLuckBox(bool Morale, const Rect &r, bool Small)

+ 1 - 0
client/widgets/MiscWidgets.h

@@ -166,6 +166,7 @@ class CreatureTooltip : public CIntObject
 	std::shared_ptr<CAnimImage> creatureImage;
 	std::shared_ptr<CTextBox> tooltipTextbox;
 
+	void show(Canvas & to) override;
 public:
 	CreatureTooltip(Point pos, const CGCreature * creature);
 };

+ 5 - 0
client/widgets/ObjectLists.cpp

@@ -168,6 +168,11 @@ std::shared_ptr<CIntObject> CListBox::getItem(size_t which)
 	return std::shared_ptr<CIntObject>();
 }
 
+std::shared_ptr<CSlider> CListBox::getSlider()
+{
+	return slider;
+}
+
 size_t CListBox::getIndexOf(std::shared_ptr<CIntObject> item)
 {
 	size_t i=first;

+ 2 - 0
client/widgets/ObjectLists.h

@@ -91,6 +91,8 @@ public:
 	//return item with index which or null if not present
 	std::shared_ptr<CIntObject> getItem(size_t which);
 
+	std::shared_ptr<CSlider> getSlider();
+
 	//return currently active items
 	const std::list<std::shared_ptr<CIntObject>> & getItems();
 

+ 3 - 0
client/widgets/RadialMenu.cpp

@@ -101,6 +101,9 @@ std::shared_ptr<RadialMenuItem> RadialMenu::findNearestItem(const Point & cursor
 		}
 	}
 
+	if (bestDistance > 2 * requiredDistanceFromCenter)
+		return nullptr;
+
 	assert(bestItem);
 	return bestItem;
 }

+ 88 - 10
client/windows/CCastleInterface.cpp

@@ -20,6 +20,7 @@
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
 #include "../PlayerLocalState.h"
+#include "../eventsSDL/InputHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
@@ -51,6 +52,7 @@
 #include "../../lib/IGameSettings.h"
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/GameConstants.h"
+#include "../../lib/gameState/UpgradeInfo.h"
 #include "../../lib/StartInfo.h"
 #include "../../lib/campaign/CampaignState.h"
 #include "../../lib/entities/building/CBuilding.h"
@@ -175,7 +177,7 @@ void CBuildingRect::show(Canvas & to)
 {
 	uint32_t stageDelay = BUILDING_APPEAR_TIMEPOINT;
 
-	bool showTextOverlay = GH.isKeyboardAltDown();
+	bool showTextOverlay = GH.isKeyboardAltDown() || GH.input().getNumTouchFingers() == 2;
 
 	if(stateTimeCounter < BUILDING_APPEAR_TIMEPOINT)
 	{
@@ -337,17 +339,92 @@ CHeroGSlot::CHeroGSlot(int x, int y, int updown, const CGHeroInstance * h, HeroS
 
 CHeroGSlot::~CHeroGSlot() = default;
 
+auto CHeroGSlot::getUpgradableSlots(const CArmedInstance *obj)
+{
+	struct result { bool isCreatureUpgradePossible; bool canAffordAny; bool canAffordAll; TResources totalCosts; std::vector<std::pair<SlotID, UpgradeInfo>> upgradeInfos; };
+
+	auto slots = std::map<SlotID, const CStackInstance*>(obj->Slots().begin(), obj->Slots().end());
+	std::vector<std::pair<SlotID, UpgradeInfo>> upgradeInfos;
+	for(auto & slot : slots)
+	{
+		auto upgradeInfo = std::make_pair(slot.first, UpgradeInfo(slot.second->getCreatureID()));
+		LOCPLINT->cb->fillUpgradeInfo(slot.second->armyObj, slot.first, upgradeInfo.second);
+		bool canUpgrade = obj->tempOwner == LOCPLINT->playerID && upgradeInfo.second.canUpgrade();
+		if(canUpgrade)
+			upgradeInfos.push_back(upgradeInfo);
+	}
+
+	std::sort(upgradeInfos.begin(), upgradeInfos.end(), [&](const std::pair<SlotID, UpgradeInfo> & lhs, const std::pair<SlotID, UpgradeInfo> & rhs) {
+		return lhs.second.oldID.toCreature()->getLevel() > rhs.second.oldID.toCreature()->getLevel();
+	});
+	bool creaturesToUpgrade = static_cast<bool>(upgradeInfos.size());
+
+	TResources costs = TResources();
+	std::vector<SlotID> slotInfosToDelete;
+	for(auto & upgradeInfo : upgradeInfos)
+	{
+		TResources upgradeCosts = upgradeInfo.second.getUpgradeCosts() * slots[upgradeInfo.first]->getCount();
+		if(LOCPLINT->cb->getResourceAmount().canAfford(costs + upgradeCosts))
+			costs += upgradeCosts;
+		else
+			slotInfosToDelete.push_back(upgradeInfo.first);
+	}
+	upgradeInfos.erase(std::remove_if(upgradeInfos.begin(), upgradeInfos.end(), [&slotInfosToDelete](const auto& item) {
+		return std::count(slotInfosToDelete.begin(), slotInfosToDelete.end(), item.first);
+	}), upgradeInfos.end());
+
+    return result { creaturesToUpgrade, static_cast<bool>(upgradeInfos.size()), !slotInfosToDelete.size(), costs, upgradeInfos };
+}
+
 void CHeroGSlot::gesture(bool on, const Point & initialPosition, const Point & finalPosition)
 {
 	if(!on)
 		return;
 
-	if(!hero)
+	const CArmedInstance *obj = hero;
+	if(upg == 0 && !obj)
+		obj = owner->town->getUpperArmy();
+	if(!obj)
 		return;
 
+	auto upgradableSlots = getUpgradableSlots(obj);
+	auto upgradeAll = [upgradableSlots, obj](){
+		if(!upgradableSlots.canAffordAny)
+		{
+			LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.townWindow.upgradeAll.notUpgradable"));
+			return;
+		}
+
+		std::vector<std::shared_ptr<CComponent>> resComps;
+		for(TResources::nziterator i(upgradableSlots.totalCosts); i.valid(); i++)
+			resComps.push_back(std::make_shared<CComponent>(ComponentType::RESOURCE, i->resType, i->resVal));
+		resComps.back()->newLine = true;
+		for(auto & upgradeInfo : upgradableSlots.upgradeInfos)
+			resComps.push_back(std::make_shared<CComponent>(ComponentType::CREATURE, upgradeInfo.second.getUpgrade(), obj->Slots().at(upgradeInfo.first)->count));
+			
+		std::string textID = upgradableSlots.canAffordAll ? "core.genrltxt.207" : "vcmi.townWindow.upgradeAll.notAllUpgradable";
+
+		LOCPLINT->showYesNoDialog(CGI->generaltexth->translate(textID), [upgradableSlots, obj](){
+			for(auto & upgradeInfo : upgradableSlots.upgradeInfos)
+				LOCPLINT->cb->upgradeCreature(obj, upgradeInfo.first, upgradeInfo.second.getUpgrade());
+		}, nullptr, resComps);
+	};
+
 	if (!settings["input"]["radialWheelGarrisonSwipe"].Bool())
 		return;
 
+	if(!hero)
+	{
+		if(upgradableSlots.isCreatureUpgradePossible)
+		{
+			std::vector<RadialMenuConfig> menuElements = {
+				{ RadialMenuConfig::ITEM_WW, true, "upgradeCreatures", "vcmi.radialWheel.upgradeCreatures", [upgradeAll](){ upgradeAll(); } },
+			};
+			GH.windows().createAndPushWindow<RadialMenu>(pos.center(), menuElements);
+		}
+		return;
+	}
+
 	std::shared_ptr<CHeroGSlot> other = upg ? owner->garrisonedHero : owner->visitingHero;
 
 	bool twoHeroes = hero && other->hero;
@@ -360,14 +437,15 @@ void CHeroGSlot::gesture(bool on, const Point & initialPosition, const Point & f
 		{ RadialMenuConfig::ITEM_NE, twoHeroes, "stackSplitDialog", "vcmi.radialWheel.heroSwapArmy", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).swapArmy();} },
 		{ RadialMenuConfig::ITEM_EE, twoHeroes, "tradeHeroes", "vcmi.radialWheel.heroExchange", [heroId, heroOtherId](){LOCPLINT->showHeroExchange(heroId, heroOtherId);} },
 		{ RadialMenuConfig::ITEM_SW, twoHeroes, "moveArtifacts", "vcmi.radialWheel.heroGetArtifacts", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).moveArtifacts(false, true, true);} },
-		{ RadialMenuConfig::ITEM_SE, twoHeroes, "swapArtifacts", "vcmi.radialWheel.heroSwapArtifacts", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).swapArtifacts(true, true);} },
-		{ RadialMenuConfig::ITEM_WW, true, "dismissHero", "vcmi.radialWheel.heroDismiss", [this]()
-		{
-			CFunctionList<void()> ony = [=](){ };
-			ony += [=](){ LOCPLINT->cb->dismissHero(hero); };
-			LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[22], ony, nullptr);
-		} },
+		{ RadialMenuConfig::ITEM_SE, twoHeroes, "swapArtifacts", "vcmi.radialWheel.heroSwapArtifacts", [heroId, heroOtherId](){CExchangeController(heroId, heroOtherId).swapArtifacts(true, true);} }
 	};
+	RadialMenuConfig upgradeSlot = { RadialMenuConfig::ITEM_WW, true, "upgradeCreatures", "vcmi.radialWheel.upgradeCreatures", [upgradeAll](){ upgradeAll(); } };
+	RadialMenuConfig dismissSlot = { RadialMenuConfig::ITEM_WW, true, "dismissHero", "vcmi.radialWheel.heroDismiss", [this](){ LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[22], [=](){ LOCPLINT->cb->dismissHero(hero); }, nullptr); } };
+
+	if(upgradableSlots.isCreatureUpgradePossible)
+		menuElements.push_back(upgradeSlot);
+	else
+		menuElements.push_back(dismissSlot);
 
 	GH.windows().createAndPushWindow<RadialMenu>(pos.center(), menuElements);
 }
@@ -692,7 +770,7 @@ void CCastleBuildings::show(Canvas & to)
 {
 	CIntObject::show(to);
 
-	bool showTextOverlay = GH.isKeyboardAltDown();
+	bool showTextOverlay = GH.isKeyboardAltDown() || GH.input().getNumTouchFingers() == 2;
 	if(showTextOverlay)
 		drawOverlays(to, buildings);
 }

+ 2 - 0
client/windows/CCastleInterface.h

@@ -103,6 +103,8 @@ class CHeroGSlot : public CIntObject
 	const CGHeroInstance * hero;
 	int upg; //0 - up garrison, 1 - down garrison
 
+	auto getUpgradableSlots(const CArmedInstance *obj);
+
 public:
 	CHeroGSlot(int x, int y, int updown, const CGHeroInstance *h, HeroSlots * Owner);
 	~CHeroGSlot();

+ 8 - 4
client/windows/CCreatureWindow.cpp

@@ -238,7 +238,7 @@ CStackWindow::ActiveSpellsSection::ActiveSpellsSection(CStackWindow * owner, int
 			if (spellBonuses->empty())
 				throw std::runtime_error("Failed to find effects for spell " + effect.toSpell()->getJsonKey());
 
-			int duration = spellBonuses->front()->duration;
+			int duration = spellBonuses->front()->turnsRemain;
 			boost::replace_first(spellText, "%d", std::to_string(duration));
 
 			spellIcons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("SpellInt"), effect + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed));
@@ -671,7 +671,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s
 			expArea->text = parent->generateStackExpDescription();
 		}
 		expLabel = std::make_shared<CLabel>(
-				pos.x + 21, pos.y + 52, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE,
+				pos.x + 21, pos.y + 55, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE,
 				TextOperations::formatMetric(stack->experience, 6));
 	}
 
@@ -776,9 +776,13 @@ CStackWindow::CStackWindow(const CStackInstance * stack, std::function<void()> d
 	info->creature = stack->getCreature();
 	info->creatureCount = stack->count;
 
-	info->upgradeInfo = std::make_optional(UnitView::StackUpgradeInfo(upgradeInfo));
+	if(upgradeInfo.canUpgrade())
+	{
+		info->upgradeInfo = std::make_optional(UnitView::StackUpgradeInfo(upgradeInfo));
+		info->upgradeInfo->callback = callback;
+	}
+	
 	info->dismissInfo = std::make_optional(UnitView::StackDismissInfo());
-	info->upgradeInfo->callback = callback;
 	info->dismissInfo->callback = dismiss;
 	info->owner = dynamic_cast<const CGHeroInstance *> (stack->armyObj);
 	init();

Některé soubory nejsou zobrazeny, neboť je v těchto rozdílových datech změněno mnoho souborů