Browse Source

Merge remote-tracking branch 'origin/develop' into parellel_rmg

# Conflicts:
#	lib/rmg/CZonePlacer.h
#	lib/rmg/TreasurePlacer.h
Tomasz Zieliński 2 years ago
parent
commit
d137f7157c
100 changed files with 1862 additions and 1577 deletions
  1. 115 3
      .github/workflows/github.yml
  2. 1 0
      .gitmodules
  3. 3 3
      AI/BattleAI/AttackPossibility.cpp
  4. 10 10
      AI/BattleAI/BattleAI.cpp
  5. 1 1
      AI/BattleAI/BattleAI.h
  6. 6 6
      AI/BattleAI/BattleExchangeVariant.cpp
  7. 2 8
      AI/BattleAI/BattleExchangeVariant.h
  8. 2 2
      AI/BattleAI/PotentialTargets.cpp
  9. 4 4
      AI/BattleAI/StackWithBonuses.cpp
  10. 2 8
      AI/BattleAI/StackWithBonuses.h
  11. 1 1
      AI/BattleAI/ThreatMap.cpp
  12. 1 1
      AI/FuzzyLite
  13. 3 43
      AI/Nullkiller/AIGateway.cpp
  14. 2 7
      AI/Nullkiller/AIGateway.h
  15. 2 2
      AI/Nullkiller/AIUtility.cpp
  16. 4 4
      AI/Nullkiller/AIUtility.h
  17. 4 4
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  18. 1 6
      AI/Nullkiller/Analyzers/ArmyManager.h
  19. 5 2
      AI/Nullkiller/Analyzers/BuildAnalyzer.h
  20. 50 3
      AI/Nullkiller/Analyzers/HeroManager.cpp
  21. 7 1
      AI/Nullkiller/Analyzers/HeroManager.h
  22. 5 11
      AI/Nullkiller/Analyzers/ObjectClusterizer.h
  23. 1 1
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp
  24. 7 7
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  25. 1 1
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  26. 1 1
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  27. 2 2
      AI/Nullkiller/Engine/AIMemory.cpp
  28. 1 1
      AI/Nullkiller/Engine/DeepDecomposer.h
  29. 6 6
      AI/Nullkiller/Engine/FuzzyEngines.cpp
  30. 1 1
      AI/Nullkiller/Engine/FuzzyHelper.h
  31. 1 1
      AI/Nullkiller/Engine/Nullkiller.cpp
  32. 1 0
      AI/Nullkiller/Engine/Nullkiller.h
  33. 14 10
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  34. 4 5
      AI/Nullkiller/Goals/AbstractGoal.h
  35. 0 2
      AI/Nullkiller/Goals/RecruitHero.cpp
  36. 1 1
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  37. 2 2
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  38. 1 3
      AI/Nullkiller/Pathfinding/Actors.h
  39. 2 4
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp
  40. 5 5
      AI/StupidAI/StupidAI.cpp
  41. 1 1
      AI/VCAI/AIUtility.cpp
  42. 3 3
      AI/VCAI/AIUtility.h
  43. 1 1
      AI/VCAI/ArmyManager.cpp
  44. 6 6
      AI/VCAI/FuzzyEngines.cpp
  45. 2 3
      AI/VCAI/Goals/AbstractGoal.h
  46. 15 10
      AI/VCAI/Goals/CollectRes.cpp
  47. 0 2
      AI/VCAI/Pathfinding/AINodeStorage.h
  48. 10 6
      AI/VCAI/VCAI.cpp
  49. 2 2
      AI/VCAI/VCAI.h
  50. 3 3
      CCallback.cpp
  51. 5 4
      CCallback.h
  52. BIN
      CI/android/android-release.jks
  53. 0 0
      CI/android/dailySigning.properties
  54. 2 0
      CI/android/releaseSigning.properties
  55. 1 0
      CMakeLists.txt
  56. 51 0
      CMakePresets.json
  57. 24 1
      ChangeLog.md
  58. 4 2
      Global.h
  59. BIN
      Mods/vcmi/Data/debug/cached.png
  60. BIN
      Mods/vcmi/Data/debug/spellRange.png
  61. 198 165
      Mods/vcmi/config/vcmi/chinese.json
  62. 10 1
      Mods/vcmi/config/vcmi/english.json
  63. 10 1
      Mods/vcmi/config/vcmi/german.json
  64. 1 1
      Mods/vcmi/config/vcmi/polish.json
  65. 2 3
      Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON
  66. 1 1
      Mods/vcmi/config/vcmi/russian.json
  67. 1 1
      Mods/vcmi/config/vcmi/ukrainian.json
  68. 1 0
      README.md
  69. 30 17
      android/vcmi-app/build.gradle
  70. 19 2
      client/CMakeLists.txt
  71. 1 1
      client/CMusicHandler.h
  72. 217 377
      client/CPlayerInterface.cpp
  73. 45 98
      client/CPlayerInterface.h
  74. 2 1
      client/Client.cpp
  75. 3 3
      client/Client.h
  76. 6 7
      client/ClientCommandManager.cpp
  77. 3 3
      client/NetPacksClient.cpp
  78. 268 0
      client/PlayerLocalState.cpp
  79. 111 0
      client/PlayerLocalState.h
  80. 248 350
      client/adventureMap/CAdventureMapInterface.cpp
  81. 72 66
      client/adventureMap/CAdventureMapInterface.h
  82. 8 9
      client/adventureMap/CAdventureOptions.cpp
  83. 21 25
      client/adventureMap/CInGameConsole.cpp
  84. 1 1
      client/adventureMap/CInGameConsole.h
  85. 7 6
      client/adventureMap/CInfoBar.cpp
  86. 21 20
      client/adventureMap/CList.cpp
  87. 10 4
      client/adventureMap/CMinimap.cpp
  88. 2 3
      client/adventureMap/CMinimap.h
  89. 1 1
      client/adventureMap/CResDataBar.cpp
  90. 1 1
      client/adventureMap/MapAudioPlayer.cpp
  91. 9 9
      client/battle/BattleActionsController.cpp
  92. 6 6
      client/battle/BattleAnimationClasses.cpp
  93. 6 6
      client/battle/BattleEffectsController.cpp
  94. 11 11
      client/battle/BattleFieldController.cpp
  95. 1 1
      client/battle/BattleFieldController.h
  96. 4 4
      client/battle/BattleInterface.cpp
  97. 7 6
      client/battle/BattleInterfaceClasses.cpp
  98. 5 5
      client/battle/BattleProjectileController.cpp
  99. 46 54
      client/battle/BattleStacksController.cpp
  100. 34 80
      client/battle/BattleWindow.cpp

+ 115 - 3
.github/workflows/github.yml

@@ -71,11 +71,11 @@ jobs:
           - platform: linux-qt6
             os: ubuntu-22.04
             test: 0
-            preset: linux-clang-release
+            preset: linux-clang-test
           - platform: linux
             os: ubuntu-20.04
             test: 0
-            preset: linux-gcc-release
+            preset: linux-gcc-test
           - platform: mac-intel
             os: macos-12
             test: 0
@@ -130,7 +130,7 @@ jobs:
             preset: android-conan-ninja-release
             conan_profile: android-64
             conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
-            artifact_platform: aarch64-v8a
+            artifact_platform: arm64-v8a
     runs-on: ${{ matrix.os }}
     defaults:
       run:
@@ -225,6 +225,7 @@ jobs:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
         path: |
           ${{github.workspace}}/out/build/${{matrix.preset}}/${{ env.VCMI_PACKAGE_FILE_NAME }}.${{ matrix.extension }}
+          
     - name: Android artifacts
       if: ${{ startsWith(matrix.platform, 'android') }}
       uses: actions/upload-artifact@v3
@@ -233,6 +234,14 @@ jobs:
         path: |
           ${{ env.ANDROID_APK_PATH }}
 
+    - name: Android JNI ${{matrix.platform}}
+      if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }}
+      uses: actions/upload-artifact@v3
+      with:
+        name: Android JNI ${{matrix.platform}}
+        path: |
+          ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs
+
     - name: Upload build
       if: ${{ (matrix.pack == 1 || startsWith(matrix.platform, 'android')) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' }}
       continue-on-error: true
@@ -254,3 +263,106 @@ jobs:
       env:
         SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
       if: always()
+  
+  # copy-pasted mostly
+  bundle_release:
+    
+    needs: build
+    if: always() && github.ref == 'refs/heads/master'
+    strategy:
+      matrix:
+        include:
+          - platform: android-32
+            os: ubuntu-22.04
+            extension: aab
+            preset: android-conan-ninja-release
+            conan_profile: android-32
+            conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
+            artifact_platform: aab
+    runs-on: ${{ matrix.os }}
+    defaults:
+      run:
+        shell: bash
+
+    steps:
+    - uses: actions/checkout@v3
+      with:
+        submodules: recursive
+
+    - name: Dependencies
+      run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh'
+      env:
+        VCMI_BUILD_PLATFORM: x64
+
+    - uses: actions/setup-python@v4
+      if: "${{ matrix.conan_profile != '' }}"
+      with:
+        python-version: '3.10'
+    - name: Conan setup
+      if: "${{ matrix.conan_profile != '' }}"
+      run: |
+        pip3 install 'conan<2.0'
+        conan profile new default --detect
+        conan install . \
+          --install-folder=conan-generated \
+          --no-imports \
+          --build=never \
+          --profile:build=default \
+          --profile:host=CI/conan/${{ matrix.conan_profile }} \
+          ${{ matrix.conan_options }}
+      env:
+        GENERATE_ONLY_BUILT_CONFIG: 1
+
+    - name: Git branch name
+      id: git-branch-name
+      uses: EthanSK/git-branch-name-action@v1
+
+    - name: Build Number
+      run: |
+        source '${{github.workspace}}/CI/get_package_name.sh'
+        if [ '${{ matrix.artifact_platform }}' ]; then
+          VCMI_PACKAGE_FILE_NAME+="-${{ matrix.artifact_platform }}"
+        fi
+        echo VCMI_PACKAGE_FILE_NAME="$VCMI_PACKAGE_FILE_NAME" >> $GITHUB_ENV
+        echo VCMI_PACKAGE_NAME_SUFFIX="$VCMI_PACKAGE_NAME_SUFFIX" >> $GITHUB_ENV
+        echo VCMI_PACKAGE_GITVERSION="$VCMI_PACKAGE_GITVERSION" >> $GITHUB_ENV
+      env:
+        PULL_REQUEST: ${{ github.event.pull_request.number }}
+
+    - name: CMake Preset
+      run: |
+        cmake --preset ${{ matrix.preset }}
+
+    - name: Build Preset
+      run: |
+        cmake --build --preset ${{matrix.preset}}
+
+    - name: Download libs x64
+      uses: actions/download-artifact@v3
+      with:
+        name: Android JNI android-64
+        path: ${{ github.workspace }}/android/vcmi-app/src/main/jniLibs/
+ 
+    - name: Create android package
+      run: |
+        cd android
+        ./gradlew bundleRelease --info
+        echo ANDROID_APK_PATH="$(ls ${{ github.workspace }}/android/vcmi-app/build/outputs/bundle/release/*.aab)" >> $GITHUB_ENV
+      env:
+        ANDROID_STORE_PASSWORD: ${{ secrets.ANDROID_STORE_PASSWORD }}
+        ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
+
+    - name: Android artifacts
+      uses: actions/upload-artifact@v3
+      with:
+        name: ${{ env.VCMI_PACKAGE_FILE_NAME }}
+        path: |
+          ${{ env.ANDROID_APK_PATH }}
+
+    - uses: act10ns/slack@v1
+      with:
+        status: ${{ job.status }}
+        channel: '#notifications'
+      env:
+        SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
+      if: always()

+ 1 - 0
.gitmodules

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

+ 3 - 3
AI/BattleAI/AttackPossibility.cpp

@@ -51,11 +51,11 @@ int64_t AttackPossibility::calculateDamageReduce(
 
 	// FIXME: provide distance info for Jousting bonus
 	auto enemyDamageBeforeAttack = cb.battleEstimateDamage(defender, attacker, 0);
-	auto enemiesKilled = damageDealt / defender->MaxHealth() + (damageDealt % defender->MaxHealth() >= defender->getFirstHPleft() ? 1 : 0);
+	auto enemiesKilled = damageDealt / defender->getMaxHealth() + (damageDealt % defender->getMaxHealth() >= defender->getFirstHPleft() ? 1 : 0);
 	auto enemyDamage = averageDmg(enemyDamageBeforeAttack.damage);
 	auto damagePerEnemy = enemyDamage / (double)defender->getCount();
 
-	return (int64_t)(damagePerEnemy * (enemiesKilled * KILL_BOUNTY + damageDealt * HEALTH_BOUNTY / (double)defender->MaxHealth()));
+	return (int64_t)(damagePerEnemy * (enemiesKilled * KILL_BOUNTY + damageDealt * HEALTH_BOUNTY / (double)defender->getMaxHealth()));
 }
 
 int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state)
@@ -97,7 +97,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
 	auto attacker = attackInfo.attacker;
 	auto defender = attackInfo.defender;
 	const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
-	static const auto selectorBlocksRetaliation = Selector::type()(Bonus::BLOCKS_RETALIATION);
+	static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
 	const auto attackerSide = state.playerToSide(state.battleGetOwner(attacker));
 	const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
 

+ 10 - 10
AI/BattleAI/BattleAI.cpp

@@ -102,14 +102,14 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 
 	try
 	{
-		if(stack->type->getId() == CreatureID::CATAPULT)
+		if(stack->creatureId() == CreatureID::CATAPULT)
 			return useCatapult(stack);
-		if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->hasBonusOfType(Bonus::HEALER))
+		if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER))
 		{
 			auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE);
 			std::map<int, const CStack*> woundHpToStack;
 			for(auto stack : healingTargets)
-				if(auto woundHp = stack->MaxHealth() - stack->getFirstHPleft())
+				if(auto woundHp = stack->getMaxHealth() - stack->getFirstHPleft())
 					woundHpToStack[woundHp] = stack;
 			if(woundHpToStack.empty())
 				return BattleAction::makeDefend(stack);
@@ -137,7 +137,7 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 		std::optional<PossibleSpellcast> bestSpellcast(std::nullopt);
 		//TODO: faerie dragon type spell should be selected by server
 		SpellID creatureSpellToCast = cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED);
-		if(stack->hasBonusOfType(Bonus::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE)
+		if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE)
 		{
 			const CSpell * spell = creatureSpellToCast.toSpell();
 
@@ -214,7 +214,7 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 					bestAttack.attackerState->unitType()->getJsonKey(),
 					bestAttack.affectedUnits[0]->unitType()->getJsonKey(),
 					(int)bestAttack.affectedUnits[0]->getCount(), action, (int)bestAttack.from, (int)bestAttack.attack.attacker->getPosition().hex,
-					bestAttack.attack.chargeDistance, bestAttack.attack.attacker->Speed(0, true),
+					bestAttack.attack.chargeDistance, bestAttack.attack.attacker->speed(0, true),
 					bestAttack.defenderDamageReduce, bestAttack.attackerDamageReduce, bestAttack.attackValue()
 				);
 			}
@@ -241,7 +241,7 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 		}
 
 		if(score <= EvaluationResult::INEFFECTIVE_SCORE
-			&& !stack->hasBonusOfType(Bonus::FLYING)
+			&& !stack->hasBonusOfType(BonusType::FLYING)
 			&& stack->unitSide() == BattleSide::ATTACKER
 			&& cb->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
 		{
@@ -282,7 +282,7 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes) const
 {
 	auto reachability = cb->getReachability(stack);
-	auto avHexes = cb->battleGetAvailableHexes(reachability, stack, true);
+	auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false);
 
 	if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
 	{
@@ -321,7 +321,7 @@ BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vector<Battl
 
 	scoreEvaluator.updateReachabilityMap(hb);
 
-	if(stack->hasBonusOfType(Bonus::FLYING))
+	if(stack->hasBonusOfType(BonusType::FLYING))
 	{
 		std::set<BattleHex> obstacleHexes;
 
@@ -420,7 +420,7 @@ BattleAction CBattleAI::useCatapult(const CStack * stack)
 	attack.aimToHex(targetHex);
 	attack.actionType = EActionType::CATAPULT;
 	attack.side = side;
-	attack.stackNumber = stack->ID;
+	attack.stackNumber = stack->unitId();
 
 	movesSkippedByDefense = 0;
 
@@ -815,7 +815,7 @@ std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
 	{
 		if(stack->alive())
 		{
-			if(stack->side == bs.ourSide)
+			if(stack->unitSide() == bs.ourSide)
 				bs.ourStacks.push_back(stack);
 			else
 			{

+ 1 - 1
AI/BattleAI/BattleAI.h

@@ -31,7 +31,7 @@ struct CurrentOffensivePotential
 	{
 		for(auto stack : cbc->battleGetStacks())
 		{
-			if(stack->side == side)
+			if(stack->unitSide() == side)
 				ourAttacks[stack] = PotentialTargets(stack);
 			else
 				enemyAttacks[stack] = PotentialTargets(stack);

+ 6 - 6
AI/BattleAI/BattleExchangeVariant.cpp

@@ -65,7 +65,7 @@ int64_t BattleExchangeVariant::trackAttack(
 	bool evaluateOnly)
 {
 	const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
-	static const auto selectorBlocksRetaliation = Selector::type()(Bonus::BLOCKS_RETALIATION);
+	static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
 	const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
 
 	DamageEstimation retaliation;
@@ -205,7 +205,7 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Uni
 	if(targets.unreachableEnemies.empty())
 		return result;
 
-	auto speed = activeStack->Speed();
+	auto speed = activeStack->speed();
 
 	if(speed == 0)
 		return result;
@@ -607,7 +607,7 @@ void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb)
 
 			for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
 			{
-				bool reachable = unitReachability.distances[hex] <= unit->Speed(turn);
+				bool reachable = unitReachability.distances[hex] <= unit->speed(turn);
 
 				if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
 				{
@@ -617,7 +617,7 @@ void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb)
 					{
 						for(BattleHex neighbor : hex.neighbouringTiles())
 						{
-							reachable = unitReachability.distances[neighbor] <= unit->Speed(turn);
+							reachable = unitReachability.distances[neighbor] <= unit->speed(turn);
 
 							if(reachable) break;
 						}
@@ -665,7 +665,7 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb
 			for(BattleHex hex = BattleHex::TOP_LEFT; hex.isValid(); hex = hex + 1)
 			{
 				bool enemyUnit = false;
-				bool reachable = unitReachability.distances[hex] <= unit->Speed(turn);
+				bool reachable = unitReachability.distances[hex] <= unit->speed(turn);
 
 				if(!reachable && unitReachability.accessibility[hex] == EAccessibility::ALIVE_STACK)
 				{
@@ -677,7 +677,7 @@ bool BattleExchangeEvaluator::checkPositionBlocksOurStacks(HypotheticBattle & hb
 
 						for(BattleHex neighbor : hex.neighbouringTiles())
 						{
-							reachable = unitReachability.distances[neighbor] <= unit->Speed(turn);
+							reachable = unitReachability.distances[neighbor] <= unit->speed(turn);
 
 							if(reachable) break;
 						}

+ 2 - 8
AI/BattleAI/BattleExchangeVariant.h

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

+ 2 - 2
AI/BattleAI/PotentialTargets.cpp

@@ -15,14 +15,14 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
 {
 	auto attackerInfo = state.battleGetUnitByID(attacker->unitId());
 	auto reachability = state.getReachability(attackerInfo);
-	auto avHexes = state.battleGetAvailableHexes(reachability, attackerInfo, true);
+	auto avHexes = state.battleGetAvailableHexes(reachability, attackerInfo, false);
 
 	//FIXME: this should part of battleGetAvailableHexes
 	bool forceTarget = false;
 	const battle::Unit * forcedTarget = nullptr;
 	BattleHex forcedHex;
 
-	if(attackerInfo->hasBonusOfType(Bonus::ATTACKS_NEAREST_CREATURE))
+	if(attackerInfo->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE))
 	{
 		forceTarget = true;
 		auto nearest = state.getNearestStack(attackerInfo);

+ 4 - 4
AI/BattleAI/StackWithBonuses.cpp

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

+ 2 - 8
AI/BattleAI/StackWithBonuses.h

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

+ 1 - 1
AI/BattleAI/ThreatMap.cpp

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

+ 1 - 1
AI/FuzzyLite

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

+ 3 - 43
AI/Nullkiller/AIGateway.cpp

@@ -256,7 +256,7 @@ void AIGateway::heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance
 	NET_EVENT_HANDLER;
 }
 
-void AIGateway::tileHidden(const std::unordered_set<int3, ShashInt3> & pos)
+void AIGateway::tileHidden(const std::unordered_set<int3> & pos)
 {
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
@@ -264,7 +264,7 @@ void AIGateway::tileHidden(const std::unordered_set<int3, ShashInt3> & pos)
 	nullkiller->memory->removeInvisibleObjects(myCb.get());
 }
 
-void AIGateway::tileRevealed(const std::unordered_set<int3, ShashInt3> & pos)
+void AIGateway::tileRevealed(const std::unordered_set<int3> & pos)
 {
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
@@ -1058,27 +1058,6 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
 	}
 }
 
-bool AIGateway::canRecruitAnyHero(const CGTownInstance * t) const
-{
-	//TODO: make gathering gold, building tavern or conquering town (?) possible subgoals
-	if(!t)
-		t = findTownWithTavern();
-
-	if(!t || !townHasFreeTavern(t))
-		return false;
-
-	if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager
-		return false;
-	if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES)
-		return false;
-	if(cb->getHeroesInfo().size() >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
-		return false;
-	if(!cb->getAvailableHeroes(t).size())
-		return false;
-
-	return true;
-}
-
 void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side)
 {
 	NET_EVENT_HANDLER;
@@ -1160,16 +1139,6 @@ void AIGateway::addVisitableObj(const CGObjectInstance * obj)
 	}
 }
 
-HeroPtr AIGateway::getHeroWithGrail() const
-{
-	for(const CGHeroInstance * h : cb->getHeroesInfo())
-	{
-		if(h->hasArt(ArtifactID::GRAIL))
-			return h;
-	}
-	return nullptr;
-}
-
 bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
 {
 	if(h->inTownGarrison && h->visitedTown)
@@ -1416,7 +1385,7 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
 				//TODO trade only as much as needed
 				if (toGive) //don't try to sell 0 resources
 				{
-					cb->trade(obj, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive);
+					cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive);
 					accquiredResources = static_cast<int>(toGet * (it->resVal / toGive));
 					logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName());
 				}
@@ -1437,15 +1406,6 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
 	}
 }
 
-const CGTownInstance * AIGateway::findTownWithTavern() const
-{
-	for(const CGTownInstance * t : cb->getTownsInfo())
-		if(townHasFreeTavern(t))
-			return t;
-
-	return nullptr;
-}
-
 void AIGateway::endTurn()
 {
 	logAi->info("Player %d (%s) ends turn", playerID, playerID.getStr());

+ 2 - 7
AI/Nullkiller/AIGateway.h

@@ -127,7 +127,7 @@ public:
 	void heroMoved(const TryMoveHero & details, bool verbose = true) override;
 	void heroInGarrisonChange(const CGTownInstance * town) override;
 	void centerView(int3 pos, int focusTime) override;
-	void tileHidden(const std::unordered_set<int3, ShashInt3> & pos) override;
+	void tileHidden(const std::unordered_set<int3> & pos) override;
 	void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override;
 	void artifactAssembled(const ArtifactLocation & al) override;
 	void showTavernWindow(const CGObjectInstance * townOrTavern) override;
@@ -142,7 +142,7 @@ public:
 	void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override;
 	void availableArtifactsChanged(const CGBlackMarket * bm = nullptr) override;
 	void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override;
-	void tileRevealed(const std::unordered_set<int3, ShashInt3> & pos) override;
+	void tileRevealed(const std::unordered_set<int3> & pos) override;
 	void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override;
 	void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override;
 	void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override;
@@ -198,11 +198,6 @@ public:
 	void retrieveVisitableObjs();
 	virtual std::vector<const CGObjectInstance *> getFlaggedObjects() const;
 
-	HeroPtr getHeroWithGrail() const;
-
-	const CGTownInstance * findTownWithTavern() const;
-	bool canRecruitAnyHero(const CGTownInstance * t = NULL) const;
-
 	void requestSent(const CPackForServer * pack, int requestID) override;
 	void answerQuery(QueryID queryID, int selection);
 	//special function that can be called ONLY from game events handling thread and will send request ASAP

+ 2 - 2
AI/Nullkiller/AIUtility.cpp

@@ -307,7 +307,7 @@ bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2
 	auto art2 = a2->artType;
 
 	if(art1->price == art2->price)
-		return art1->valOfBonuses(Bonus::PRIMARY_SKILL) > art2->valOfBonuses(Bonus::PRIMARY_SKILL);
+		return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);
 	else
 		return art1->price > art2->price;
 }
@@ -319,7 +319,7 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj)
 
 	//TODO: allow polling of remaining creatures in dwelling
 	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj))
-		return rewardable->getResetDuration() == 7;
+		return rewardable->configuration.getResetDuration() == 7;
 
 	if(dynamic_cast<const CGDwelling *>(obj))
 		return true;

+ 4 - 4
AI/Nullkiller/AIUtility.h

@@ -54,7 +54,7 @@
 
 using namespace tbb;
 
-typedef std::pair<ui32, std::vector<CreatureID>> dwellingContent;
+using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
 
 namespace NKAI
 {
@@ -305,10 +305,10 @@ public:
 public:
 	using ptr_type = std::unique_ptr<T, External_Deleter>;
 
-	SharedPool(std::function<std::unique_ptr<T>()> elementFactory)
-		: elementFactory(elementFactory), pool(), sync(), instance_tracker(new SharedPool<T>*(this))
+	SharedPool(std::function<std::unique_ptr<T>()> elementFactory):
+		elementFactory(elementFactory), pool(), instance_tracker(new SharedPool<T> *(this))
 	{}
-	
+
 	void add(std::unique_ptr<T> t)
 	{
 		boost::lock_guard<boost::mutex> lock(sync);

+ 4 - 4
AI/Nullkiller/Analyzers/ArmyManager.cpp

@@ -77,7 +77,7 @@ std::vector<SlotInfo>::iterator ArmyManager::getWeakestCreature(std::vector<Slot
 		if(left.creature->getLevel() != right.creature->getLevel())
 			return left.creature->getLevel() < right.creature->getLevel();
 		
-		return left.creature->Speed() > right.creature->Speed();
+		return left.creature->speed() > right.creature->speed();
 	});
 
 	return weakest;
@@ -108,12 +108,12 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 	uint64_t armyValue = 0;
 
 	TemporaryArmy newArmyInstance;
-	auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(Bonus::MORALE));
+	auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE));
 
 	for(auto bonus : *bonusModifiers)
 	{
 		// army bonuses will change and object bonuses are temporary
-		if(bonus->source != Bonus::ARMY && bonus->source != Bonus::OBJECT)
+		if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT)
 		{
 			newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
 		}
@@ -150,7 +150,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
 
 		for(auto & slot : newArmyInstance.Slots())
 		{
-			auto morale = slot.second->MoraleVal();
+			auto morale = slot.second->moraleVal();
 			auto multiplier = 1.0f;
 
 			const float BadMoraleChance = 0.083f;

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

@@ -32,13 +32,8 @@ struct SlotInfo
 struct ArmyUpgradeInfo
 {
 	std::vector<SlotInfo> resultingArmy;
-	uint64_t upgradeValue;
+	uint64_t upgradeValue = 0;
 	TResources upgradeCost;
-
-	ArmyUpgradeInfo()
-		: resultingArmy(), upgradeValue(0), upgradeCost()
-	{
-	}
 };
 
 class DLL_EXPORT IArmyManager //: public: IAbstractManager

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

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

+ 50 - 3
AI/Nullkiller/Analyzers/HeroManager.cpp

@@ -12,6 +12,8 @@
 #include "../Engine/Nullkiller.h"
 #include "../../../lib/mapObjects/MapObjects.h"
 #include "../../../lib/CHeroHandler.h"
+#include "../../../lib/GameSettings.h"
+#include "../../../lib/CGameState.h"
 
 namespace NKAI
 {
@@ -70,10 +72,10 @@ float HeroManager::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance *
 
 float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
 {
-	auto heroSpecial = Selector::source(Bonus::HERO_SPECIAL, hero->type->getIndex());
-	auto secondarySkillBonus = Selector::targetSourceType()(Bonus::SECONDARY_SKILL);
+	auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, hero->type->getIndex());
+	auto secondarySkillBonus = Selector::targetSourceType()(BonusSource::SECONDARY_SKILL);
 	auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus));
-	auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(Bonus::SECONDARY_SKILL));
+	auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(BonusSource::SECONDARY_SKILL));
 	float specialityScore = 0.0f;
 
 	for(auto bonus : *secondarySkillBonuses)
@@ -179,6 +181,51 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
 	return evaluateFightingStrength(hero);
 }
 
+bool HeroManager::canRecruitHero(const CGTownInstance * town) const
+{
+	if(!town)
+		town = findTownWithTavern();
+
+	if(!town || !townHasFreeTavern(town))
+		return false;
+
+	if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST)
+		return false;
+
+	const bool includeGarnisoned = true;
+	int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
+
+	if(heroCount >= ALLOWED_ROAMING_HEROES)
+		return false;
+
+	if(heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
+		return false;
+
+	if(!cb->getAvailableHeroes(town).size())
+		return false;
+
+	return true;
+}
+
+const CGTownInstance * HeroManager::findTownWithTavern() const
+{
+	for(const CGTownInstance * t : cb->getTownsInfo())
+		if(townHasFreeTavern(t))
+			return t;
+
+	return nullptr;
+}
+
+const CGHeroInstance * HeroManager::findHeroWithGrail() const
+{
+	for(const CGHeroInstance * h : cb->getHeroesInfo())
+	{
+		if(h->hasArt(ArtifactID::GRAIL))
+			return h;
+	}
+	return nullptr;
+}
+
 SecondarySkillScoreMap::SecondarySkillScoreMap(std::map<SecondarySkill, float> scoreMap)
 	:scoreMap(scoreMap)
 {

+ 7 - 1
AI/Nullkiller/Analyzers/HeroManager.h

@@ -30,6 +30,8 @@ public:
 	virtual void update() = 0;
 	virtual float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const = 0;
 	virtual float evaluateHero(const CGHeroInstance * hero) const = 0;
+	virtual bool canRecruitHero(const CGTownInstance * t = nullptr) const = 0;
+	virtual const CGHeroInstance * findHeroWithGrail() const = 0;
 };
 
 class DLL_EXPORT ISecondarySkillRule
@@ -57,20 +59,24 @@ private:
 	static SecondarySkillEvaluator scountSkillsScores;
 
 	CCallback * cb; //this is enough, but we downcast from CCallback
+	const Nullkiller * ai;
 	std::map<HeroPtr, HeroRole> heroRoles;
 
 public:
-	HeroManager(CCallback * CB, const Nullkiller * ai) : cb(CB) {}
+	HeroManager(CCallback * CB, const Nullkiller * ai) : cb(CB), ai(ai) {}
 	const std::map<HeroPtr, HeroRole> & getHeroRoles() const override;
 	HeroRole getHeroRole(const HeroPtr & hero) const override;
 	int selectBestSkill(const HeroPtr & hero, const std::vector<SecondarySkill> & skills) const override;
 	void update() override;
 	float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const override;
 	float evaluateHero(const CGHeroInstance * hero) const override;
+	bool canRecruitHero(const CGTownInstance * t = nullptr) const override;
+	const CGHeroInstance * findHeroWithGrail() const override;
 
 private:
 	float evaluateFightingStrength(const CGHeroInstance * hero) const;
 	float evaluateSpeciality(const CGHeroInstance * hero) const;
+	const CGTownInstance * findTownWithTavern() const;
 };
 
 // basic skill scores. missing skills will have score of 0

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

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

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

@@ -209,7 +209,7 @@ Goals::TGoalVec CaptureObjectsBehavior::decompose() const
 	{
 		captureObjects(ai->nullkiller->objectClusterizer->getNearbyObjects());
 
-		if(tasks.empty())
+		if(tasks.empty() || ai->nullkiller->getScanDepth() == ScanDepth::FULL)
 			captureObjects(ai->nullkiller->objectClusterizer->getFarObjects());
 	}
 

+ 7 - 7
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

@@ -58,13 +58,6 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 	auto treatNode = ai->nullkiller->dangerHitMap->getObjectTreat(town);
 	auto treats = { treatNode.maximumDanger, treatNode.fastestDanger };
 
-	if(!treatNode.fastestDanger.hero)
-	{
-		logAi->trace("No treat found for town %s", town->getNameTranslated());
-
-		return;
-	}
-
 	int dayOfWeek = cb->getDate(Date::DAY_OF_WEEK);
 
 	if(town->garrisonHero)
@@ -91,6 +84,13 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
 
 		return;
 	}
+
+	if(!treatNode.fastestDanger.hero)
+	{
+		logAi->trace("No treat found for town %s", town->getNameTranslated());
+
+		return;
+	}
 	
 	uint64_t reinforcement = ai->nullkiller->armyManager->howManyReinforcementsCanBuy(town->getUpperArmy(), town);
 

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

@@ -53,7 +53,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const
 
 	for(auto town : towns)
 	{
-		if(ai->canRecruitAnyHero(town))
+		if(ai->nullkiller->heroManager->canRecruitHero(town))
 		{
 			auto availableHeroes = cb->getAvailableHeroes(town);
 

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

@@ -66,7 +66,7 @@ const CGHeroInstance * getNearestHero(const CGTownInstance * town)
 
 bool needToRecruitHero(const CGTownInstance * startupTown)
 {
-	if(!ai->canRecruitAnyHero(startupTown))
+	if(!ai->nullkiller->heroManager->canRecruitHero(startupTown))
 		return false;
 
 	if(!startupTown->garrisonHero && !startupTown->visitingHero)

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

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

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

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

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

@@ -53,14 +53,14 @@ armyStructure evaluateArmyStructure(const CArmedInstance * army)
 	double shootersStrength = 0;
 	ui32 maxSpeed = 0;
 
-	static const CSelector selectorSHOOTER = Selector::type()(Bonus::SHOOTER);
-	static const std::string keySHOOTER = "type_"+std::to_string((int32_t)Bonus::SHOOTER);
+	static const CSelector selectorSHOOTER = Selector::type()(BonusType::SHOOTER);
+	static const std::string keySHOOTER = "type_"+std::to_string((int32_t)BonusType::SHOOTER);
 
-	static const CSelector selectorFLYING = Selector::type()(Bonus::FLYING);
-	static const std::string keyFLYING = "type_"+std::to_string((int32_t)Bonus::FLYING);
+	static const CSelector selectorFLYING = Selector::type()(BonusType::FLYING);
+	static const std::string keyFLYING = "type_"+std::to_string((int32_t)BonusType::FLYING);
 
-	static const CSelector selectorSTACKS_SPEED = Selector::type()(Bonus::STACKS_SPEED);
-	static const std::string keySTACKS_SPEED = "type_"+std::to_string((int32_t)Bonus::STACKS_SPEED);
+	static const CSelector selectorSTACKS_SPEED = Selector::type()(BonusType::STACKS_SPEED);
+	static const std::string keySTACKS_SPEED = "type_"+std::to_string((int32_t)BonusType::STACKS_SPEED);
 
 	for(auto s : army->Slots())
 	{

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

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

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

@@ -117,7 +117,7 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TSubgoal behavior, int decompositi
 void Nullkiller::resetAiState()
 {
 	lockedResources = TResources();
-	scanDepth = ScanDepth::SMALL;
+	scanDepth = ScanDepth::FULL;
 	playerID = ai->playerID;
 	lockedHeroes.clear();
 	dangerHitMap->reset();

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

@@ -88,6 +88,7 @@ public:
 	int32_t getFreeGold() const { return getFreeResources()[EGameResID::GOLD]; }
 	void lockResources(const TResources & res);
 	const TResources & getLockedResources() const { return lockedResources; }
+	ScanDepth getScanDepth() const { return scanDepth; }
 
 private:
 	void resetAiState();

+ 14 - 10
AI/Nullkiller/Engine/PriorityEvaluator.cpp

@@ -210,14 +210,14 @@ uint64_t evaluateArtifactArmyValue(CArtifactInstance * art)
 		return 1500;
 
 	auto statsValue =
-		10 * art->valOfBonuses(Bonus::MOVEMENT, 1)
-		+ 1200 * art->valOfBonuses(Bonus::STACKS_SPEED)
-		+ 700 * art->valOfBonuses(Bonus::MORALE)
-		+ 700 * art->getAttack(false)
-		+ 700 * art->getDefense(false)
-		+ 700 * art->valOfBonuses(Bonus::PRIMARY_SKILL, PrimarySkill::KNOWLEDGE)
-		+ 700 * art->valOfBonuses(Bonus::PRIMARY_SKILL, PrimarySkill::SPELL_POWER)
-		+ 500 * art->valOfBonuses(Bonus::LUCK);
+		10 * art->valOfBonuses(BonusType::MOVEMENT, 1)
+		+ 1200 * art->valOfBonuses(BonusType::STACKS_SPEED)
+		+ 700 * art->valOfBonuses(BonusType::MORALE)
+		+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK)
+		+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE)
+		+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::KNOWLEDGE)
+		+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::SPELL_POWER)
+		+ 500 * art->valOfBonuses(BonusType::LUCK);
 
 	auto classValue = 0;
 
@@ -297,6 +297,12 @@ int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroIn
 {
 	if(!target)
 		return 0;
+	
+	if(auto * m = dynamic_cast<const IMarket *>(target))
+	{
+		if(m->allowsTrade(EMarketMode::RESOURCE_SKILL))
+			return 2000;
+	}
 
 	switch(target->ID)
 	{
@@ -305,8 +311,6 @@ int RewardEvaluator::getGoldCost(const CGObjectInstance * target, const CGHeroIn
 	case Obj::SCHOOL_OF_MAGIC:
 	case Obj::SCHOOL_OF_WAR:
 		return 1000;
-	case Obj::UNIVERSITY:
-		return 2000;
 	case Obj::CREATURE_GENERATOR1:
 	case Obj::CREATURE_GENERATOR2:
 	case Obj::CREATURE_GENERATOR3:

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

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

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

@@ -33,8 +33,6 @@ void RecruitHero::accept(AIGateway * ai)
 {
 	auto t = town;
 
-	if(!t) t = ai->findTownWithTavern();
-
 	if(!t)
 	{
 		throw cannotFulfillGoalException("No town to recruit hero!");

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

@@ -1093,7 +1093,7 @@ void AINodeStorage::calculateTownPortal(
 			if(nodeOptional)
 			{
 #if NKAI_PATHFINDER_TRACE_LEVEL >= 1
-				logAi->trace("Adding town portal node at %s", targetTown->name);
+				logAi->trace("Adding town portal node at %s", targetTown->getObjectName());
 #endif
 				output.push_back(nodeOptional.value());
 			}

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

@@ -205,14 +205,14 @@ public:
 
 	inline void updateAINode(CGPathNode * node, std::function<void (AIPathNode *)> updater)
 	{
-		auto aiNode = static_cast<AIPathNode *>(node);
+		auto * aiNode = static_cast<AIPathNode *>(node);
 
 		updater(aiNode);
 	}
 
 	inline const CGHeroInstance * getHero(const CGPathNode * node) const
 	{
-		auto aiNode = getAINode(node);
+		const auto * aiNode = getAINode(node);
 
 		return aiNode->actor->hero;
 	}

+ 1 - 3
AI/Nullkiller/Pathfinding/Actors.h

@@ -32,9 +32,7 @@ public:
 	virtual bool needsLastStack() const override;
 	std::shared_ptr<SpecialAction> getActorAction() const;
 
-	HeroExchangeArmy() : CArmedInstance(true), armyCost(), requireBuyArmy(false)
-	{
-	}
+	HeroExchangeArmy(): CArmedInstance(true), requireBuyArmy(false) {}
 };
 
 struct ExchangeResult

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

@@ -53,15 +53,13 @@ namespace AIPathfinding
 
 		for(const CGTownInstance * t : cb->getTownsInfo())
 		{
-			// do not allow ally shipyards because of bug
-			if(t->hasBuilt(BuildingID::SHIPYARD) && t->getOwner() == ai->playerID)
+			if(t->hasBuilt(BuildingID::SHIPYARD))
 				shipyards.push_back(t);
 		}
 
 		for(const CGObjectInstance * obj : ai->memory->visitableObjs)
 		{
-			// do not allow ally shipyards because of bug
-			if(obj->ID != Obj::TOWN && obj->getOwner() == ai->playerID) //towns were handled in the previous loop
+			if(obj->ID != Obj::TOWN) //towns were handled in the previous loop
 			{
 				if(const IShipyard * shipyard = IShipyard::castFrom(obj))
 					shipyards.push_back(shipyard);

+ 5 - 5
AI/StupidAI/StupidAI.cpp

@@ -95,7 +95,7 @@ BattleAction CStupidAI::activeStack( const CStack * stack )
 	ReachabilityInfo dists = cb->getReachability(stack);
 	std::vector<EnemyInfo> enemiesShootable, enemiesReachable, enemiesUnreachable;
 
-	if(stack->type->getId() == CreatureID::CATAPULT)
+	if(stack->creatureId() == CreatureID::CATAPULT)
 	{
 		BattleAction attack;
 		static const std::vector<int> wallHexes = {50, 183, 182, 130, 78, 29, 12, 95};
@@ -103,11 +103,11 @@ BattleAction CStupidAI::activeStack( const CStack * stack )
 		attack.aimToHex(seletectedHex);
 		attack.actionType = EActionType::CATAPULT;
 		attack.side = side;
-		attack.stackNumber = stack->ID;
+		attack.stackNumber = stack->unitId();
 
 		return attack;
 	}
-	else if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON))
+	else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON))
 	{
 		return BattleAction::makeDefend(stack);
 	}
@@ -120,7 +120,7 @@ BattleAction CStupidAI::activeStack( const CStack * stack )
 		}
 		else
 		{
-			std::vector<BattleHex> avHexes = cb->battleGetAvailableHexes(stack, true);
+			std::vector<BattleHex> avHexes = cb->battleGetAvailableHexes(stack, false);
 
 			for (BattleHex hex : avHexes)
 			{
@@ -270,7 +270,7 @@ BattleAction CStupidAI::goTowards(const CStack * stack, std::vector<BattleHex> h
 		return BattleAction::makeDefend(stack);
 	}
 
-	if(stack->hasBonusOfType(Bonus::FLYING))
+	if(stack->hasBonusOfType(BonusType::FLYING))
 	{
 		// Flying stack doesn't go hex by hex, so we can't backtrack using predecessors.
 		// We just check all available hexes and pick the one closest to the target.

+ 1 - 1
AI/VCAI/AIUtility.cpp

@@ -257,7 +257,7 @@ bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2
 	auto art2 = a2->artType;
 
 	if(art1->price == art2->price)
-		return art1->valOfBonuses(Bonus::PRIMARY_SKILL) > art2->valOfBonuses(Bonus::PRIMARY_SKILL);
+		return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);
 	else
 		return art1->price > art2->price;
 }

+ 3 - 3
AI/VCAI/AIUtility.h

@@ -23,9 +23,9 @@
 class CCallback;
 struct creInfo;
 
-typedef const int3 & crint3;
-typedef const std::string & crstring;
-typedef std::pair<ui32, std::vector<CreatureID>> dwellingContent;
+using crint3 = const int3 &;
+using crstring = const std::string &;
+using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
 
 const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1;
 const int ACTUAL_RESOURCE_COUNT = 7;

+ 1 - 1
AI/VCAI/ArmyManager.cpp

@@ -63,7 +63,7 @@ std::vector<SlotInfo>::iterator ArmyManager::getWeakestCreature(std::vector<Slot
 		if(left.creature->getLevel() != right.creature->getLevel())
 			return left.creature->getLevel() < right.creature->getLevel();
 		
-		return left.creature->Speed() > right.creature->Speed();
+		return left.creature->speed() > right.creature->speed();
 	});
 
 	return weakest;

+ 6 - 6
AI/VCAI/FuzzyEngines.cpp

@@ -52,14 +52,14 @@ armyStructure evaluateArmyStructure(const CArmedInstance * army)
 	double shootersStrength = 0;
 	ui32 maxSpeed = 0;
 
-	static const CSelector selectorSHOOTER = Selector::type()(Bonus::SHOOTER);
-	static const std::string keySHOOTER = "type_"+std::to_string((int32_t)Bonus::SHOOTER);
+	static const CSelector selectorSHOOTER = Selector::type()(BonusType::SHOOTER);
+	static const std::string keySHOOTER = "type_"+std::to_string((int32_t)BonusType::SHOOTER);
 
-	static const CSelector selectorFLYING = Selector::type()(Bonus::FLYING);
-	static const std::string keyFLYING = "type_"+std::to_string((int32_t)Bonus::FLYING);
+	static const CSelector selectorFLYING = Selector::type()(BonusType::FLYING);
+	static const std::string keyFLYING = "type_"+std::to_string((int32_t)BonusType::FLYING);
 
-	static const CSelector selectorSTACKS_SPEED = Selector::type()(Bonus::STACKS_SPEED);
-	static const std::string keySTACKS_SPEED = "type_"+std::to_string((int32_t)Bonus::STACKS_SPEED);
+	static const CSelector selectorSTACKS_SPEED = Selector::type()(BonusType::STACKS_SPEED);
+	static const std::string keySTACKS_SPEED = "type_"+std::to_string((int32_t)BonusType::STACKS_SPEED);
 
 	for(auto s : army->Slots())
 	{

+ 2 - 3
AI/VCAI/Goals/AbstractGoal.h

@@ -76,7 +76,7 @@ namespace Goals
 		//TODO: serialize?
 	};
 
-	typedef std::vector<TSubgoal> TGoalVec;
+	using TGoalVec = std::vector<TSubgoal>;
 
 	//method chaining + clone pattern
 #define VSETTER(type, field) virtual AbstractGoal & set ## field(const type &rhs) {field = rhs; return *this;};
@@ -121,8 +121,7 @@ namespace Goals
 			TSubgoal parent; VSETTER(TSubgoal, parent)
 			EvaluationContext evaluationContext; VSETTER(EvaluationContext, evaluationContext)
 
-			AbstractGoal(EGoals goal = EGoals::INVALID)
-			: goalType(goal), evaluationContext()
+		AbstractGoal(EGoals goal = EGoals::INVALID): goalType(goal)
 		{
 			priority = 0;
 			isElementar = false;

+ 15 - 10
AI/VCAI/Goals/CollectRes.cpp

@@ -131,13 +131,16 @@ TSubgoal CollectRes::whatToDoToTrade()
 
 	std::vector<const CGObjectInstance *> visObjs;
 	ai->retrieveVisitableObjs(visObjs, true);
-	for (const CGObjectInstance * obj : visObjs)
+	for(const CGObjectInstance * obj : visObjs)
 	{
-		if (const IMarket * m = IMarket::castFrom(obj, false))
+		if(const IMarket * m = IMarket::castFrom(obj, false); m->allowsTrade(EMarketMode::RESOURCE_RESOURCE))
 		{
-			if (obj->ID == Obj::TOWN && obj->tempOwner == ai->playerID && m->allowsTrade(EMarketMode::RESOURCE_RESOURCE))
-				markets.push_back(m);
-			else if (obj->ID == Obj::TRADING_POST)
+			if(obj->ID == Obj::TOWN)
+			{
+				if(obj->tempOwner == ai->playerID)
+					markets.push_back(m);
+			}
+			else
 				markets.push_back(m);
 		}
 	}
@@ -149,9 +152,10 @@ TSubgoal CollectRes::whatToDoToTrade()
 
 	markets.erase(boost::remove_if(markets, [](const IMarket * market) -> bool
 	{
-		if (!(market->o->ID == Obj::TOWN && market->o->tempOwner == ai->playerID))
+		auto * o = dynamic_cast<const CGObjectInstance *>(market);
+		if(o && !(o->ID == Obj::TOWN && o->tempOwner == ai->playerID))
 		{
-			if (!ai->isAccessible(market->o->visitablePos()))
+			if(!ai->isAccessible(o->visitablePos()))
 				return true;
 		}
 		return false;
@@ -170,7 +174,7 @@ TSubgoal CollectRes::whatToDoToTrade()
 		const IMarket * m = markets.back();
 		//attempt trade at back (best prices)
 		int howManyCanWeBuy = 0;
-		for (auto i = EGameResID::WOOD; i <= EGameResID::GOLD; vstd::advance(i, 1))
+		for (GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i)
 		{
 			if (GameResID(i) == resID)
 				continue;
@@ -182,9 +186,10 @@ TSubgoal CollectRes::whatToDoToTrade()
 
 		if (howManyCanWeBuy >= value)
 		{
-			auto backObj = cb->getTopObj(m->o->visitablePos()); //it'll be a hero if we have one there; otherwise marketplace
+			auto * o = dynamic_cast<const CGObjectInstance *>(m);
+			auto backObj = cb->getTopObj(o->visitablePos()); //it'll be a hero if we have one there; otherwise marketplace
 			assert(backObj);
-			auto objid = m->o->id.getNum();
+			auto objid = o->id.getNum();
 			if (backObj->tempOwner != ai->playerID) //top object not owned
 			{
 				return sptr(VisitObj(objid)); //just go there

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

@@ -19,8 +19,6 @@
 
 class CCallback;
 
-extern boost::thread_specific_ptr<CCallback> cb; //for templates
-
 struct AIPathNode : public CGPathNode
 {
 	uint32_t chainMask;

+ 10 - 6
AI/VCAI/VCAI.cpp

@@ -22,6 +22,10 @@
 #include "../../lib/CGameState.h"
 #include "../../lib/NetPacksBase.h"
 #include "../../lib/NetPacks.h"
+#include "../../lib/bonuses/CBonusSystemNode.h"
+#include "../../lib/bonuses/Limiters.h"
+#include "../../lib/bonuses/Updaters.h"
+#include "../../lib/bonuses/Propagators.h"
 #include "../../lib/serializer/CTypeList.h"
 #include "../../lib/serializer/BinarySerializer.h"
 #include "../../lib/serializer/BinaryDeserializer.h"
@@ -268,7 +272,7 @@ void VCAI::heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * to
 	//moveCreaturesToHero(town);
 }
 
-void VCAI::tileHidden(const std::unordered_set<int3, ShashInt3> & pos)
+void VCAI::tileHidden(const std::unordered_set<int3> & pos)
 {
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
@@ -277,7 +281,7 @@ void VCAI::tileHidden(const std::unordered_set<int3, ShashInt3> & pos)
 	clearPathsInfo();
 }
 
-void VCAI::tileRevealed(const std::unordered_set<int3, ShashInt3> & pos)
+void VCAI::tileRevealed(const std::unordered_set<int3> & pos)
 {
 	LOG_TRACE(logAi);
 	NET_EVENT_HANDLER;
@@ -1605,10 +1609,10 @@ void VCAI::markObjectVisited(const CGObjectInstance * obj)
 
 	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj)) //we may want to visit it with another hero
 	{
-		if (rewardable->getVisitMode() == CRewardableObject::VISIT_HERO) //we may want to visit it with another hero
+		if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_HERO) //we may want to visit it with another hero
 			return;
 
-		if (rewardable->getVisitMode() == CRewardableObject::VISIT_BONUS) //or another time
+		if (rewardable->configuration.getVisitMode() == Rewardable::VISIT_BONUS) //or another time
 			return;
 	}
 
@@ -2130,7 +2134,7 @@ void VCAI::tryRealize(Goals::Trade & g) //trade
 				//TODO trade only as much as needed
 				if (toGive) //don't try to sell 0 resources
 				{
-					cb->trade(obj, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive);
+					cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, g.resID, toGive);
 					accquiredResources = static_cast<int>(toGet * (it->resVal / toGive));
 					logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName());
 				}
@@ -2746,7 +2750,7 @@ bool isWeeklyRevisitable(const CGObjectInstance * obj)
 {
 	//TODO: allow polling of remaining creatures in dwelling
 	if(const auto * rewardable = dynamic_cast<const CRewardableObject *>(obj))
-		return rewardable->getResetDuration() == 7;
+		return rewardable->configuration.getResetDuration() == 7;
 
 	if(dynamic_cast<const CGDwelling *>(obj))
 		return true;

+ 2 - 2
AI/VCAI/VCAI.h

@@ -160,7 +160,7 @@ public:
 	void heroMoved(const TryMoveHero & details, bool verbose = true) override;
 	void heroInGarrisonChange(const CGTownInstance * town) override;
 	void centerView(int3 pos, int focusTime) override;
-	void tileHidden(const std::unordered_set<int3, ShashInt3> & pos) override;
+	void tileHidden(const std::unordered_set<int3> & pos) override;
 	void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override;
 	void artifactAssembled(const ArtifactLocation & al) override;
 	void showTavernWindow(const CGObjectInstance * townOrTavern) override;
@@ -175,7 +175,7 @@ public:
 	void heroVisit(const CGHeroInstance * visitor, const CGObjectInstance * visitedObj, bool start) override;
 	void availableArtifactsChanged(const CGBlackMarket * bm = nullptr) override;
 	void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override;
-	void tileRevealed(const std::unordered_set<int3, ShashInt3> & pos) override;
+	void tileRevealed(const std::unordered_set<int3> & pos) override;
 	void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override;
 	void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override;
 	void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override;

+ 3 - 3
CCallback.cpp

@@ -244,15 +244,15 @@ void CCallback::buyArtifact(const CGHeroInstance *hero, ArtifactID aid)
 	sendRequest(&pack);
 }
 
-void CCallback::trade(const CGObjectInstance * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero)
+void CCallback::trade(const IMarket * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero)
 {
 	trade(market, mode, std::vector<ui32>(1, id1), std::vector<ui32>(1, id2), std::vector<ui32>(1, val1), hero);
 }
 
-void CCallback::trade(const CGObjectInstance * market, EMarketMode::EMarketMode mode, const std::vector<ui32> & id1, const std::vector<ui32> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero)
+void CCallback::trade(const IMarket * market, EMarketMode::EMarketMode mode, const std::vector<ui32> & id1, const std::vector<ui32> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero)
 {
 	TradeOnMarketplace pack;
-	pack.marketId = market->id;
+	pack.marketId = dynamic_cast<const CGObjectInstance *>(market)->id;
 	pack.heroId = hero ? hero->id : ObjectInstanceID();
 	pack.mode = mode;
 	pack.r1 = id1;

+ 5 - 4
CCallback.h

@@ -33,6 +33,7 @@ class IBattleEventsReceiver;
 class IGameEventsReceiver;
 struct ArtifactLocation;
 class BattleStateInfoForRetreat;
+class IMarket;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -73,8 +74,8 @@ public:
 	virtual bool upgradeCreature(const CArmedInstance *obj, SlotID stackPos, CreatureID newID=CreatureID::NONE)=0; //if newID==-1 then best possible upgrade will be made
 	virtual void swapGarrisonHero(const CGTownInstance *town)=0;
 
-	virtual void trade(const CGObjectInstance * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce
-	virtual void trade(const CGObjectInstance * market, EMarketMode::EMarketMode mode, const std::vector<ui32> & id1, const std::vector<ui32> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr)=0;
+	virtual void trade(const IMarket * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr)=0; //mode==0: sell val1 units of id1 resource for id2 resiurce
+	virtual void trade(const IMarket * market, EMarketMode::EMarketMode mode, const std::vector<ui32> & id1, const std::vector<ui32> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr)=0;
 
 	virtual int selectionMade(int selection, QueryID queryID) =0;
 	virtual int sendQueryReply(const JsonNode & reply, QueryID queryID) =0;
@@ -168,8 +169,8 @@ public:
 	void endTurn() override;
 	void swapGarrisonHero(const CGTownInstance *town) override;
 	void buyArtifact(const CGHeroInstance *hero, ArtifactID aid) override;
-	void trade(const CGObjectInstance * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr) override;
-	void trade(const CGObjectInstance * market, EMarketMode::EMarketMode mode, const std::vector<ui32> & id1, const std::vector<ui32> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr) override;
+	void trade(const IMarket * market, EMarketMode::EMarketMode mode, ui32 id1, ui32 id2, ui32 val1, const CGHeroInstance * hero = nullptr) override;
+	void trade(const IMarket * market, EMarketMode::EMarketMode mode, const std::vector<ui32> & id1, const std::vector<ui32> & id2, const std::vector<ui32> & val1, const CGHeroInstance * hero = nullptr) override;
 	void setFormation(const CGHeroInstance * hero, bool tight) override;
 	void recruitHero(const CGObjectInstance *townOrTavern, const CGHeroInstance *hero) override;
 	void save(const std::string &fname) override;

BIN
CI/android/android-release.jks


+ 0 - 0
CI/android/signing.properties → CI/android/dailySigning.properties


+ 2 - 0
CI/android/releaseSigning.properties

@@ -0,0 +1,2 @@
+STORE_FILE=android-release.jks
+KEY_ALIAS=vcmi

+ 1 - 0
CMakeLists.txt

@@ -251,6 +251,7 @@ if(MINGW OR MSVC)
 		add_definitions(-D_CRT_SECURE_NO_WARNINGS)
 		add_definitions(-D_SCL_SECURE_NO_WARNINGS)
 
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /utf-8")
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /bigobj")
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4250") # 4250: 'class1' : inherits 'class2::member' via dominance
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /wd4251") # 4251: class 'xxx' needs to have dll-interface to be used by clients of class 'yyy'

+ 51 - 0
CMakePresets.json

@@ -45,6 +45,15 @@
                 "CMAKE_INSTALL_PREFIX" : "/usr/local"
             }
         },
+        {
+            "name": "linux-test",
+            "inherits": "linux-release",
+            "hidden": true,
+            "cacheVariables": {
+                "ENABLE_TEST": "ON",
+                "ENABLE_LUA": "ON"
+            }
+        },
         {
             "name": "linux-clang-release",
             "displayName": "Clang x86_64-pc-linux-gnu",
@@ -67,6 +76,28 @@
                 "CMAKE_CXX_COMPILER": "/usr/bin/g++"
             }
         },
+        {
+            "name": "linux-clang-test",
+            "displayName": "Clang x86_64-pc-linux-gnu with unit testing",
+            "description": "VCMI Linux Clang",
+            "inherits": "linux-test",
+            "cacheVariables": {
+                "CMAKE_C_COMPILER": "/usr/bin/clang",
+                "CMAKE_CXX_COMPILER": "/usr/bin/clang++"
+            }
+        },
+        {
+            "name": "linux-gcc-test",
+            "displayName": "GCC x86_64-pc-linux-gnu with unit testing",
+            "description": "VCMI Linux GCC",
+            "inherits": "linux-test",
+            "cacheVariables": {
+                "ENABLE_LUA" : "OFF",
+                "ENABLE_PCH" : "OFF",
+                "CMAKE_C_COMPILER": "/usr/bin/gcc",
+                "CMAKE_CXX_COMPILER": "/usr/bin/g++"
+            }
+        },
         {
             "name": "windows-msvc-release",
             "displayName": "Windows x64 RelWithDebInfo",
@@ -213,6 +244,16 @@
             "configurePreset": "linux-clang-release",
             "inherits": "default-release"
         },
+        {
+            "name": "linux-clang-test",
+            "configurePreset": "linux-clang-test",
+            "inherits": "default-release"
+        },
+        {
+            "name": "linux-gcc-test",
+            "configurePreset": "linux-gcc-test",
+            "inherits": "default-release"
+        },
         {
             "name": "linux-gcc-release",
             "configurePreset": "linux-gcc-release",
@@ -295,6 +336,16 @@
             "configurePreset": "linux-gcc-release",
             "inherits": "default-release"
         },
+        {
+            "name": "linux-clang-test",
+            "configurePreset": "linux-clang-test",
+            "inherits": "default-release"
+        },
+        {
+            "name": "linux-gcc-test",
+            "configurePreset": "linux-gcc-test",
+            "inherits": "default-release"
+        },
         {
             "name": "macos-xcode-release",
             "configurePreset": "macos-xcode-release",

+ 24 - 1
ChangeLog.md

@@ -1,6 +1,29 @@
-# 1.2.0 -> 1.3.0
+# 1.2.1 -> 1.3.0
 (unreleased)
 
+# 1.2.0 -> 1.2.1
+
+### GENERAL:
+* Implemented spell range overlay for Dimension Door and Scuttle Boat
+* Fixed movement cost penalty from terrain
+* Fixed empty Black Market on game start
+* Fixed bad morale happening after waiting
+* Fixed good morale happening after defeating last enemy unit
+* Fixed death animation of Efreeti killed by petrification attack
+* Fixed crash on leaving to main menu from battle in hotseat mode
+* Fixed music playback on switching between towns
+* Special months (double growth and plague) will now appear correctly
+* Adventure map spells are no longer visible on units in battle
+* Attempt to cast spell with no valid targets in hotseat will show appropriate error message
+* RMG settings will now show all existing in game templates and not just those suitable for current settings
+* RMG settings (map size and two-level maps) that are not compatible with current template will be blocked
+* Fixed centering of scenario information window
+* Fixed crash on empty save game list after filtering
+* Fixed blocked progress in Launcher on language detection failure
+* Launcher will now correctly handle selection of Ddata directory in H3 install
+* Map editor will now correctly save message property for events and pandoras
+* Fixed incorrect saving of heroes portraits in editor
+
 # 1.1.1 -> 1.2.0
 
 ### GENERAL:

+ 4 - 2
Global.h

@@ -543,10 +543,12 @@ namespace vstd
 		});
 	}
 
+	/// Increments value by specific delta
+	/// similar to std::next but works with other types, e.g. enum class
 	template<typename T>
-	void advance(T &obj, int change)
+	T next(const T &obj, int change)
 	{
-		obj = (T)(((int)obj) + change);
+		return static_cast<T>(static_cast<ptrdiff_t>(obj) + change);
 	}
 
 	template <typename Container>

BIN
Mods/vcmi/Data/debug/cached.png


BIN
Mods/vcmi/Data/debug/spellRange.png


+ 198 - 165
Mods/vcmi/config/vcmi/chinese.json

@@ -1,17 +1,17 @@
 {
-	"vcmi.adventureMap.monsterThreat.title"     : "\n\n 威胁等级: ",
-	"vcmi.adventureMap.monsterThreat.levels.0"  : "极低",
+	"vcmi.adventureMap.monsterThreat.title"     : "\n\n威胁度: ",
+	"vcmi.adventureMap.monsterThreat.levels.0"  : "不值一提",
 	"vcmi.adventureMap.monsterThreat.levels.1"  : "很低",
 	"vcmi.adventureMap.monsterThreat.levels.2"  : "低",
 	"vcmi.adventureMap.monsterThreat.levels.3"  : "较低",
-	"vcmi.adventureMap.monsterThreat.levels.4"  : "中等",
+	"vcmi.adventureMap.monsterThreat.levels.4"  : "势均力敌",
 	"vcmi.adventureMap.monsterThreat.levels.5"  : "较高",
 	"vcmi.adventureMap.monsterThreat.levels.6"  : "高",
 	"vcmi.adventureMap.monsterThreat.levels.7"  : "很高",
-	"vcmi.adventureMap.monsterThreat.levels.8"  : "挑战性的",
+	"vcmi.adventureMap.monsterThreat.levels.8"  : "略有挑战",
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "压倒性的",
-	"vcmi.adventureMap.monsterThreat.levels.10" : "致命的",
-	"vcmi.adventureMap.monsterThreat.levels.11" : "无法取胜的",
+	"vcmi.adventureMap.monsterThreat.levels.10" : "自寻死路",
+	"vcmi.adventureMap.monsterThreat.levels.11" : "天方夜谭",
 
 	"vcmi.adventureMap.confirmRestartGame"     : "你想要重新开始游戏吗?",
 	"vcmi.adventureMap.noTownWithMarket"       : "没有足够的市场。",
@@ -21,100 +21,139 @@
 	"vcmi.adventureMap.moveCostDetails"        : "移动点数 - 花费: %TURNS 轮 + %POINTS 点移动力, 剩余移动力: %REMAINING",
 	"vcmi.adventureMap.moveCostDetailsNoTurns" : "移动点数 - 花费: %POINTS 点移动力, 剩余移动力: %REMAINING",
 
-	"vcmi.server.errors.existingProcess"     : "另一个VCMI进程在运行,请结束当前进程。",
-	"vcmi.server.errors.modsIncompatibility" : "需要加载mod:",
-	"vcmi.server.confirmReconnect"           : "连接到上次吗?",
+	"vcmi.capitalColors.0" : "红色",
+	"vcmi.capitalColors.1" : "蓝色",
+	"vcmi.capitalColors.2" : "青色",
+	"vcmi.capitalColors.3" : "绿色",
+	"vcmi.capitalColors.4" : "橙色",
+	"vcmi.capitalColors.5" : "紫色",
+	"vcmi.capitalColors.6" : "褐色",
+	"vcmi.capitalColors.7" : "粉色",
 
-	"vcmi.settingsMainWindow.generalTab.hover" : "常规",
-	"vcmi.settingsMainWindow.generalTab.help"     : "切换到“系统选项”选项卡 - 这些设置与常规游戏客户端行为相关",
-	"vcmi.settingsMainWindow.battleTab.hover" : "战斗",
-	"vcmi.settingsMainWindow.battleTab.help"     : "切换到“战斗选项”选项卡 - 这些设置允许配置战斗界面和相关内容",
+	"vcmi.server.errors.existingProcess"     : "一个VCMI进程已经在运行,启动新进程前请结束它。",
+	"vcmi.server.errors.modsIncompatibility" : "需要加载的MOD列表:",
+	"vcmi.server.confirmReconnect"           : "您想要重连上一个会话么?",
+
+	"vcmi.settingsMainWindow.generalTab.hover"   : "常规",
+	"vcmi.settingsMainWindow.generalTab.help"    : "切换到“常规”选项卡 - 设置游戏客户端呈现",
+	"vcmi.settingsMainWindow.battleTab.hover"    : "战斗",
+	"vcmi.settingsMainWindow.battleTab.help"     : "切换到“战斗”选项卡 - 这些设置允许配置战斗界面和相关内容",
 	"vcmi.settingsMainWindow.adventureTab.hover" : "冒险地图",
-	"vcmi.settingsMainWindow.adventureTab.help"  : "切换到“冒险地图”选项卡 - 冒险地图允许你移动英雄",
-	"vcmi.settingsMainWindow.otherTab.hover"     : "其他设置",
-	"vcmi.settingsMainWindow.otherTab.help"      : "切换到“其他设置”选项卡 - 由于各种原因,这些选项不适合其他类别",
+	"vcmi.settingsMainWindow.adventureTab.help"  : "切换到“冒险地图”选项卡 - 冒险地图即玩家能操作英雄移动的界面",
+
+	"vcmi.systemOptions.videoGroup" : "视频设置",
+	"vcmi.systemOptions.audioGroup" : "音频设置",
+	"vcmi.systemOptions.otherGroup" : "其他设置", // unused right now
+	"vcmi.systemOptions.townsGroup" : "城镇画面",
 
 	"vcmi.systemOptions.fullscreenButton.hover" : "全屏",
-	"vcmi.systemOptions.fullscreenButton.help"  : "{全屏n}\n\n 当你选择全屏时,VCMI将会全屏运行,否则只会运行在指定框内",
+	"vcmi.systemOptions.fullscreenButton.help"  : "{全屏}\n\n选中时,VCMI将以全屏运行,否则运行在窗口模式。",
 	"vcmi.systemOptions.resolutionButton.hover" : "分辨率",
-	"vcmi.systemOptions.resolutionButton.help"  : "{选择分辨率}\n\n 改变游戏的分辨率,达到更加清晰的效果。需要重新启动才能完成更改。",
-	"vcmi.systemOptions.resolutionMenu.hover"   : "选择分辨率",
-	"vcmi.systemOptions.resolutionMenu.help"    : "选择游戏的分辨率。",
-	"vcmi.systemOptions.fullscreenFailed"       : "{全屏}\n\n 选择切换到全屏失败!当前分辨率不支持全屏!",
-	"vcmi.systemOptions.framerateButton.hover"  : "显示传输帧数",
-	"vcmi.systemOptions.framerateButton.help"   : "{显示传输帧数}\n\n 打开/关闭在游戏窗口角落的传输帧数计数器。",
+	"vcmi.systemOptions.resolutionButton.help"  : "{分辨率选择}\n\n改变游戏内的分辨率,更改后需要重启游戏使其生效。",
+	"vcmi.systemOptions.resolutionMenu.hover"   : "分辨率选择",
+	"vcmi.systemOptions.resolutionMenu.help"    : "修改游戏运行时的分辨率。",
+	"vcmi.systemOptions.fullscreenFailed"       : "{全屏}\n\n选择切换到全屏模式失败!当前显示器不支持该分辨率!",
+	"vcmi.systemOptions.framerateButton.hover"  : "显示FPS",
+	"vcmi.systemOptions.framerateButton.help"   : "{显示FPS}\n\n打开/关闭在游戏窗口角落的FPS指示器。",
 
+	"vcmi.adventureOptions.infoBarPick.hover" : "在信息面板显示消息",
+	"vcmi.adventureOptions.infoBarPick.help" : "{在信息面板显示消息}\n\n来自访问地图物件的信息将显示在信息面板,而不是弹出窗口。",
 	"vcmi.adventureOptions.numericQuantities.hover" : "生物数量显示",
-	"vcmi.adventureOptions.numericQuantities.help" : "{生物数量显示}\n\n 以数字 A-B 格式显示不准确的敌方生物数量。",
+	"vcmi.adventureOptions.numericQuantities.help" : "{生物数量显示}\n\n以数字 A-B 格式显示不准确的敌方生物数量。",
 	"vcmi.adventureOptions.forceMovementInfo.hover" : "在状态栏中显示移动力",
-	"vcmi.adventureOptions.forceMovementInfo.help" : "{在状态栏中显示移动力}\n\n 不需要按ALT就可以显示移动力。",
-	"vcmi.adventureOptions.showGrid.hover" : "显示六角网格",
-	"vcmi.adventureOptions.showGrid.help" : "{显示六角网格}\n\n 在战场上显示六角网格。",
-	"vcmi.adventureOptions.mapScrollSpeed4.hover": "4",
-	"vcmi.adventureOptions.mapScrollSpeed4.help": "设置动画速度为超快",
-	"vcmi.adventureOptions.mapScrollSpeed5.hover": "5",
-	"vcmi.adventureOptions.mapScrollSpeed5.help": "设置动画速度为极速",
+	"vcmi.adventureOptions.forceMovementInfo.help" : "{在状态栏中显示移动力}\n\n不需要按ALT就可以显示移动力。",
+	"vcmi.adventureOptions.showGrid.hover" : "显示网格",
+	"vcmi.adventureOptions.showGrid.help" : "{显示网格}\n\n显示网格覆盖层,高亮冒险地图物件的边沿。",
+	"vcmi.adventureOptions.mapSwipe.hover" : "地图拖动/镜头",
+	"vcmi.adventureOptions.mapSwipe.help" : "{地图拖动/镜头}\n\n在触摸屏设备上,你可以用手指轻扫来移动地图。使用鼠标时,按住鼠标左键或中键移动地图。",
+	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed1.help": "将地图卷动速度设置为非常慢",
+	"vcmi.adventureOptions.mapScrollSpeed5.help": "将地图卷动速度设置为非常快",
+	"vcmi.adventureOptions.mapScrollSpeed6.help": "将地图卷动速度设置为即刻。",
+
+	"vcmi.battleOptions.queueSizeLabel.hover": "回合顺序指示器",
+	"vcmi.battleOptions.queueSizeNoneButton.hover": "关闭",
+	"vcmi.battleOptions.queueSizeAutoButton.hover": "自动",
+	"vcmi.battleOptions.queueSizeSmallButton.hover": "小",
+	"vcmi.battleOptions.queueSizeBigButton.hover": "大",
+	"vcmi.battleOptions.queueSizeNoneButton.help": "不显示回合顺序指示器",
+	"vcmi.battleOptions.queueSizeAutoButton.help": "根据游戏的分辨率自动调整回合顺序指示器的大小(游戏处于高度低于700像素的分辨率时,使用小,否则使用大)",
+	"vcmi.battleOptions.queueSizeSmallButton.help": "设置回合顺序指示器为小",
+	"vcmi.battleOptions.queueSizeBigButton.help": "设置次寻条为大尺寸(无法在游戏高度像素低于700时生效)",
+	"vcmi.battleOptions.animationsSpeed1.hover": "",
+	"vcmi.battleOptions.animationsSpeed5.hover": "",
+	"vcmi.battleOptions.animationsSpeed6.hover": "",
+	"vcmi.battleOptions.animationsSpeed1.help": "设置动画速度为非常慢",
+	"vcmi.battleOptions.animationsSpeed5.help": "设置动画速度为非常快",
+	"vcmi.battleOptions.animationsSpeed6.help": "设置动画速度为即刻",
+	"vcmi.battleOptions.touchscreenMode.hover": "触屏模式",
+	"vcmi.battleOptions.touchscreenMode.help": "{触屏模式}\n\n当启用时,需要进行双击进行确认和执行动作。减少触屏设备误触。",
+	"vcmi.battleOptions.movementHighlightOnHover.hover": "鼠标悬停高亮单位移动范围",
+	"vcmi.battleOptions.movementHighlightOnHover.help": "{鼠标悬停高亮单位移动范围}\n\n当你的鼠标悬停在单位上时高亮他的行动范围。",
+	"vcmi.battleOptions.skipBattleIntroMusic.hover": "跳过战斗开始音乐",
+	"vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过战斗开始音乐}\n\n战斗开始音乐播放期间,你也能够进行操作。",
+	"vcmi.battleWindow.pressKeyToSkipIntro" : "按下任意键立即开始战斗",
+
+	"vcmi.battleWindow.damageEstimation.melee" : "近战攻击 %CREATURE (%DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.meleeKills" : "近战攻击 %CREATURE (%DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.ranged" : "射击 %CREATURE (%SHOTS, %DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.rangedKills" : "射击 %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.shots" : "%d 弹药剩余",
+	"vcmi.battleWindow.damageEstimation.shots.1" : "%d 弹药剩余",
+	"vcmi.battleWindow.damageEstimation.damage" : "%d 伤害",
+	"vcmi.battleWindow.damageEstimation.damage.1" : "%d 伤害",
+	"vcmi.battleWindow.damageEstimation.kills" : "%d 将被消灭",
+	"vcmi.battleWindow.damageEstimation.kills.1" : "%d 将被消灭",
 
-	"vcmi.battleOptions.showQueue.hover": "显示移动次序",
-	"vcmi.battleOptions.showQueue.help": "{显示移动次序}\n\n 显示当前生物的移动次序。",
-	"vcmi.battleOptions.queueSizeLabel.hover": "次序条尺寸 (设置后下一场战斗生效)",
-	"vcmi.battleOptions.queueSizeAutoButton.hover": "自动设置尺寸",
-	"vcmi.battleOptions.queueSizeAutoButton.help": "根据游戏分辨率设置尺寸 (像素小于700为小尺寸,根据实际调整)",
-	"vcmi.battleOptions.queueSizeSmallButton.hover": "小尺寸",
-	"vcmi.battleOptions.queueSizeSmallButton.help": "设置次序条为小尺寸",
-	"vcmi.battleOptions.queueSizeBigButton.hover": "大尺寸",
-	"vcmi.battleOptions.queueSizeBigButton.help": "设置次寻条为大尺寸(不能在像素小于700时生效)",
-	"vcmi.battleOptions.animationsSpeed4.hover": "4",
-	"vcmi.battleOptions.animationsSpeed4.help": "设置动画速度为超快",
-	"vcmi.battleOptions.animationsSpeed5.hover": "5",
-	"vcmi.battleOptions.animationsSpeed5.help": "设置动画速度为极速",
-	"vcmi.battleOptions.animationsSpeed6.hover": "6",
-	"vcmi.battleOptions.animationsSpeed6.help": "设置动画速度为最快",
-	"vcmi.battleOptions.skipBattleIntroMusic.hover": "跳过开场音乐",
-	"vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过开场音乐}\n\n 战斗开始时跳过开场音乐,直接按Esc也可以跳过。",
+	"vcmi.battleResultsWindow.applyResultsLabel" : "接受战斗结果",
 
-	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "显示所有可以招募的城镇生物",
-	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{显示所有可以招募的城镇生物}\n\n 显示当前所有可供招募的城镇生物 (左下角)。",
-	"vcmi.otherOptions.compactTownCreatureInfo.hover": "缩小城镇生物信息",
-	"vcmi.otherOptions.compactTownCreatureInfo.help": "{缩小城镇生物信息}\n\n 将城镇生物信息最小化.",
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "显示可招募生物",
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{显示可招募生物}\n\n在城镇摘要(城镇屏幕的左下角)中显示可招募的生物数量,而不是增长。",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "显示生物增长",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{显示生物增长}\n\n在城镇摘要(城镇屏幕的左下角)中显示生物的每周增长而不是可用数量。",
+	"vcmi.otherOptions.compactTownCreatureInfo.hover": "紧凑生物信息",
+	"vcmi.otherOptions.compactTownCreatureInfo.help": "{紧凑生物信息}\n\n城镇概要(城镇屏幕的左下角)中城镇生物信息紧凑显示。",
 
-	"vcmi.townHall.missingBase"             : "你必须先建造%s ",
-	"vcmi.townHall.noCreaturesToRecruit"    : "没有可供雇佣的生物。",
-	"vcmi.townHall.greetingManaVortex"      : "当你接近%s时,你的身体充满了新的能量。这使你的魔法值加倍。",
-	"vcmi.townHall.greetingKnowledge"       : "你学习了%s上的图形,并深入了解各种魔法的运作,这使你的知识点数+1。",
-	"vcmi.townHall.greetingSpellPower"      : "%s教你新的方法来集中你的魔法力量,这使你的力量点数+1。",
-	"vcmi.townHall.greetingExperience"      : "访问%s给你提供了更好的学习方法。这使你的经验值+1000。",
-	"vcmi.townHall.greetingAttack"          : "在%s参观后给你提供了更好的战斗技巧,这使你的攻击点数+1。",
-	"vcmi.townHall.greetingDefence"         : "在%s中度过一段时间后,经验丰富的勇士会教你额外的防御技能,这使你的防御点数+1。",
+	"vcmi.townHall.missingBase"             : "必须先建造基础建筑 %s",
+	"vcmi.townHall.noCreaturesToRecruit"    : "没有可供招募的生物。",
+	"vcmi.townHall.greetingManaVortex"      : "接近%s时,你会全身充满活力,并且你的魔法值会加倍。",
+	"vcmi.townHall.greetingKnowledge"       : "你研究了%s的浮雕,洞察了魔法的秘密(知识+1)。",
+	"vcmi.townHall.greetingSpellPower"      : "%s教你如何运用魔法力量(力量+1)。",
+	"vcmi.townHall.greetingExperience"      : "参观%s可以让你学会许多新的技能(经验值+1000)。",
+	"vcmi.townHall.greetingAttack"          : "在%s中稍待片刻可以让你学会更有效的战斗技巧(攻击力+1)。",
+	"vcmi.townHall.greetingDefence"         : "在%s中稍待片刻,富有战斗经验的战士会教你防御技巧(防御力+1)。",
 	"vcmi.townHall.hasNotProduced"          : "本周%s并没有产生什么资源。",
 	"vcmi.townHall.hasProduced"             : "本周%s产生了%d个%s。",
-	"vcmi.townHall.greetingCustomBonus"     : "参观%s后,你的技巧有了提升。这使你受益匪浅。并且使你+%d %s%s",
+	"vcmi.townHall.greetingCustomBonus"     : "%s 赋予你 +%d %s%s",
 	"vcmi.townHall.greetingCustomUntil"     : "直到下一场战斗。",
 	"vcmi.townHall.greetingInTownMagicWell" : "%s使你的魔法值恢复到最大值。",
 
-	"vcmi.logicalExpressions.anyOf"  : "以下任前提:",
+	"vcmi.logicalExpressions.anyOf"  : "以下任前提:",
 	"vcmi.logicalExpressions.allOf"  : "以下所有前提:",
 	"vcmi.logicalExpressions.noneOf" : "无前提:",
 
 	"vcmi.heroWindow.openCommander.hover" : "开启指挥官界面",
-	"vcmi.heroWindow.openCommander.help"  : "开启英雄的指挥官界面",
+	"vcmi.heroWindow.openCommander.help"  : "显示该英雄指挥官详细信息",
 
 	"vcmi.commanderWindow.artifactMessage" : "你要把这个宝物还给英雄吗?",
 
-	"vcmi.creatureWindow.showBonuses.hover"    : "属性界面",
-	"vcmi.creatureWindow.showBonuses.help"     : "显示指挥官的所有增强属性",
-	"vcmi.creatureWindow.showSkills.hover"     : "技能页面",
-	"vcmi.creatureWindow.showSkills.help"      : "显示指挥官的所有技能",
+	"vcmi.creatureWindow.showBonuses.hover"    : "属性视图",
+	"vcmi.creatureWindow.showBonuses.help"     : "显示指挥官的所有属性增益",
+	"vcmi.creatureWindow.showSkills.hover"     : "技能视图",
+	"vcmi.creatureWindow.showSkills.help"      : "显示指挥官的所有学习的技能",
 	"vcmi.creatureWindow.returnArtifact.hover" : "交换宝物",
-	"vcmi.creatureWindow.returnArtifact.help"  : "将宝物还到英雄的背包里",
+	"vcmi.creatureWindow.returnArtifact.help"  : "点击这个按钮将宝物还到英雄的背包里",
 
 	"vcmi.questLog.hideComplete.hover" : "隐藏完成任务",
 	"vcmi.questLog.hideComplete.help"  : "隐藏所有完成的任务",
 
 	"vcmi.randomMapTab.widgets.defaultTemplate"      : "默认",
-	"vcmi.randomMapTab.widgets.templateLabel"        : "格式",
-	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "设...",
+	"vcmi.randomMapTab.widgets.templateLabel"        : "模板",
+	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "设...",
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "同盟关系",
+	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "道路类型",
 	
 	// few strings from WoG used by vcmi
 	"vcmi.stackExperience.description" : "» 经 验 获 得 明 细 «\n\n生物类型 ................... : %s\n经验等级 ................. : %s (%i)\n经验点数 ............... : %i\n下一个等级所需经验 .. : %i\n每次战斗最大获得经验 ... : %i%% (%i)\n获得经验的生物数量 .... : %i\n最大招募数量\n不会丢失经验升级 .... : %i\n经验倍数 ........... : %.2f\n升级倍数 .............. : %.2f\n10级后经验值 ........ : %i\n最大招募数量下\n 升级到10级所需经验数量: %i",
@@ -130,154 +169,148 @@
 	"vcmi.stackExperience.rank.9" : "中校 10级",
 	"vcmi.stackExperience.rank.10" : "上校 11级",
 	
-	"core.bonus.ADDITIONAL_ATTACK.name": "双击",
-	"core.bonus.ADDITIONAL_ATTACK.description": "可以攻击两次",
+	"core.bonus.ADDITIONAL_ATTACK.name": "双重攻击",
+	"core.bonus.ADDITIONAL_ATTACK.description": "攻击两次",
 	"core.bonus.ADDITIONAL_RETALIATION.name": "额外反击",
-	"core.bonus.ADDITIONAL_RETALIATION.description": "可以额外反击 ${val} 次",
+	"core.bonus.ADDITIONAL_RETALIATION.description": "每回合额外获得${val}次反击机会",
 	"core.bonus.AIR_IMMUNITY.name": "气系免疫",
 	"core.bonus.AIR_IMMUNITY.description": "免疫所有气系魔法",
 	"core.bonus.ATTACKS_ALL_ADJACENT.name": "环击",
-	"core.bonus.ATTACKS_ALL_ADJACENT.description": "攻击所有相邻部队",
+	"core.bonus.ATTACKS_ALL_ADJACENT.description": "攻击所有相邻敌人",
 	"core.bonus.BLOCKS_RETALIATION.name": "无反击",
 	"core.bonus.BLOCKS_RETALIATION.description": "敌人无法反击",
 	"core.bonus.BLOCKS_RANGED_RETALIATION.name": "远程无反击",
 	"core.bonus.BLOCKS_RANGED_RETALIATION.description": "敌人无法对射击进行反击",
 	"core.bonus.CATAPULT.name": "攻城",
 	"core.bonus.CATAPULT.description": "可以攻击城墙",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "施法消耗 - (${val})",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "减少英雄施法消耗",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "对方施法消耗 + (${val})",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "增加对方施法消耗",
-	"core.bonus.CHARGE_IMMUNITY.name": "I免疫冲锋",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "施法消耗减少 (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "英雄施法消耗魔法值减少${val}点",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "施法阻碍 (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "敌方施法消耗的魔法值增加${val}点",
+	"core.bonus.CHARGE_IMMUNITY.name": "免疫冲锋",
 	"core.bonus.CHARGE_IMMUNITY.description": "对冲锋特技的额外伤害免疫",
-	"core.bonus.DARKNESS.name": "黑暗天幕",
-	"core.bonus.DARKNESS.description": "增加 ${val} 半径黑幕",
+	"core.bonus.DARKNESS.name": "阴影庇体",
+	"core.bonus.DARKNESS.description": "创建${val}半径黑幕",
 	"core.bonus.DEATH_STARE.name": "死亡凝视 (${val}%)",
-	"core.bonus.DEATH_STARE.description": "${val}% 几率直接杀死生物",
-	"core.bonus.DEFENSIVE_STANCE.name": "防御奖励",
-	"core.bonus.DEFENSIVE_STANCE.description": "当选择防御时+${val} 防御力",
+	"core.bonus.DEATH_STARE.description": "${val}%几率直接杀死一单位生物",
+	"core.bonus.DEFENSIVE_STANCE.name": "防御增效",
+	"core.bonus.DEFENSIVE_STANCE.description": "当选择防御时+${val}防御力",
 	"core.bonus.DESTRUCTION.name": "毁灭",
-	"core.bonus.DESTRUCTION.description": "有${val}% 杀死额外数量的部队",
+	"core.bonus.DESTRUCTION.description": "${val}%几率在攻击后追加消灭数量",
 	"core.bonus.DOUBLE_DAMAGE_CHANCE.name": "致命一击",
-	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}% 几率造成双倍伤害",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "${val}%几率造成双倍基础伤害",
 	"core.bonus.DRAGON_NATURE.name": "龙",
-	"core.bonus.DRAGON_NATURE.description": "生物属于龙类",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.name": "魔法伤免疫",
-	"core.bonus.DIRECT_DAMAGE_IMMUNITY.description": "对魔法伤害免疫",
+	"core.bonus.DRAGON_NATURE.description": "生物拥有龙的特性",
+	"core.bonus.DIRECT_DAMAGE_IMMUNITY.name": "魔法伤免疫",
+	"core.bonus.DIRECT_DAMAGE_IMMUNITY.description": "免疫直接造成伤害的魔法",
 	"core.bonus.EARTH_IMMUNITY.name": "土系免疫",
 	"core.bonus.EARTH_IMMUNITY.description": "免疫所有土系魔法",
-	"core.bonus.ENCHANTER.name": "施法者",
-	"core.bonus.ENCHANTER.description": "每回合群体施放 ${subtype.spell} ",
-	"core.bonus.ENCHANTED.name": "魔法身",
-	"core.bonus.ENCHANTED.description": "自身被 ${subtype.spell} 魔法影响",
+	"core.bonus.ENCHANTER.name": "强化师",
+	"core.bonus.ENCHANTER.description": "每回合群体施放${subtype.spell}",
+	"core.bonus.ENCHANTED.name": "魔法身",
+	"core.bonus.ENCHANTED.description": "永久处于${subtype.spell}影响",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "忽略防御 (${val}%)",
-	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "攻击时忽略对方部分防御力",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "当攻击时,目标生物${val}%的防御力将被无视。",
 	"core.bonus.FIRE_IMMUNITY.name": "火系免疫",
 	"core.bonus.FIRE_IMMUNITY.description": "免疫所有火系魔法",
 	"core.bonus.FIRE_SHIELD.name": "烈火神盾 (${val}%)",
-	"core.bonus.FIRE_SHIELD.description": "拥有烈火神盾护身",
+	"core.bonus.FIRE_SHIELD.description": "反弹部分受到的近战伤害",
 	"core.bonus.FIRST_STRIKE.name": "抢先攻击",
-	"core.bonus.FIRST_STRIKE.description": "在被反击前做出攻击",
+	"core.bonus.FIRST_STRIKE.description": "该生物的反击将会在被攻击前进行",
 	"core.bonus.FEAR.name": "恐惧",
-	"core.bonus.FEAR.description": "引起恐惧",
+	"core.bonus.FEAR.description": "使得敌方一只部队恐惧",
 	"core.bonus.FEARLESS.name": "无惧",
-	"core.bonus.FEARLESS.description": "免疫恐惧",
-	"core.bonus.FLYING.name": "飞行兵种",
-	"core.bonus.FLYING.description": "生物可以飞行",
+	"core.bonus.FEARLESS.description": "免疫恐惧特质",
+	"core.bonus.FLYING.name": "飞行",
+	"core.bonus.FLYING.description": "以飞行的方式移动(无视障碍)",
 	"core.bonus.FREE_SHOOTING.name": "近身射击",
-	"core.bonus.FREE_SHOOTING.description": "靠近敌方也能射击",
-	"core.bonus.FULL_HP_REGENERATION.name": "重生",
-	"core.bonus.FULL_HP_REGENERATION.description": "可以自动恢复所有生命值",
-	"core.bonus.GARGOYLE.name": "石像鬼属性",
+	"core.bonus.FREE_SHOOTING.description": "能在近战范围内进行射击",
+	"core.bonus.GARGOYLE.name": "石像鬼",
 	"core.bonus.GARGOYLE.description": "不能被复活或治疗",
 	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "减少伤害 (${val}%)",
-	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "受攻击时减少受的伤害",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "减少从远程和近战中遭受的物理伤害",
 	"core.bonus.HATE.name": "${subtype.creature}的死敌",
-	"core.bonus.HATE.description": "对该部队造成 ${val}% 的额外伤害",
-	"core.bonus.HEALER.name": "治疗",
+	"core.bonus.HATE.description": "对${subtype.creature}造成额外${val}%伤害",
+	"core.bonus.HEALER.name": "治疗",
 	"core.bonus.HEALER.description": "可以治疗友军单位",
-	"core.bonus.HP_REGENERATION.name": "重生",
-	"core.bonus.HP_REGENERATION.description": "每回合恢复 ${val} 点生命值",
-	"core.bonus.JOUSTING.name": "冲锋",
-	"core.bonus.JOUSTING.description": "每格行动增加+5%伤害",
-	"core.bonus.KING1.name": "一般顶级怪物",
-	"core.bonus.KING1.description": "被初级屠戮成性影响",
-	"core.bonus.KING2.name": "智慧顶级怪物",
-	"core.bonus.KING2.description": "被中级屠戮成性影响",
-	"core.bonus.KING3.name": "精神顶级怪物",
-	"core.bonus.KING3.description":"被高级屠戮成性影响",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "免疫 1-${val} 级魔法",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "免疫等级为 1-${val} 级的所有魔法",
-	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "半程射击",
-	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "超过 ${val} 格不能射击",
+	"core.bonus.HP_REGENERATION.name": "再生",
+	"core.bonus.HP_REGENERATION.description": "每回合恢复${SHval}点生命值",
+	"core.bonus.JOUSTING.name": "英勇冲锋",
+	"core.bonus.JOUSTING.description": "每移动一格 +${val}%伤害",
+	"core.bonus.KING.name": "王牌",
+	"core.bonus.KING.description": "受${val}级或更高级屠戮成性影响",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "免疫1-${val}级魔法",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "免疫1-${val}级的魔法",
+	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "受限射击距离",
+	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "无法以${val}格外的单位为射击目标",
 	"core.bonus.LIFE_DRAIN.name": "吸取生命 (${val}%)",
-	"core.bonus.LIFE_DRAIN.description": "吸取 ${val}% 伤害回复自身",
-	"core.bonus.MANA_CHANNELING.name": "偷取魔法 ${val}%",
-	"core.bonus.MANA_CHANNELING.description": "偷取部分敌人施法消耗",
+	"core.bonus.LIFE_DRAIN.description": "吸取${val}%伤害回复自身",
+	"core.bonus.MANA_CHANNELING.name": "魔法通道${val}%",
+	"core.bonus.MANA_CHANNELING.description": "使你的英雄有${val}%几率获得敌人施法的魔法值",
 	"core.bonus.MANA_DRAIN.name": "吸取魔力",
-	"core.bonus.MANA_DRAIN.description": "每回合吸取 ${val} 魔法值",
-	"core.bonus.MAGIC_MIRROR.name": "带有魔法神镜 (${val}%)",
-	"core.bonus.MAGIC_MIRROR.description": "${val}% 几率反射魔法",
-	"core.bonus.MAGIC_RESISTANCE.name": "(${MR}%) 魔法抵抗",
-	"core.bonus.MAGIC_RESISTANCE.description": "${MR}% 几率抵抗敌人的魔法",
+	"core.bonus.MANA_DRAIN.description": "每回合吸取${val}魔法值",
+	"core.bonus.MAGIC_MIRROR.name": "魔法神镜 (${val}%)",
+	"core.bonus.MAGIC_MIRROR.description": "${val}%几率将进攻性魔法导向一个敌人单位",
+	"core.bonus.MAGIC_RESISTANCE.name": "魔法抵抗 (${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description": "${val}%几率抵抗敌人的魔法",
 	"core.bonus.MIND_IMMUNITY.name": "免疫心智",
 	"core.bonus.MIND_IMMUNITY.description": "不受心智魔法的影响",
-	"core.bonus.NO_DISTANCE_PENALTY.name": "无障碍射击",
-	"core.bonus.NO_DISTANCE_PENALTY.description": "射击不受距离影响",
+	"core.bonus.NO_DISTANCE_PENALTY.name": "无视距离惩罚",
+	"core.bonus.NO_DISTANCE_PENALTY.description": "任意距离均造成全额伤害",
 	"core.bonus.NO_MELEE_PENALTY.name": "无近战惩罚",
-	"core.bonus.NO_MELEE_PENALTY.description": "近战伤害不减",
+	"core.bonus.NO_MELEE_PENALTY.description": "该生物没有近战伤害惩罚",
 	"core.bonus.NO_MORALE.name": "无士气",
 	"core.bonus.NO_MORALE.description": "生物不受士气影响",
 	"core.bonus.NO_WALL_PENALTY.name": "无城墙影响",
-	"core.bonus.NO_WALL_PENALTY.description": "射击不受城墙的影响",
+	"core.bonus.NO_WALL_PENALTY.description": "攻城战中被城墙阻挡造成全额伤害",
 	"core.bonus.NON_LIVING.name": "无生命",
-	"core.bonus.NON_LIVING.description": "不受只对生命实体生物有效的魔法",
+	"core.bonus.NON_LIVING.description": "免疫大多数的效果",
 	"core.bonus.RANDOM_SPELLCASTER.name": "随机施法",
-	"core.bonus.RANDOM_SPELLCASTER.description": "随机施放增益魔法",
+	"core.bonus.RANDOM_SPELLCASTER.description": "可以施放随机魔法",
 	"core.bonus.RANGED_RETALIATION.name": "远程反击",
 	"core.bonus.RANGED_RETALIATION.description": "可以对远程攻击进行反击",
-	"core.bonus.RECEPTIVE.name": "接受有益魔法",
-	"core.bonus.RECEPTIVE.description": "不会免疫有益魔法",
+	"core.bonus.RECEPTIVE.name": "适应",
+	"core.bonus.RECEPTIVE.description": "不会免疫有益魔法",
 	"core.bonus.REBIRTH.name": "复生 (${val}%)",
-	"core.bonus.REBIRTH.description": "{val}% 数量死亡后会复活",
-	"core.bonus.RETURN_AFTER_STRIKE.name": "攻击返回",
-	"core.bonus.RETURN_AFTER_STRIKE.description": "攻击后回到初始位置",
-	"core.bonus.SHOOTER.name": "射手",
-	"core.bonus.SHOOTER.description": "生物可以设计",
+	"core.bonus.REBIRTH.description": "当整支部队死亡后${val}%会复活",
+	"core.bonus.RETURN_AFTER_STRIKE.name": "攻击返回",
+	"core.bonus.RETURN_AFTER_STRIKE.description": "近战攻击后回到初始位置",
+	"core.bonus.SHOOTER.name": "远程攻击",
+	"core.bonus.SHOOTER.description": "生物可以射击",
 	"core.bonus.SHOOTS_ALL_ADJACENT.name": "范围远程攻击",
-	"core.bonus.SHOOTS_ALL_ADJACENT.description": "远程攻击可伤害范围内的多个目标",
-	"core.bonus.SOUL_STEAL.name": "杀死敌人复生",
-	"core.bonus.SOUL_STEAL.description": "当杀死敌人时获得 ${val} 数量",
+	"core.bonus.SHOOTS_ALL_ADJACENT.description": "该生物的远程攻击将同时命中小范围内所有目标",
+	"core.bonus.SOUL_STEAL.name": "灵魂窃取",
+	"core.bonus.SOUL_STEAL.description": "每杀死一个敌人将获得${val}数量的生物",
 	"core.bonus.SPELLCASTER.name": "施法者",
-	"core.bonus.SPELLCASTER.description": "生物可以施放 ${subtype.spell}",
+	"core.bonus.SPELLCASTER.description": "生物可以施放${subtype.spell}",
 	"core.bonus.SPELL_AFTER_ATTACK.name": "攻击后施法",
-	"core.bonus.SPELL_AFTER_ATTACK.description": "${val}% 攻击后施放 ${subtype.spell}",
+	"core.bonus.SPELL_AFTER_ATTACK.description": "攻击后${val}%几率施放${subtype.spell}魔法",
 	"core.bonus.SPELL_BEFORE_ATTACK.name": "攻击前施法",
-	"core.bonus.SPELL_BEFORE_ATTACK.description": "${val}% 攻击前施放 ${subtype.spell}",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "魔法伤害抵抗",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "受魔法攻击时伤害减少 ${val}%",
+	"core.bonus.SPELL_BEFORE_ATTACK.description": "攻击前${val}%几率施放${subtype.spell}魔法",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "魔法伤害减免",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "受魔法攻击时伤害减少${val}%",
 	"core.bonus.SPELL_IMMUNITY.name": "特定魔法免疫",
-	"core.bonus.SPELL_IMMUNITY.description": "免疫 ${subtype.spell}",
-	"core.bonus.SPELL_LIKE_ATTACK.name": "法攻击",
-	"core.bonus.SPELL_LIKE_ATTACK.description": "攻击时使用 ${subtype.spell}",
+	"core.bonus.SPELL_IMMUNITY.description": "免疫${subtype.spell}魔法",
+	"core.bonus.SPELL_LIKE_ATTACK.name": "类施法攻击",
+	"core.bonus.SPELL_LIKE_ATTACK.description": "攻击时使用${subtype.spell}",
 	"core.bonus.SPELL_RESISTANCE_AURA.name": "抗魔光环",
-	"core.bonus.SPELL_RESISTANCE_AURA.description": "邻近部队获得 ${val}% 魔法抵抗",
+	"core.bonus.SPELL_RESISTANCE_AURA.description": "邻近部队获得${val}%几率抵抗魔法",
 	"core.bonus.SUMMON_GUARDIANS.name": "召唤守卫",
-	"core.bonus.SUMMON_GUARDIANS.description": "战斗前召唤 ${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.name": "可协助攻击",
+	"core.bonus.SUMMON_GUARDIANS.description": "战斗开始时召唤${subtype.creature}(${val}%)",
+	"core.bonus.SYNERGY_TARGET.name": "协同攻击",
 	"core.bonus.SYNERGY_TARGET.description": "生物受到协助攻击的影响",
-	"core.bonus.TWO_HEX_ATTACK_BREATH.name": "息",
-	"core.bonus.TWO_HEX_ATTACK_BREATH.description": "吐息攻击2个部队",
-	"core.bonus.THREE_HEADED_ATTACK.name": "半环击",
-	"core.bonus.THREE_HEADED_ATTACK.description": "攻击正前方多个敌人",
-	"core.bonus.TRANSMUTATION.name": "变",
-	"core.bonus.TRANSMUTATION.description": "${val}% 机会将敌人变成其他生物",
-	"core.bonus.UNDEAD.name": "不死生物",
-	"core.bonus.UNDEAD.description": "生物有丧尸属性",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.name": "息",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.description": "吐息攻击(2格范围)",
+	"core.bonus.THREE_HEADED_ATTACK.name": "三头攻击",
+	"core.bonus.THREE_HEADED_ATTACK.description": "攻击三格邻接单位",
+	"core.bonus.TRANSMUTATION.name": "变形术",
+	"core.bonus.TRANSMUTATION.description": "${val}%机会将被攻击单位变成其他生物",
+	"core.bonus.UNDEAD.name": "亡灵",
+	"core.bonus.UNDEAD.description": "该生物属于亡灵",
 	"core.bonus.UNLIMITED_RETALIATIONS.name": "无限反击",
 	"core.bonus.UNLIMITED_RETALIATIONS.description": "每回合可以无限反击敌人",
 	"core.bonus.WATER_IMMUNITY.name": "水系免疫",
 	"core.bonus.WATER_IMMUNITY.description": "免疫水系魔法",
 	"core.bonus.WIDE_BREATH.name": "弧形焰息",
-	"core.bonus.WIDE_BREATH.description": "吐息攻击前方扇形6个部队"
+	"core.bonus.WIDE_BREATH.description": "大范围喷吐攻击(目标左右以及后方共6格)"
 }

+ 10 - 1
Mods/vcmi/config/vcmi/english.json

@@ -21,6 +21,15 @@
 	"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.capitalColors.0" : "Red",
+	"vcmi.capitalColors.1" : "Blue",
+	"vcmi.capitalColors.2" : "Tan",
+	"vcmi.capitalColors.3" : "Green",
+	"vcmi.capitalColors.4" : "Orange",
+	"vcmi.capitalColors.5" : "Purple",
+	"vcmi.capitalColors.6" : "Teal",
+	"vcmi.capitalColors.7" : "Pink",
+
 	"vcmi.server.errors.existingProcess"     : "Another VCMI server process is running. Please terminate it before starting a new game.",
 	"vcmi.server.errors.modsIncompatibility" : "The following mods are required to load the game:",
 	"vcmi.server.confirmReconnect"           : "Do you want to reconnect to the last session?",
@@ -225,7 +234,7 @@
 	"core.bonus.HEALER.name": "Healer",
 	"core.bonus.HEALER.description": "Heals allied units",
 	"core.bonus.HP_REGENERATION.name": "Regeneration",
-	"core.bonus.HP_REGENERATION.description": "Heals ${SHval} hit points every round",
+	"core.bonus.HP_REGENERATION.description": "Heals ${val} hit points every round",
 	"core.bonus.JOUSTING.name": "Champion charge",
 	"core.bonus.JOUSTING.description": "+${val}% damage for each hex travelled",
 	"core.bonus.KING.name": "King",

+ 10 - 1
Mods/vcmi/config/vcmi/german.json

@@ -21,6 +21,15 @@
 	"vcmi.adventureMap.moveCostDetails"        : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING",
 	"vcmi.adventureMap.moveCostDetailsNoTurns" : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING",
 
+	"vcmi.capitalColors.0" : "Rot",
+	"vcmi.capitalColors.1" : "Blau",
+	"vcmi.capitalColors.2" : "Braun",
+	"vcmi.capitalColors.3" : "Grün",
+	"vcmi.capitalColors.4" : "Orange",
+	"vcmi.capitalColors.5" : "Violett",
+	"vcmi.capitalColors.6" : "Türkis",
+	"vcmi.capitalColors.7" : "Rosa",
+
 	"vcmi.server.errors.existingProcess"     : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst",
 	"vcmi.server.errors.modsIncompatibility" : "Erforderliche Mods um das Spiel zu laden:",
 	"vcmi.server.confirmReconnect"           : "Mit der letzten Sitzung verbinden?",
@@ -220,7 +229,7 @@
 	"core.bonus.HEALER.name": "Heiler",
 	"core.bonus.HEALER.description": "Heilt verbündete Einheiten",
 	"core.bonus.HP_REGENERATION.name": "Regeneration",
-	"core.bonus.HP_REGENERATION.description": "Heilt ${SHval} Trefferpunkte jede Runde",
+	"core.bonus.HP_REGENERATION.description": "Heilt ${val} Trefferpunkte jede Runde",
 	"core.bonus.JOUSTING.name": "Champion Charge",
 	"core.bonus.JOUSTING.description": "+${val}% Schaden pro zurückgelegtem Feld",
 	"core.bonus.KING.name": "König",

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

@@ -208,7 +208,7 @@
 	"core.bonus.HEALER.name": "Uzdrowiciel",
 	"core.bonus.HEALER.description": "Leczy sprzymierzone jednostki",
 	"core.bonus.HP_REGENERATION.name": "Regeneracja",
-	"core.bonus.HP_REGENERATION.description": "Leczy ${SHval} punktów zdrowia każdej rundy",
+	"core.bonus.HP_REGENERATION.description": "Leczy ${val} punktów zdrowia każdej rundy",
 	"core.bonus.JOUSTING.name": "Szarża Czempiona",
 	"core.bonus.JOUSTING.description": "+${val}% obrażeń na przebytego heksa",
 	"core.bonus.KING.name": "Król",

+ 2 - 3
Mods/vcmi/config/vcmi/rmg/symmetric/3sm3d.JSON

@@ -112,9 +112,8 @@
 				"matchTerrainToTown" : false,
 				"treasure" :
 				[
-					{ "min" : 0, "max" : 0, "density" : 1 },
-					{ "min" : 0, "max" : 0, "density" : 1 },
-					{ "min" : 0, "max" : 0, "density" : 1 }
+					{ "min" : 100, "max" : 1000, "density" : 10 },
+					{ "min" : 1000, "max" : 3000, "density" : 1 }
 				]
 			},
 			"12" :

+ 1 - 1
Mods/vcmi/config/vcmi/russian.json

@@ -232,7 +232,7 @@
 	"core.bonus.HEALER.name": "Целитель",
 	"core.bonus.HEALER.description": "Исцеляет дружественные юниты",
 	"core.bonus.HP_REGENERATION.name": "Регенерация",
-	"core.bonus.HP_REGENERATION.description": "Исцеляет ${SHval} очков здоровья каждый ход",
+	"core.bonus.HP_REGENERATION.description": "Исцеляет ${val} очков здоровья каждый ход",
 	"core.bonus.JOUSTING.name": "Разгон",
 	"core.bonus.JOUSTING.description": "+${val}% урона за каждую пройденную клетку",
 	"core.bonus.KING.name": "Король",

+ 1 - 1
Mods/vcmi/config/vcmi/ukrainian.json

@@ -208,7 +208,7 @@
 	"core.bonus.HEALER.name" : "Цілитель",
 	"core.bonus.HEALER.description" : "Лікує союзників",
 	"core.bonus.HP_REGENERATION.name" : "Регенерація",
-	"core.bonus.HP_REGENERATION.description" : "Відновлює ${SHval} очок здоров'я кожного раунду",
+	"core.bonus.HP_REGENERATION.description" : "Відновлює ${val} очок здоров'я кожного раунду",
 	"core.bonus.JOUSTING.name" : "Турнірна перевага",
 	"core.bonus.JOUSTING.description" : "+${val}% шкоди за кожен пройдений гекс",
 	"core.bonus.KING.name" : "Король",

+ 1 - 0
README.md

@@ -1,5 +1,6 @@
 [![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml)
 [![Coverity Scan Build Status](https://scan.coverity.com/projects/vcmi/badge.svg)](https://scan.coverity.com/projects/vcmi)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.2.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.2.1)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.2.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.2.0)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 # VCMI Project

+ 30 - 17
android/vcmi-app/build.gradle

@@ -10,14 +10,16 @@ android {
 		applicationId "is.xyz.vcmi"
 		minSdk 19
 		targetSdk 31
-		versionCode 1103
-		versionName "1.1"
+		versionCode 1201
+		versionName "1.2.1"
 		setProperty("archivesBaseName", "vcmi")
 	}
 
 	signingConfigs {
 		releaseSigning
-		LoadSigningConfig()
+		dailySigning
+		LoadSigningConfig("releaseSigning")
+		LoadSigningConfig("dailySigning")
 	}
 
 	buildTypes {
@@ -46,6 +48,7 @@ android {
 		daily {
 			initWith release
 			applicationIdSuffix '.daily'
+			signingConfig signingConfigs.dailySigning
 			manifestPlaceholders = [
 				applicationLabel: 'VCMI daily',
 			]
@@ -118,38 +121,48 @@ def ResolveGitInfo() {
 		CommandOutput("git", ["describe", "--match=", "--always", "--abbrev=7"], ".")
 }
 
-def SigningPropertiesPath(final basePath) {
-	return file("${basePath}/signing.properties")
+def SigningPropertiesPath(final basePath, final signingConfigKey) {
+	return file("${basePath}/${signingConfigKey}.properties")
 }
 
 def SigningKeystorePath(final basePath, final keystoreFileName) {
 	return file("${basePath}/${keystoreFileName}")
 }
 
-def LoadSigningConfig() {
+def LoadSigningConfig(final signingConfigKey) {
 	final def projectRoot = "${project.projectDir}/../../CI/android"
 	final def props = new Properties()
-	final def propFile = SigningPropertiesPath(projectRoot)
+	final def propFile = SigningPropertiesPath(projectRoot, signingConfigKey)
+	
+	def signingConfig = android.signingConfigs.getAt(signingConfigKey)
+	
 	if (propFile.canRead()) {
 		props.load(new FileInputStream(propFile))
 
 		if (props != null
 			&& props.containsKey('STORE_FILE')
-			&& props.containsKey('STORE_PASSWORD')
-			&& props.containsKey('KEY_ALIAS')
-			&& props.containsKey('KEY_PASSWORD')) {
-
-			android.signingConfigs.releaseSigning.storeFile = SigningKeystorePath(projectRoot, props['STORE_FILE'])
-			android.signingConfigs.releaseSigning.storePassword = props['STORE_PASSWORD']
-			android.signingConfigs.releaseSigning.keyAlias = props['KEY_ALIAS']
-			android.signingConfigs.releaseSigning.keyPassword = props['KEY_PASSWORD']
+			&& props.containsKey('KEY_ALIAS')) {
+
+			signingConfig.storeFile = SigningKeystorePath(projectRoot, props['STORE_FILE'])
+			signingConfig.storePassword = props['STORE_PASSWORD']
+			signingConfig.keyAlias = props['KEY_ALIAS']
+			
+			if(props.containsKey('STORE_PASSWORD'))
+				signingConfig.storePassword = props['STORE_PASSWORD']
+			else
+				signingConfig.storePassword = System.getenv("ANDROID_STORE_PASSWORD")
+			
+			if(props.containsKey('KEY_PASSWORD'))
+				signingConfig.keyPassword = props['KEY_PASSWORD']
+			else
+				signingConfig.keyPassword = System.getenv("ANDROID_KEY_PASSWORD")
 		} else {
 			println("Some props from signing file are missing")
-			android.buildTypes.release.signingConfig = null
+			android.signingConfigs.putAt(signingConfigKey, null)
 		}
 	} else {
 		println("file with signing properties is missing")
-		android.buildTypes.release.signingConfig = null
+		android.signingConfigs.putAt(signingConfigKey, null)
 	}
 }
 

+ 19 - 2
client/CMakeLists.txt

@@ -3,7 +3,7 @@ set(client_SRCS
 	../CCallback.cpp
 
 	adventureMap/CAdvMapPanel.cpp
-	adventureMap/CAdvMapInt.cpp
+	adventureMap/CAdventureMapInterface.cpp
 	adventureMap/CAdventureOptions.cpp
 	adventureMap/CInGameConsole.cpp
 	adventureMap/CInfoBar.cpp
@@ -31,6 +31,7 @@ set(client_SRCS
 	gui/CursorHandler.cpp
 	gui/InterfaceObjectConfigurable.cpp
 	gui/NotificationHandler.cpp
+	gui/ShortcutHandler.cpp
 
 	lobby/CBonusSelection.cpp
 	lobby/CCampaignInfoScreen.cpp
@@ -85,6 +86,12 @@ set(client_SRCS
 	widgets/MiscWidgets.cpp
 	widgets/ObjectLists.cpp
 	widgets/TextControls.cpp
+	widgets/CArtifactsOfHeroBase.cpp
+	widgets/CArtifactsOfHeroMain.cpp
+	widgets/CArtifactsOfHeroKingdom.cpp
+	widgets/CArtifactsOfHeroAltar.cpp
+	widgets/CArtifactsOfHeroMarket.cpp
+	widgets/CWindowWithArtifacts.cpp
 
 	windows/CCastleInterface.cpp
 	windows/CCreatureWindow.cpp
@@ -110,6 +117,7 @@ set(client_SRCS
 	CMT.cpp
 	CMusicHandler.cpp
 	CPlayerInterface.cpp
+	PlayerLocalState.cpp
 	CServerHandler.cpp
 	CVideoHandler.cpp
 	Client.cpp
@@ -122,7 +130,7 @@ set(client_HEADERS
 	StdInc.h
 
 	adventureMap/CAdvMapPanel.h
-	adventureMap/CAdvMapInt.h
+	adventureMap/CAdventureMapInterface.h
 	adventureMap/CAdventureOptions.h
 	adventureMap/CInGameConsole.h
 	adventureMap/CInfoBar.h
@@ -152,6 +160,8 @@ set(client_HEADERS
 	gui/InterfaceObjectConfigurable.h
 	gui/MouseButton.h
 	gui/NotificationHandler.h
+	gui/Shortcut.h
+	gui/ShortcutHandler.h
 	gui/TextAlignment.h
 
 	lobby/CBonusSelection.h
@@ -213,6 +223,12 @@ set(client_HEADERS
 	widgets/MiscWidgets.h
 	widgets/ObjectLists.h
 	widgets/TextControls.h
+	widgets/CArtifactsOfHeroBase.h
+	widgets/CArtifactsOfHeroMain.h
+	widgets/CArtifactsOfHeroKingdom.h
+	widgets/CArtifactsOfHeroAltar.h
+	widgets/CArtifactsOfHeroMarket.h
+	widgets/CWindowWithArtifacts.h
 
 	windows/CCastleInterface.h
 	windows/CCreatureWindow.h
@@ -238,6 +254,7 @@ set(client_HEADERS
 	CMT.h
 	CMusicHandler.h
 	CPlayerInterface.h
+	PlayerLocalState.h
 	CServerHandler.h
 	CVideoHandler.h
 	Client.h

+ 1 - 1
client/CMusicHandler.h

@@ -14,7 +14,7 @@
 
 struct _Mix_Music;
 struct SDL_RWops;
-typedef struct _Mix_Music Mix_Music;
+using Mix_Music = struct _Mix_Music;
 struct Mix_Chunk;
 
 class CAudioBase {

File diff suppressed because it is too large
+ 217 - 377
client/CPlayerInterface.cpp


+ 45 - 98
client/CPlayerInterface.h

@@ -9,16 +9,10 @@
  */
 #pragma once
 
-
 #include "../lib/FunctionList.h"
 #include "../lib/CGameInterface.h"
-#include "../lib/NetPacksBase.h"
 #include "gui/CIntObject.h"
 
-#ifdef __GNUC__
-#define sprintf_s snprintf
-#endif
-
 VCMI_LIB_NAMESPACE_BEGIN
 
 class Artifact;
@@ -37,7 +31,7 @@ struct CPathsInfo;
 VCMI_LIB_NAMESPACE_END
 
 class CButton;
-class CAdvMapInt;
+class CAdventureMapInterface;
 class CCastleInterface;
 class BattleInterface;
 class CComponent;
@@ -51,103 +45,63 @@ class ClickableR;
 class Hoverable;
 class KeyInterested;
 class MotionInterested;
+class PlayerLocalState;
 class TimeInterested;
 class IShowable;
 
-struct SDL_Surface;
-union SDL_Event;
-
 namespace boost
 {
 	class mutex;
 	class recursive_mutex;
 }
 
-class CPlayerInterface;
-
-class HeroPathStorage
-{
-	CPlayerInterface & owner;
-
-	std::map<const CGHeroInstance *, CGPath> paths; //maps hero => selected path in adventure map
-
-public:
-	explicit HeroPathStorage(CPlayerInterface &owner);
-
-	void setPath(const CGHeroInstance *h, const CGPath & path);
-	bool setPath(const CGHeroInstance *h, const int3 & destination);
-
-	const CGPath & getPath(const CGHeroInstance *h) const;
-	bool hasPath(const CGHeroInstance *h) const;
-
-	void removeLastNode(const CGHeroInstance *h);
-	void erasePath(const CGHeroInstance *h);
-	void verifyPath(const CGHeroInstance *h);
-
-	template <typename Handler>
-	void serialize( Handler &h, int version );
-};
-
 /// Central class for managing user interface logic
 class CPlayerInterface : public CGameInterface, public IUpdateable
 {
-public:
-	HeroPathStorage paths;
+	bool duringMovement;
+	bool ignoreEvents;
+	size_t numOfMovedArts;
+
+	// -1 - just loaded game; 1 - just started game; 0 otherwise
+	int firstCall;
+	int autosaveCount;
+	static const int SAVES_COUNT = 5;
+
+	std::pair<const CCreatureSet *, const CCreatureSet *> lastBattleArmies;
+	bool allowBattleReplay = false;
+	std::list<std::shared_ptr<CInfoWindow>> dialogs; //queue of dialogs awaiting to be shown (not currently shown!)
+	const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr)
 
-	std::shared_ptr<Environment> env;
 	ObjectInstanceID destinationTeleport; //contain -1 or object id if teleportation
 	int3 destinationTeleportPos;
 
+public: // TODO: make private
+	std::shared_ptr<Environment> env;
+
+	std::unique_ptr<PlayerLocalState> localState;
+
 	//minor interfaces
 	CondSh<bool> *showingDialog; //indicates if dialog box is displayed
 
 	static boost::recursive_mutex *pim;
 	bool makingTurn; //if player is already making his turn
-	int firstCall; // -1 - just loaded game; 1 - just started game; 0 otherwise
-	int autosaveCount;
-	static const int SAVES_COUNT = 5;
 
 	CCastleInterface * castleInt; //nullptr if castle window isn't opened
 	static std::shared_ptr<BattleInterface> battleInt; //nullptr if no battle
 	CInGameConsole * cingconsole;
 
 	std::shared_ptr<CCallback> cb; //to communicate with engine
-	const BattleAction *curAction; //during the battle - action currently performed by active stack (or nullptr)
-
-	std::list<std::shared_ptr<CInfoWindow>> dialogs; //queue of dialogs awaiting to be shown (not currently shown!)
-
-	std::vector<const CGHeroInstance *> wanderingHeroes; //our heroes on the adventure map (not the garrisoned ones)
-	std::vector<const CGTownInstance *> towns; //our towns on the adventure map
-	std::vector<const CGHeroInstance *> sleepingHeroes; //if hero is in here, he's sleeping
 
 	//During battle is quick combat mode is used
 	std::shared_ptr<CBattleGameInterface> autofightingAI; //AI that makes decisions
 	bool isAutoFightOn; //Flag, switch it to stop quick combat. Don't touch if there is no battle interface.
-	bool allowBattleReplay = false;
-	std::pair<const CCreatureSet *, const CCreatureSet *> lastBattleArmies;
 
-	struct SpellbookLastSetting
-	{
-		int spellbookLastPageBattle, spellbokLastPageAdvmap; //on which page we left spellbook
-		int spellbookLastTabBattle, spellbookLastTabAdvmap; //on which page we left spellbook
-
-		SpellbookLastSetting();
-		template <typename Handler> void serialize( Handler &h, const int version )
-		{
-			h & spellbookLastPageBattle;
-			h & spellbokLastPageAdvmap;
-			h & spellbookLastTabBattle;
-			h & spellbookLastTabAdvmap;
-		}
-	} spellbookSettings;
+protected: // Call-ins from server, should not be called directly, but only via GameInterface
 
 	void update() override;
-	void initializeHeroTownList();
-	int getLastIndex(std::string namePrefix);
+	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
 
-	//overridden funcs from CGameInterface
 	void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override;
-
 	void buildChanged(const CGTownInstance *town, BuildingID buildingID, int what) override; //what: 1 - built, 2 - demolished
 
 	void artifactPut(const ArtifactLocation &al) override;
@@ -172,22 +126,16 @@ public:
 	void receivedResource() override;
 	void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID) override;
 	void showRecruitmentDialog(const CGDwelling *dwelling, const CArmedInstance *dst, int level) override;
-	void showShipyardDialog(const IShipyard *obj) override; //obj may be town or shipyard;
 	void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID.
 	void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
 	void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override;
 	void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
-	void showPuzzleMap() override;
-	void viewWorldMap() override;
 	void showMarketWindow(const IMarket *market, const CGHeroInstance *visitor) override;
 	void showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor) override;
 	void showHillFortWindow(const CGObjectInstance *object, const CGHeroInstance *visitor) override;
-	void showTavernWindow(const CGObjectInstance *townOrTavern) override;
-	void showThievesGuildWindow (const CGObjectInstance * obj) override;
-	void showQuestLog() override;
 	void advmapSpellCast(const CGHeroInstance * caster, int spellID) override; //called when a hero casts a spell
-	void tileHidden(const std::unordered_set<int3, ShashInt3> &pos) override; //called when given tiles become hidden under fog of war
-	void tileRevealed(const std::unordered_set<int3, ShashInt3> &pos) override; //called when fog of war disappears from given tiles
+	void tileHidden(const std::unordered_set<int3> &pos) override; //called when given tiles become hidden under fog of war
+	void tileRevealed(const std::unordered_set<int3> &pos) override; //called when fog of war disappears from given tiles
 	void newObject(const CGObjectInstance * obj) override;
 	void availableArtifactsChanged(const CGBlackMarket *bm = nullptr) override; //bm may be nullptr, then artifacts are changed in the global pool (used by merchants in towns)
 	void yourTurn() override;
@@ -230,21 +178,23 @@ public:
 	void yourTacticPhase(int distance) override;
 	void forceEndTacticPhase() override;
 
-	//-------------//
+public: // public interface for use by client via LOCPLINT access
+
+	// part of GameInterface that is also used by client code
+	void showPuzzleMap() override;
+	void viewWorldMap() override;
+	void showQuestLog() override;
+	void showThievesGuildWindow (const CGObjectInstance * obj) override;
+	void showTavernWindow(const CGObjectInstance *townOrTavern) override;
+	void showShipyardDialog(const IShipyard *obj) override; //obj may be town or shipyard;
+
+	void showHeroExchange(ObjectInstanceID hero1, ObjectInstanceID hero2);
 	void showArtifactAssemblyDialog(const Artifact * artifact, const Artifact * assembledArtifact, CFunctionList<bool()> onYes);
-	void garrisonsChanged(std::vector<const CGObjectInstance *> objs);
-	void garrisonChanged(const CGObjectInstance * obj);
-	void heroKilled(const CGHeroInstance* hero);
 	void waitWhileDialog(bool unlockPim = true);
 	void waitForAllDialogs(bool unlockPim = true);
-	void redrawHeroWin(const CGHeroInstance * hero);
 	void openTownWindow(const CGTownInstance * town); //shows townscreen
 	void openHeroWindow(const CGHeroInstance * hero); //shows hero window with given hero
-	void updateInfo(const CGObjectInstance * specific);
-	void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
-	void activateForSpectator(); // TODO: spectator probably need own player interface class
 
-	// show dialogs
 	void showInfoDialog(const std::string &text, std::shared_ptr<CComponent> component);
 	void showInfoDialog(const std::string &text, const std::vector<std::shared_ptr<CComponent>> & components = std::vector<std::shared_ptr<CComponent>>(), int soundID = 0);
 	void showInfoDialogAndWait(std::vector<Component> & components, const MetaString & text);
@@ -253,10 +203,8 @@ public:
 	void stopMovement();
 	void moveHero(const CGHeroInstance *h, const CGPath& path);
 
-	void acceptTurn(); //used during hot seat after your turn message is close
 	void tryDiggging(const CGHeroInstance *h);
 	void showShipyardDialogOrProblemPopup(const IShipyard *obj); //obj may be town or shipyard;
-	void requestReturningToMainMenu(bool won);
 	void proposeLoadingGame();
 
 	///returns true if all events are processed internally
@@ -266,11 +214,6 @@ public:
 	~CPlayerInterface();
 
 private:
-
-	template <typename Handler> void serializeTempl(Handler &h, const int version);
-
-private:
-
 	struct IgnoreEvents
 	{
 		CPlayerInterface & owner;
@@ -285,14 +228,18 @@ private:
 
 	};
 
-
-
-	bool duringMovement;
-	bool ignoreEvents;
-	size_t numOfMovedArts;
-
+	void heroKilled(const CGHeroInstance* hero);
+	void garrisonsChanged(std::vector<const CGObjectInstance *> objs);
+	void requestReturningToMainMenu(bool won);
+	void acceptTurn(); //used during hot seat after your turn message is close
+	void initializeHeroTownList();
+	int getLastIndex(std::string namePrefix);
 	void doMoveHero(const CGHeroInstance *h, CGPath path);
 	void setMovementStatus(bool value);
+
+	/// Performs autosave, if needed according to settings
+	void performAutosave();
 };
 
+/// Provides global access to instance of interface of currently active player
 extern CPlayerInterface * LOCPLINT;

+ 2 - 1
client/Client.cpp

@@ -15,7 +15,7 @@
 #include "CPlayerInterface.h"
 #include "CServerHandler.h"
 #include "ClientNetPackVisitors.h"
-#include "adventureMap/CAdvMapInt.h"
+#include "adventureMap/CAdventureMapInterface.h"
 #include "battle/BattleInterface.h"
 #include "gui/CGuiHandler.h"
 #include "mapView/mapHandler.h"
@@ -374,6 +374,7 @@ void CClient::endGame()
 	//threads cleanup has to be after gs cleanup and before battleints cleanup to stop tacticThread
 	cleanThreads();
 
+	CPlayerInterface::battleInt.reset();
 	playerint.clear();
 	battleints.clear();
 	battleCallbacks.clear();

+ 3 - 3
client/Client.h

@@ -61,8 +61,8 @@ namespace boost { class thread; }
 template<typename T>
 class ThreadSafeVector
 {
-	typedef std::vector<T> TVector;
-	typedef boost::unique_lock<boost::mutex> TLock;
+	using TVector = std::vector<T>;
+	using TLock = boost::unique_lock<boost::mutex>;
 	TVector items;
 	boost::mutex mx;
 	boost::condition_variable cond;
@@ -235,7 +235,7 @@ public:
 	void castSpell(const spells::Caster * caster, SpellID spellID, const int3 &pos) override {};
 
 	void changeFogOfWar(int3 center, ui32 radius, PlayerColor player, bool hide) override {}
-	void changeFogOfWar(std::unordered_set<int3, ShashInt3> & tiles, PlayerColor player, bool hide) override {}
+	void changeFogOfWar(std::unordered_set<int3> & tiles, PlayerColor player, bool hide) override {}
 
 	void setObjProperty(ObjectInstanceID objid, int prop, si64 val) override {}
 

+ 6 - 7
client/ClientCommandManager.cpp

@@ -8,13 +8,12 @@
  *
  */
 #include "StdInc.h"
-
 #include "ClientCommandManager.h"
 
 #include "Client.h"
 #include "adventureMap/CInGameConsole.h"
-#include "adventureMap/CAdvMapInt.h"
 #include "CPlayerInterface.h"
+#include "PlayerLocalState.h"
 #include "CServerHandler.h"
 #include "gui/CGuiHandler.h"
 #include "../lib/NetPacks.h"
@@ -387,12 +386,12 @@ void ClientCommandManager::handleBonusesCommand(std::istringstream & singleWordB
 		ss << b;
 		return ss.str();
 	};
-	printCommandMessage("Bonuses of " + adventureInt->curArmy()->getObjectName() + "\n");
-	printCommandMessage(format(adventureInt->curArmy()->getBonusList()) + "\n");
+		printCommandMessage("Bonuses of " + LOCPLINT->localState->getCurrentArmy()->getObjectName() + "\n");
+		printCommandMessage(format(LOCPLINT->localState->getCurrentArmy()->getBonusList()) + "\n");
 
 	printCommandMessage("\nInherited bonuses:\n");
 	TCNodes parents;
-	adventureInt->curArmy()->getParents(parents);
+		LOCPLINT->localState->getCurrentArmy()->getParents(parents);
 	for(const CBonusSystemNode *parent : parents)
 	{
 		printCommandMessage(std::string("\nBonuses from ") + typeid(*parent).name() + "\n" + format(*parent->getAllBonuses(Selector::all, Selector::all)) + "\n");
@@ -416,7 +415,7 @@ void ClientCommandManager::handleTellCommand(std::istringstream& singleWordBuffe
 
 void ClientCommandManager::handleMpCommand()
 {
-	if(const CGHeroInstance* h = adventureInt->curHero())
+	if(const CGHeroInstance* h = LOCPLINT->localState->getCurrentHero())
 		printCommandMessage(std::to_string(h->movement) + "; max: " + std::to_string(h->maxMovePoints(true)) + "/" + std::to_string(h->maxMovePoints(false)) + "\n");
 }
 
@@ -602,7 +601,7 @@ void ClientCommandManager::processCommand(const std::string & message, bool call
 	else if(commandName == "tell")
 		handleTellCommand(singleWordBuffer);
 
-	else if(commandName == "mp" && adventureInt)
+	else if(commandName == "mp" && LOCPLINT)
 		handleMpCommand();
 
 	else if (commandName == "set")

+ 3 - 3
client/NetPacksClient.cpp

@@ -706,15 +706,15 @@ void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack &
 
 	const CStack *activated = gs.curB->battleGetStackByID(pack.stack);
 	PlayerColor playerToCall; //pack.player that will move activated stack
-	if (activated->hasBonusOfType(Bonus::HYPNOTIZED))
+	if (activated->hasBonusOfType(BonusType::HYPNOTIZED))
 	{
-		playerToCall = (gs.curB->sides[0].color == activated->owner
+		playerToCall = (gs.curB->sides[0].color == activated->unitOwner()
 			? gs.curB->sides[1].color
 			: gs.curB->sides[0].color);
 	}
 	else
 	{
-		playerToCall = activated->owner;
+		playerToCall = activated->unitOwner();
 	}
 
 	cl.startPlayerBattleAction(playerToCall);

+ 268 - 0
client/PlayerLocalState.cpp

@@ -0,0 +1,268 @@
+/*
+ * PlayerLocalState.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "PlayerLocalState.h"
+
+#include "../CCallback.h"
+#include "../lib/CPathfinder.h"
+#include "../lib/mapObjects/CGHeroInstance.h"
+#include "../lib/mapObjects/CGTownInstance.h"
+#include "CPlayerInterface.h"
+#include "adventureMap/CAdventureMapInterface.h"
+
+PlayerLocalState::PlayerLocalState(CPlayerInterface & owner)
+	: owner(owner)
+	, currentSelection(nullptr)
+{
+}
+
+void PlayerLocalState::saveHeroPaths(std::map<const CGHeroInstance *, int3> & pathsMap)
+{
+	for(auto & p : paths)
+	{
+		if(p.second.nodes.size())
+			pathsMap[p.first] = p.second.endPos();
+		else
+			logGlobal->debug("%s has assigned an empty path! Ignoring it...", p.first->getNameTranslated());
+	}
+}
+
+void PlayerLocalState::loadHeroPaths(std::map<const CGHeroInstance *, int3> & pathsMap)
+{
+	if(owner.cb)
+	{
+		for(auto & p : pathsMap)
+		{
+			CGPath path;
+			owner.cb->getPathsInfo(p.first)->getPath(path, p.second);
+			paths[p.first] = path;
+			logGlobal->trace("Restored path for hero %s leading to %s with %d nodes", p.first->nodeName(), p.second.toString(), path.nodes.size());
+		}
+	}
+}
+
+void PlayerLocalState::setPath(const CGHeroInstance * h, const CGPath & path)
+{
+	paths[h] = path;
+}
+
+const CGPath & PlayerLocalState::getPath(const CGHeroInstance * h) const
+{
+	assert(hasPath(h));
+	return paths.at(h);
+}
+
+bool PlayerLocalState::hasPath(const CGHeroInstance * h) const
+{
+	return paths.count(h) > 0;
+}
+
+bool PlayerLocalState::setPath(const CGHeroInstance * h, const int3 & destination)
+{
+	CGPath path;
+	if(!owner.cb->getPathsInfo(h)->getPath(path, destination))
+		return false;
+
+	setPath(h, path);
+	return true;
+}
+
+void PlayerLocalState::removeLastNode(const CGHeroInstance * h)
+{
+	assert(hasPath(h));
+	if(!hasPath(h))
+		return;
+
+	auto & path = paths[h];
+	path.nodes.pop_back();
+	if(path.nodes.size() < 2) //if it was the last one, remove entire path and path with only one tile is not a real path
+		erasePath(h);
+}
+
+void PlayerLocalState::erasePath(const CGHeroInstance * h)
+{
+	paths.erase(h);
+	adventureInt->onHeroChanged(h);
+}
+
+void PlayerLocalState::verifyPath(const CGHeroInstance * h)
+{
+	if(!hasPath(h))
+		return;
+	setPath(h, getPath(h).endPos());
+}
+
+const CGHeroInstance * PlayerLocalState::getCurrentHero() const
+{
+	if(currentSelection && currentSelection->ID == Obj::HERO)
+		return dynamic_cast<const CGHeroInstance *>(currentSelection);
+	else
+		return nullptr;
+}
+
+const CGHeroInstance * PlayerLocalState::getNextWanderingHero(const CGHeroInstance * currentHero)
+{
+	bool currentHeroFound = false;
+	const CGHeroInstance * firstSuitable = nullptr;
+	const CGHeroInstance * nextSuitable = nullptr;
+
+	for(const auto * hero : getWanderingHeroes())
+	{
+		if (hero == currentHero)
+		{
+			currentHeroFound = true;
+			continue;
+		}
+
+		if (isHeroSleeping(hero))
+			continue;
+
+		if (hero->movement == 0)
+			continue;
+
+		if (!firstSuitable)
+			firstSuitable = hero;
+
+		if (!nextSuitable && currentHeroFound)
+			nextSuitable = hero;
+	}
+
+	// if we found suitable hero after currently selected hero -> return this hero
+	if (nextSuitable)
+		return nextSuitable;
+
+	// othervice -> loop over and return first suitable hero in the list (or null if none)
+	return firstSuitable;
+}
+
+const CGTownInstance * PlayerLocalState::getCurrentTown() const
+{
+	if(currentSelection && currentSelection->ID == Obj::TOWN)
+		return dynamic_cast<const CGTownInstance *>(currentSelection);
+	else
+		return nullptr;
+}
+
+const CArmedInstance * PlayerLocalState::getCurrentArmy() const
+{
+	if(currentSelection)
+		return dynamic_cast<const CArmedInstance *>(currentSelection);
+	else
+		return nullptr;
+}
+
+void PlayerLocalState::setSelection(const CArmedInstance * selection)
+{
+	if (currentSelection == selection)
+		return;
+
+	currentSelection = selection;
+
+	if (selection)
+		adventureInt->onSelectionChanged(selection);
+}
+
+bool PlayerLocalState::isHeroSleeping(const CGHeroInstance * hero) const
+{
+	return vstd::contains(sleepingHeroes, hero);
+}
+
+void PlayerLocalState::setHeroAsleep(const CGHeroInstance * hero)
+{
+	assert(hero);
+	assert(vstd::contains(wanderingHeroes, hero));
+	assert(!vstd::contains(sleepingHeroes, hero));
+
+	sleepingHeroes.push_back(hero);
+}
+
+void PlayerLocalState::setHeroAwaken(const CGHeroInstance * hero)
+{
+	assert(hero);
+	assert(vstd::contains(wanderingHeroes, hero));
+	assert(vstd::contains(sleepingHeroes, hero));
+
+	vstd::erase(sleepingHeroes, hero);
+}
+
+const std::vector<const CGHeroInstance *> & PlayerLocalState::getWanderingHeroes()
+{
+	return wanderingHeroes;
+}
+
+const CGHeroInstance * PlayerLocalState::getWanderingHero(size_t index)
+{
+	if(index < wanderingHeroes.size())
+		return wanderingHeroes[index];
+	return nullptr;
+}
+
+void PlayerLocalState::addWanderingHero(const CGHeroInstance * hero)
+{
+	assert(hero);
+	assert(!vstd::contains(wanderingHeroes, hero));
+	wanderingHeroes.push_back(hero);
+}
+
+void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero)
+{
+	assert(hero);
+	assert(vstd::contains(wanderingHeroes, hero));
+
+	if (hero == currentSelection)
+	{
+		auto const * nextHero = getNextWanderingHero(hero);
+		setSelection(nextHero);
+	}
+
+	vstd::erase(wanderingHeroes, hero);
+	vstd::erase(sleepingHeroes, hero);
+
+	if (currentSelection == nullptr && !wanderingHeroes.empty())
+		setSelection(wanderingHeroes.front());
+
+	if (currentSelection == nullptr && !ownedTowns.empty())
+		setSelection(ownedTowns.front());
+}
+
+const std::vector<const CGTownInstance *> & PlayerLocalState::getOwnedTowns()
+{
+	return ownedTowns;
+}
+
+const CGTownInstance * PlayerLocalState::getOwnedTown(size_t index)
+{
+	if(index < ownedTowns.size())
+		return ownedTowns[index];
+	return nullptr;
+}
+
+void PlayerLocalState::addOwnedTown(const CGTownInstance * town)
+{
+	assert(town);
+	assert(!vstd::contains(ownedTowns, town));
+	ownedTowns.push_back(town);
+}
+
+void PlayerLocalState::removeOwnedTown(const CGTownInstance * town)
+{
+	assert(town);
+	assert(vstd::contains(ownedTowns, town));
+	vstd::erase(ownedTowns, town);
+
+	if (town == currentSelection)
+		setSelection(nullptr);
+
+	if (currentSelection == nullptr && !wanderingHeroes.empty())
+		setSelection(wanderingHeroes.front());
+
+	if (currentSelection == nullptr && !ownedTowns.empty())
+		setSelection(ownedTowns.front());
+}

+ 111 - 0
client/PlayerLocalState.h

@@ -0,0 +1,111 @@
+/*
+ * PlayerLocalState.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class CGHeroInstance;
+class CGTownInstance;
+class CArmedInstance;
+struct CGPath;
+class int3;
+
+VCMI_LIB_NAMESPACE_END
+
+class CPlayerInterface;
+
+/// Class that contains potentially serializeable state of a local player
+class PlayerLocalState
+{
+	CPlayerInterface & owner;
+
+	/// Currently selected object, can be town, hero or null
+	const CArmedInstance * currentSelection;
+
+	std::map<const CGHeroInstance *, CGPath> paths; //maps hero => selected path in adventure map
+	std::vector<const CGHeroInstance *> sleepingHeroes; //if hero is in here, he's sleeping
+	std::vector<const CGHeroInstance *> wanderingHeroes; //our heroes on the adventure map (not the garrisoned ones)
+	std::vector<const CGTownInstance *> ownedTowns; //our towns on the adventure map
+
+	void saveHeroPaths(std::map<const CGHeroInstance *, int3> & paths);
+	void loadHeroPaths(std::map<const CGHeroInstance *, int3> & paths);
+
+public:
+	struct SpellbookLastSetting
+	{
+		//on which page we left spellbook
+		int spellbookLastPageBattle = 0;
+		int spellbokLastPageAdvmap = 0;
+		int spellbookLastTabBattle = 4;
+		int spellbookLastTabAdvmap = 4;
+
+		template<typename Handler>
+		void serialize(Handler & h, const int version)
+		{
+			h & spellbookLastPageBattle;
+			h & spellbokLastPageAdvmap;
+			h & spellbookLastTabBattle;
+			h & spellbookLastTabAdvmap;
+		}
+	} spellbookSettings;
+
+	explicit PlayerLocalState(CPlayerInterface & owner);
+
+	bool isHeroSleeping(const CGHeroInstance * hero) const;
+	void setHeroAsleep(const CGHeroInstance * hero);
+	void setHeroAwaken(const CGHeroInstance * hero);
+
+	const std::vector<const CGTownInstance *> & getOwnedTowns();
+	const CGTownInstance * getOwnedTown(size_t index);
+	void addOwnedTown(const CGTownInstance * hero);
+	void removeOwnedTown(const CGTownInstance * hero);
+
+	const std::vector<const CGHeroInstance *> & getWanderingHeroes();
+	const CGHeroInstance * getWanderingHero(size_t index);
+	const CGHeroInstance * getNextWanderingHero(const CGHeroInstance * hero);
+	void addWanderingHero(const CGHeroInstance * hero);
+	void removeWanderingHero(const CGHeroInstance * hero);
+
+	void setPath(const CGHeroInstance * h, const CGPath & path);
+	bool setPath(const CGHeroInstance * h, const int3 & destination);
+
+	const CGPath & getPath(const CGHeroInstance * h) const;
+	bool hasPath(const CGHeroInstance * h) const;
+
+	void removeLastNode(const CGHeroInstance * h);
+	void erasePath(const CGHeroInstance * h);
+	void verifyPath(const CGHeroInstance * h);
+
+	/// Returns currently selected object
+	const CGHeroInstance * getCurrentHero() const;
+	const CGTownInstance * getCurrentTown() const;
+	const CArmedInstance * getCurrentArmy() const;
+
+	/// Changes currently selected object
+	void setSelection(const CArmedInstance *sel);
+
+	template<typename Handler>
+	void serialize(Handler & h, int version)
+	{
+		//WARNING: this code is broken and not used. See CClient::loadGame
+		std::map<const CGHeroInstance *, int3> pathsMap; //hero -> dest
+		if(h.saving)
+			saveHeroPaths(pathsMap);
+
+		h & pathsMap;
+
+		if(!h.saving)
+			loadHeroPaths(pathsMap);
+
+		h & ownedTowns;
+		h & wanderingHeroes;
+		h & sleepingHeroes;
+	}
+};

File diff suppressed because it is too large
+ 248 - 350
client/adventureMap/CAdventureMapInterface.cpp


+ 72 - 66
client/adventureMap/CAdvMapInt.h → client/adventureMap/CAdventureMapInterface.h

@@ -11,9 +11,6 @@
 
 #include "../gui/CIntObject.h"
 
-#include "../../lib/int3.h"
-#include "../../lib/GameConstants.h"
-
 VCMI_LIB_NAMESPACE_BEGIN
 
 class CGObjectInstance;
@@ -23,6 +20,8 @@ class CArmedInstance;
 class IShipyard;
 struct CGPathNode;
 struct ObjectPosInfo;
+struct Component;
+class int3;
 
 VCMI_LIB_NAMESPACE_END
 
@@ -43,38 +42,28 @@ class MapAudioPlayer;
 
 struct MapDrawingInfo;
 
-enum class EAdvMapMode
-{
-	NORMAL,
-	WORLD_VIEW
-};
-
 /// That's a huge class which handles general adventure map actions and
 /// shows the right menu(questlog, spellbook, end turn,..) from where you
 /// can get to the towns and heroes.
-class CAdvMapInt : public CIntObject
+class CAdventureMapInterface : public CIntObject
 {
-	//TODO: remove
-	friend class CPlayerInterface;
-
 private:
-	enum EDirections {LEFT=1, RIGHT=2, UP=4, DOWN=8};
-	enum EGameStates {NA, INGAME, WAITING};
+	enum class EGameState
+	{
+		NOT_INITIALIZED,
+		HOTSEAT_WAIT,
+		MAKING_TURN,
+		ENEMY_TURN,
+		WORLD_VIEW
+	};
 
-	EGameStates state;
-	EAdvMapMode mode;
-
-	/// Currently selected object, can be town, hero or null
-	const CArmedInstance *selection;
+	EGameState state;
 
 	/// currently acting player
-	PlayerColor player;
-
-	bool duringAITurn;
+	PlayerColor currentPlayerID;
 
 	/// uses EDirections enum
-	ui8 scrollingDir;
-	bool scrollingState;
+	bool scrollingCursorSet;
 
 	const CSpell *spellBeingCasted; //nullptr if none
 
@@ -127,15 +116,15 @@ private:
 	void fnextHero();
 	void fendTurn();
 
-	void setScrollingCursor(ui8 direction) const;
-	void selectionChanged();
+	void hotkeyMoveHeroDirectional(Point direction);
+
 	bool isActive();
 	void adjustActiveness(bool aiTurnStart); //should be called every time at AI/human turn transition; blocks GUI during AI turn
 
 	const IShipyard * ourInaccessibleShipyard(const CGObjectInstance *obj) const; //checks if obj is our ashipyard and cursor is 0,0 -> returns shipyard or nullptr else
-	//button updates
-	void updateSleepWake(const CGHeroInstance *h);
-	void updateSpellbook(const CGHeroInstance *h);
+
+	// update locked state of buttons
+	void updateButtons();
 
 	void handleMapScrollingUpdate();
 
@@ -143,11 +132,17 @@ private:
 
 	const CGObjectInstance *getActiveObject(const int3 &tile);
 
-	std::optional<Point> keyToMoveDirection(const SDL_Keycode & key);
+	std::optional<Point> keyToMoveDirection(EShortcut key);
 
-public:
-	CAdvMapInt();
+	void endingTurn();
 
+	/// exits currently opened world view mode and returns to normal map
+	void exitWorldView();
+	void exitCastingMode();
+	void leaveCastingMode(const int3 & castTarget);
+	void abortCastingMode();
+
+protected:
 	// CIntObject interface implementation
 
 	void activate() override;
@@ -156,59 +151,70 @@ public:
 	void show(SDL_Surface * to) override;
 	void showAll(SDL_Surface * to) override;
 
-	void keyPressed(const SDL_Keycode & key) override;
-	void keyReleased(const SDL_Keycode & key) override;
-	void mouseMoved (const Point & cursorPosition) override;
+	void keyPressed(EShortcut key) override;
 
-	// public interface
+public:
+	CAdventureMapInterface();
 
-	/// called by MapView whenever currently visible area changes
-	/// visibleArea describen now visible map section measured in tiles
-	void onMapViewMoved(const Rect & visibleArea, int mapLevel);
+	/// Called by PlayerInterface when specified player is ready to start his turn
+	void onHotseatWaitStarted(PlayerColor playerID);
+
+	/// Called by PlayerInterface when AI or remote human player starts his turn
+	void onEnemyTurnStarted(PlayerColor playerID);
+
+	/// Called by PlayerInterface when local human player starts his turn
+	void onPlayerTurnStarted(PlayerColor playerID);
 
-	/// Called when map audio should be paused, e.g. on combat or town scren access
+	/// Called by PlayerInterface when interface should be switched to specified player without starting turn
+	void onCurrentPlayerChanged(PlayerColor playerID);
+
+	/// Called by PlayerInterface when specific map tile changed and must be updated on minimap
+	void onMapTilesChanged(boost::optional<std::unordered_set<int3>> positions);
+
+	/// Called by PlayerInterface when hero starts movement
+	void onHeroMovementStarted(const CGHeroInstance * hero);
+
+	/// Called by PlayerInterface when hero state changed and hero list must be updated
+	void onHeroChanged(const CGHeroInstance * hero);
+
+	/// Called by PlayerInterface when town state changed and town list must be updated
+	void onTownChanged(const CGTownInstance * town);
+
+	/// Called when currently selected object changes
+	void onSelectionChanged(const CArmedInstance *sel);
+
+	/// Called when map audio should be paused, e.g. on combat or town screen access
 	void onAudioPaused();
 
 	/// Called when map audio should be resume, opposite to onPaused
 	void onAudioResumed();
 
-	void select(const CArmedInstance *sel, bool centerView = true);
+	/// Requests to display provided information inside infobox
+	void showInfoBoxMessage(const std::vector<Component> & components, std::string message, int timer);
+
+	/// Changes position on map to center selected location
 	void centerOnTile(int3 on);
 	void centerOnObject(const CGObjectInstance *obj);
 
-	bool isHeroSleeping(const CGHeroInstance *hero);
-	void setHeroSleeping(const CGHeroInstance *hero, bool sleep);
-	int getNextHeroIndex(int startIndex); //for Next Hero button - cycles awake heroes with movement only
-
-	void setPlayer(PlayerColor Player);
-	void startHotSeatWait(PlayerColor Player);
-	void startTurn();
-	void initializeNewTurn();
-	void endingTurn();
-	void aiTurnStarted();
-
-	void quickCombatLock(); //should be called when quick battle started
-	void quickCombatUnlock();
+	/// called by MapView whenever currently visible area changes
+	/// visibleArea describes now visible map section measured in tiles
+	void onMapViewMoved(const Rect & visibleArea, int mapLevel);
 
+	/// called by MapView whenever tile is clicked
 	void onTileLeftClicked(const int3 & mapPos);
+
+	/// called by MapView whenever tile is hovered
 	void onTileHovered(const int3 & mapPos);
+
+	/// called by MapView whenever tile is clicked
 	void onTileRightClicked(const int3 & mapPos);
 
+	/// called by spell window when spell to cast has been selected
 	void enterCastingMode(const CSpell * sp);
-	void leaveCastingMode(bool cast = false, int3 dest = int3(-1, -1, -1));
-	const CGHeroInstance * curHero() const;
-	const CGTownInstance * curTown() const;
-	const CArmedInstance * curArmy() const;
-
-	void updateMoveHero(const CGHeroInstance *h, tribool hasPath = boost::logic::indeterminate);
-	void updateNextHero(const CGHeroInstance *h);
 
 	/// returns area of screen covered by terrain (main game area)
 	Rect terrainAreaPixels() const;
 
-	/// exits currently opened world view mode and returns to normal map
-	void exitWorldView();
-
 	/// opens world view at default scale
 	void openWorldView();
 
@@ -219,4 +225,4 @@ public:
 	void openWorldView(const std::vector<ObjectPosInfo>& objectPositions, bool showTerrain);
 };
 
-extern std::shared_ptr<CAdvMapInt> adventureInt;
+extern std::shared_ptr<CAdventureMapInterface> adventureInt;

+ 8 - 9
client/adventureMap/CAdventureOptions.cpp

@@ -11,13 +11,13 @@
 #include "StdInc.h"
 #include "CAdventureOptions.h"
 
-#include "CAdvMapInt.h"
-
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
+#include "../PlayerLocalState.h"
 #include "../lobby/CCampaignInfoScreen.h"
 #include "../lobby/CScenarioInfoScreen.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../widgets/Buttons.h"
 
 #include "../../CCallback.h"
@@ -28,20 +28,19 @@ CAdventureOptions::CAdventureOptions()
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
-	viewWorld = std::make_shared<CButton>(Point(24, 23), "ADVVIEW.DEF", CButton::tooltip(), [&](){ close(); }, SDLK_v);
+	viewWorld = std::make_shared<CButton>(Point(24, 23), "ADVVIEW.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_WORLD);
 	viewWorld->addCallback( [] { LOCPLINT->viewWorldMap(); });
 
-	exit = std::make_shared<CButton>(Point(204, 313), "IOK6432.DEF", CButton::tooltip(), std::bind(&CAdventureOptions::close, this), SDLK_RETURN);
-	exit->assignedKeys.insert(SDLK_ESCAPE);
+	exit = std::make_shared<CButton>(Point(204, 313), "IOK6432.DEF", CButton::tooltip(), std::bind(&CAdventureOptions::close, this), EShortcut::GLOBAL_RETURN);
 
-	scenInfo = std::make_shared<CButton>(Point(24, 198), "ADVINFO.DEF", CButton::tooltip(), [&](){ close(); }, SDLK_i);
+	scenInfo = std::make_shared<CButton>(Point(24, 198), "ADVINFO.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_SCENARIO);
 	scenInfo->addCallback(CAdventureOptions::showScenarioInfo);
 
-	puzzle = std::make_shared<CButton>(Point(24, 81), "ADVPUZ.DEF", CButton::tooltip(), [&](){ close(); }, SDLK_p);
+	puzzle = std::make_shared<CButton>(Point(24, 81), "ADVPUZ.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_VIEW_PUZZLE);
 	puzzle->addCallback(std::bind(&CPlayerInterface::showPuzzleMap, LOCPLINT));
 
-	dig = std::make_shared<CButton>(Point(24, 139), "ADVDIG.DEF", CButton::tooltip(), [&](){ close(); }, SDLK_d);
-	if(const CGHeroInstance *h = adventureInt->curHero())
+	dig = std::make_shared<CButton>(Point(24, 139), "ADVDIG.DEF", CButton::tooltip(), [&](){ close(); }, EShortcut::ADVENTURE_DIG_GRAIL);
+	if(const CGHeroInstance *h = LOCPLINT->localState->getCurrentHero())
 		dig->addCallback(std::bind(&CPlayerInterface::tryDiggging, LOCPLINT, h));
 	else
 		dig->block(true);

+ 21 - 25
client/adventureMap/CInGameConsole.cpp

@@ -14,9 +14,10 @@
 #include "../CGameInfo.h"
 #include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
+#include "../PlayerLocalState.h"
 #include "../ClientCommandManager.h"
-#include "../adventureMap/CAdvMapInt.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../render/Colors.h"
 
 #include "../../CCallback.h"
@@ -108,30 +109,29 @@ void CInGameConsole::print(const std::string & txt)
 	GH.totalRedraw(); // FIXME: ingame console has no parent widget set
 }
 
-void CInGameConsole::keyPressed (const SDL_Keycode & key)
+void CInGameConsole::keyPressed (EShortcut key)
 {
 	if (LOCPLINT->cingconsole != this)
 		return;
 
-	if(!captureAllKeys && key != SDLK_TAB)
+	if(!captureAllKeys && key != EShortcut::GAME_ACTIVATE_CONSOLE)
 		return; //because user is not entering any text
 
 	switch(key)
 	{
-	case SDLK_TAB:
-	case SDLK_ESCAPE:
-		{
-			if(captureAllKeys)
-			{
-				endEnteringText(false);
-			}
-			else if(SDLK_TAB == key)
-			{
-				startEnteringText();
-			}
-			break;
-		}
-	case SDLK_RETURN: //enter key
+	case EShortcut::GLOBAL_CANCEL:
+		if(captureAllKeys)
+			endEnteringText(false);
+		break;
+
+	case EShortcut::GAME_ACTIVATE_CONSOLE:
+		if(captureAllKeys)
+			endEnteringText(false);
+		else
+			startEnteringText();
+		break;
+
+	case EShortcut::GLOBAL_ACCEPT:
 		{
 			if(!enteredText.empty() && captureAllKeys)
 			{
@@ -145,7 +145,7 @@ void CInGameConsole::keyPressed (const SDL_Keycode & key)
 			}
 			break;
 		}
-	case SDLK_BACKSPACE:
+	case EShortcut::GLOBAL_BACKSPACE:
 		{
 			if(enteredText.size() > 1)
 			{
@@ -155,7 +155,7 @@ void CInGameConsole::keyPressed (const SDL_Keycode & key)
 			}
 			break;
 		}
-	case SDLK_UP: //up arrow
+	case EShortcut::MOVE_UP:
 		{
 			if(previouslyEntered.empty())
 				break;
@@ -174,7 +174,7 @@ void CInGameConsole::keyPressed (const SDL_Keycode & key)
 			}
 			break;
 		}
-	case SDLK_DOWN: //down arrow
+	case EShortcut::MOVE_DOWN:
 		{
 			if(prevEntDisp != -1 && prevEntDisp+1 < previouslyEntered.size())
 			{
@@ -190,10 +190,6 @@ void CInGameConsole::keyPressed (const SDL_Keycode & key)
 			}
 			break;
 		}
-	default:
-		{
-			break;
-		}
 	}
 }
 
@@ -259,7 +255,7 @@ void CInGameConsole::endEnteringText(bool processEnteredText)
 			clientCommandThread.detach();
 		}
 		else
-			LOCPLINT->cb->sendMessage(txt, adventureInt->curArmy());
+			LOCPLINT->cb->sendMessage(txt, LOCPLINT->localState->getCurrentArmy());
 	}
 	enteredText.clear();
 

+ 1 - 1
client/adventureMap/CInGameConsole.h

@@ -47,7 +47,7 @@ public:
 	void tick(uint32_t msPassed) override;
 	void show(SDL_Surface * to) override;
 	void showAll(SDL_Surface * to) override;
-	void keyPressed(const SDL_Keycode & key) override;
+	void keyPressed(EShortcut key) override;
 	void textInputed(const std::string & enteredText) override;
 	void textEdited(const std::string & enteredText) override;
 

+ 7 - 6
client/adventureMap/CInfoBar.cpp

@@ -11,7 +11,7 @@
 #include "StdInc.h"
 #include "CInfoBar.h"
 
-#include "CAdvMapInt.h"
+#include "CAdventureMapInterface.h"
 
 #include "../widgets/CComponent.h"
 #include "../widgets/Images.h"
@@ -22,6 +22,7 @@
 #include "../CGameInfo.h"
 #include "../CMusicHandler.h"
 #include "../CPlayerInterface.h"
+#include "../PlayerLocalState.h"
 #include "../gui/CGuiHandler.h"
 
 #include "../../CCallback.h"
@@ -114,7 +115,7 @@ CInfoBar::VisibleGameStatusInfo::VisibleGameStatusInfo()
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 	//get amount of halls of each level
 	std::vector<int> halls(4, 0);
-	for(auto town : LOCPLINT->towns)
+	for(auto town : LOCPLINT->localState->getOwnedTowns())
 	{
 		int hallLevel = town->hallLevel();
 		//negative value means no village hall, unlikely but possible
@@ -237,15 +238,15 @@ void CInfoBar::reset()
 void CInfoBar::showSelection()
 {
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);
-	if(adventureInt->curHero())
+	if(LOCPLINT->localState->getCurrentHero())
 	{
-		showHeroSelection(adventureInt->curHero());
+		showHeroSelection(LOCPLINT->localState->getCurrentHero());
 		return;
 	}
 
-	if(adventureInt->curTown())
+	if(LOCPLINT->localState->getCurrentTown())
 	{
-		showTownSelection(adventureInt->curTown());
+		showTownSelection(LOCPLINT->localState->getCurrentTown());
 		return;
 	}
 

+ 21 - 20
client/adventureMap/CList.cpp

@@ -11,13 +11,14 @@
 #include "StdInc.h"
 #include "CList.h"
 
-#include "CAdvMapInt.h"
+#include "CAdventureMapInterface.h"
 
 #include "../widgets/Images.h"
 #include "../widgets/Buttons.h"
 #include "../windows/InfoWindows.h"
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
+#include "../PlayerLocalState.h"
 #include "../gui/CGuiHandler.h"
 
 #include "../../lib/CGeneralTextHandler.h"
@@ -203,8 +204,8 @@ std::shared_ptr<CIntObject> CHeroList::CHeroItem::genSelection()
 
 void CHeroList::CHeroItem::select(bool on)
 {
-	if(on && adventureInt->curHero() != hero)
-		adventureInt->select(hero);
+	if(on)
+		LOCPLINT->localState->setSelection(hero);
 }
 
 void CHeroList::CHeroItem::open()
@@ -224,19 +225,19 @@ std::string CHeroList::CHeroItem::getHoverText()
 
 std::shared_ptr<CIntObject> CHeroList::createHeroItem(size_t index)
 {
-	if (LOCPLINT->wanderingHeroes.size() > index)
-		return std::make_shared<CHeroItem>(this, LOCPLINT->wanderingHeroes[index]);
+	if (LOCPLINT->localState->getWanderingHeroes().size() > index)
+		return std::make_shared<CHeroItem>(this, LOCPLINT->localState->getWanderingHero(index));
 	return std::make_shared<CEmptyHeroItem>();
 }
 
 CHeroList::CHeroList(int size, Point position, std::string btnUp, std::string btnDown):
-	CList(size, position, btnUp, btnDown, LOCPLINT->wanderingHeroes.size(), 303, 304, std::bind(&CHeroList::createHeroItem, this, _1))
+	CList(size, position, btnUp, btnDown, LOCPLINT->localState->getWanderingHeroes().size(), 303, 304, std::bind(&CHeroList::createHeroItem, this, _1))
 {
 }
 
 void CHeroList::select(const CGHeroInstance * hero)
 {
-	selectIndex(vstd::find_pos(LOCPLINT->wanderingHeroes, hero));
+	selectIndex(vstd::find_pos(LOCPLINT->localState->getWanderingHeroes(), hero));
 }
 
 void CHeroList::update(const CGHeroInstance * hero)
@@ -245,7 +246,7 @@ void CHeroList::update(const CGHeroInstance * hero)
 	for(auto & elem : listBox->getItems())
 	{
 		auto item = std::dynamic_pointer_cast<CHeroItem>(elem);
-		if(item && item->hero == hero && vstd::contains(LOCPLINT->wanderingHeroes, hero))
+		if(item && item->hero == hero && vstd::contains(LOCPLINT->localState->getWanderingHeroes(), hero))
 		{
 			item->update();
 			return;
@@ -253,17 +254,17 @@ void CHeroList::update(const CGHeroInstance * hero)
 	}
 	//simplest solution for now: reset list and restore selection
 
-	listBox->resize(LOCPLINT->wanderingHeroes.size());
-	if (adventureInt->curHero())
-		select(adventureInt->curHero());
+	listBox->resize(LOCPLINT->localState->getWanderingHeroes().size());
+	if (LOCPLINT->localState->getCurrentHero())
+		select(LOCPLINT->localState->getCurrentHero());
 
 	CList::update();
 }
 
 std::shared_ptr<CIntObject> CTownList::createTownItem(size_t index)
 {
-	if (LOCPLINT->towns.size() > index)
-		return std::make_shared<CTownItem>(this, LOCPLINT->towns[index]);
+	if (LOCPLINT->localState->getOwnedTowns().size() > index)
+		return std::make_shared<CTownItem>(this, LOCPLINT->localState->getOwnedTown(index));
 	return std::make_shared<CAnimImage>("ITPA", 0);
 }
 
@@ -292,8 +293,8 @@ void CTownList::CTownItem::update()
 
 void CTownList::CTownItem::select(bool on)
 {
-	if (on && adventureInt->curTown() != town)
-		adventureInt->select(town);
+	if(on)
+		LOCPLINT->localState->setSelection(town);
 }
 
 void CTownList::CTownItem::open()
@@ -312,22 +313,22 @@ std::string CTownList::CTownItem::getHoverText()
 }
 
 CTownList::CTownList(int size, Point position, std::string btnUp, std::string btnDown):
-	CList(size, position, btnUp, btnDown, LOCPLINT->towns.size(),  306, 307, std::bind(&CTownList::createTownItem, this, _1))
+	CList(size, position, btnUp, btnDown, LOCPLINT->localState->getOwnedTowns().size(),  306, 307, std::bind(&CTownList::createTownItem, this, _1))
 {
 }
 
 void CTownList::select(const CGTownInstance * town)
 {
-	selectIndex(vstd::find_pos(LOCPLINT->towns, town));
+	selectIndex(vstd::find_pos(LOCPLINT->localState->getOwnedTowns(), town));
 }
 
 void CTownList::update(const CGTownInstance *)
 {
 	//simplest solution for now: reset list and restore selection
 
-	listBox->resize(LOCPLINT->towns.size());
-	if (adventureInt->curTown())
-		select(adventureInt->curTown());
+	listBox->resize(LOCPLINT->localState->getOwnedTowns().size());
+	if (LOCPLINT->localState->getCurrentTown())
+		select(LOCPLINT->localState->getCurrentTown());
 
 	CList::update();
 }

+ 10 - 4
client/adventureMap/CMinimap.cpp

@@ -11,14 +11,15 @@
 #include "StdInc.h"
 #include "CMinimap.h"
 
-#include "CAdvMapInt.h"
+#include "CAdventureMapInterface.h"
 
 #include "../widgets/Images.h"
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
 #include "../gui/CGuiHandler.h"
 #include "../render/Colors.h"
-#include "../renderSDL/SDL_PixelAccess.h"
+#include "../renderSDL/SDL_Extensions.h"
+#include "../render/Canvas.h"
 #include "../windows/InfoWindows.h"
 
 #include "../../CCallback.h"
@@ -27,6 +28,8 @@
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapping/CMapDefines.h"
 
+#include <SDL_pixels.h>
+
 ColorRGBA CMinimapInstance::getTileColor(const int3 & pos) const
 {
 	const TerrainTile * tile = LOCPLINT->cb->getTile(pos, false);
@@ -225,10 +228,13 @@ void CMinimap::setAIRadar(bool on)
 	redraw();
 }
 
-void CMinimap::updateTile(const int3 &pos)
+void CMinimap::updateTiles(std::unordered_set<int3> positions)
 {
 	if(minimap)
-		minimap->refreshTile(pos);
+	{
+		for (auto const & tile : positions)
+			minimap->refreshTile(tile);
+	}
 	redraw();
 }
 

+ 2 - 3
client/adventureMap/CMinimap.h

@@ -10,13 +10,12 @@
 #pragma once
 
 #include "../gui/CIntObject.h"
-#include "../../lib/GameConstants.h"
-#include "../render/Canvas.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 class ColorRGBA;
 VCMI_LIB_NAMESPACE_END
 
+class Canvas;
 class CMinimap;
 
 class CMinimapInstance : public CIntObject
@@ -68,6 +67,6 @@ public:
 
 	void showAll(SDL_Surface * to) override;
 
-	void updateTile(const int3 &pos);
+	void updateTiles(std::unordered_set<int3> positions);
 };
 

+ 1 - 1
client/adventureMap/CResDataBar.cpp

@@ -80,7 +80,7 @@ std::string CResDataBar::buildDateString()
 void CResDataBar::draw(SDL_Surface * to)
 {
 	//TODO: all this should be labels, but they require proper text update on change
-	for (auto i=GameResID(EGameResID::WOOD); i <= GameResID(EGameResID::GOLD); vstd::advance(i, 1))
+	for (GameResID i=EGameResID::WOOD; i <= GameResID(EGameResID::GOLD); ++i)
 	{
 		std::string text = std::to_string(LOCPLINT->cb->getResourceAmount(i));
 

+ 1 - 1
client/adventureMap/MapAudioPlayer.cpp

@@ -157,7 +157,7 @@ void MapAudioPlayer::updateAmbientSounds()
 	};
 
 	int3 pos = currentSelection->getSightCenter();
-	std::unordered_set<int3, ShashInt3> tiles;
+	std::unordered_set<int3> tiles;
 	LOCPLINT->cb->getVisibleTilesInRange(tiles, pos, CCS->soundh->ambientGetRange(), int3::DIST_CHEBYSHEV);
 	for(int3 tile : tiles)
 	{

+ 9 - 9
client/battle/BattleActionsController.cpp

@@ -140,7 +140,7 @@ bool BattleActionsController::isActiveStackSpellcaster() const
 	if (!casterStack)
 		return false;
 
-	bool spellcaster = casterStack->hasBonusOfType(Bonus::SPELLCASTER);
+	bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER);
 	return (spellcaster && casterStack->canCast());
 }
 
@@ -228,7 +228,7 @@ void BattleActionsController::reorderPossibleActionsPriority(const CStack * stac
 		case PossiblePlayerBattleAction::NO_LOCATION:
 		case PossiblePlayerBattleAction::FREE_LOCATION:
 		case PossiblePlayerBattleAction::OBSTACLE:
-			if(!stack->hasBonusOfType(Bonus::NO_SPELLCAST_BY_DEFAULT) && context == MouseHoveredHexContext::OCCUPIED_HEX)
+			if(!stack->hasBonusOfType(BonusType::NO_SPELLCAST_BY_DEFAULT) && context == MouseHoveredHexContext::OCCUPIED_HEX)
 				return 1;
 			else
 				return 100;//bottom priority
@@ -349,7 +349,7 @@ void BattleActionsController::actionSetCursor(PossiblePlayerBattleAction action,
 
 		case PossiblePlayerBattleAction::MOVE_TACTICS:
 		case PossiblePlayerBattleAction::MOVE_STACK:
-			if (owner.stacksController->getActiveStack()->hasBonusOfType(Bonus::FLYING))
+			if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING))
 				CCS->curh->set(Cursor::Combat::FLY);
 			else
 				CCS->curh->set(Cursor::Combat::MOVE);
@@ -434,7 +434,7 @@ std::string BattleActionsController::actionGetStatusMessage(PossiblePlayerBattle
 
 		case PossiblePlayerBattleAction::MOVE_TACTICS:
 		case PossiblePlayerBattleAction::MOVE_STACK:
-			if (owner.stacksController->getActiveStack()->hasBonusOfType(Bonus::FLYING))
+			if (owner.stacksController->getActiveStack()->hasBonusOfType(BonusType::FLYING))
 				return (boost::format(CGI->generaltexth->allTexts[295]) % owner.stacksController->getActiveStack()->getName()).str(); //Fly %s here
 			else
 				return (boost::format(CGI->generaltexth->allTexts[294]) % owner.stacksController->getActiveStack()->getName()).str(); //Move %s here
@@ -524,12 +524,12 @@ std::string BattleActionsController::actionGetStatusMessageBlocked(PossiblePlaye
 bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, BattleHex targetHex)
 {
 	const CStack * targetStack = getStackForHex(targetHex);
-	bool targetStackOwned = targetStack && targetStack->owner == owner.curInt->playerID;
+	bool targetStackOwned = targetStack && targetStack->unitOwner() == owner.curInt->playerID;
 
 	switch (action.get())
 	{
 		case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
-			return (targetStack && targetStackOwned && targetStack->Speed() > 0);
+			return (targetStack && targetStackOwned && targetStack->speed() > 0);
 
 		case PossiblePlayerBattleAction::CREATURE_INFO:
 			return (targetStack && targetStackOwned && targetStack->alive());
@@ -620,7 +620,7 @@ void BattleActionsController::actionRealize(PossiblePlayerBattleAction action, B
 		{
 			if(owner.stacksController->getActiveStack()->doubleWide())
 			{
-				std::vector<BattleHex> acc = owner.curInt->cb->battleGetAvailableHexes(owner.stacksController->getActiveStack(), true);
+				std::vector<BattleHex> acc = owner.curInt->cb->battleGetAvailableHexes(owner.stacksController->getActiveStack(), false);
 				BattleHex shiftedDest = targetHex.cloneInDirection(owner.stacksController->getActiveStack()->destShiftDir(), false);
 				if(vstd::contains(acc, targetHex))
 					owner.giveCommand(EActionType::WALK, targetHex);
@@ -863,7 +863,7 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS
 {
 	creatureSpells.clear();
 
-	bool spellcaster = casterStack->hasBonusOfType(Bonus::SPELLCASTER);
+	bool spellcaster = casterStack->hasBonusOfType(BonusType::SPELLCASTER);
 	if(casterStack->canCast() && spellcaster)
 	{
 		// faerie dragon can cast only one, randomly selected spell until their next move
@@ -874,7 +874,7 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS
 			creatureSpells.push_back(spellToCast);
 	}
 
-	TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(Bonus::SPELLCASTER));
+	TConstBonusListPtr bl = casterStack->getBonuses(Selector::type()(BonusType::SPELLCASTER));
 
 	for (auto const & bonus : *bl)
 	{

+ 6 - 6
client/battle/BattleAnimationClasses.cpp

@@ -71,17 +71,17 @@ std::vector<BattleAnimation *> & BattleAnimation::pendingAnimations()
 
 std::shared_ptr<CreatureAnimation> BattleAnimation::stackAnimation(const CStack * stack) const
 {
-	return owner.stacksController->stackAnimation[stack->ID];
+	return owner.stacksController->stackAnimation[stack->unitId()];
 }
 
 bool BattleAnimation::stackFacingRight(const CStack * stack)
 {
-	return owner.stacksController->stackFacingRight[stack->ID];
+	return owner.stacksController->stackFacingRight[stack->unitId()];
 }
 
 void BattleAnimation::setStackFacingRight(const CStack * stack, bool facingRight)
 {
-	owner.stacksController->stackFacingRight[stack->ID] = facingRight;
+	owner.stacksController->stackFacingRight[stack->unitId()] = facingRight;
 }
 
 BattleStackAnimation::BattleStackAnimation(BattleInterface & owner, const CStack * stack)
@@ -137,7 +137,7 @@ bool StackActionAnimation::init()
 
 StackActionAnimation::~StackActionAnimation()
 {
-	if (stack->isFrozen())
+	if (stack->isFrozen() && currGroup != ECreatureAnimType::DEATH && currGroup != ECreatureAnimType::DEATH_RANGED)
 		myAnim->setType(ECreatureAnimType::HOLDING);
 	else
 		myAnim->setType(nextGroup);
@@ -279,7 +279,7 @@ ECreatureAnimType MeleeAttackAnimation::selectGroup(bool multiAttack)
 		getForwardGroup  (multiAttack)
 	};
 
-	int revShiftattacker = (attackingStack->side == BattleSide::ATTACKER ? -1 : 1);
+	int revShiftattacker = (attackingStack->unitSide() == BattleSide::ATTACKER ? -1 : 1);
 
 	int mutPos = BattleHex::mutualPosition(attackingStackPosBeforeReturn, dest);
 	if(mutPos == -1 && attackingStack->doubleWide())
@@ -370,7 +370,7 @@ bool MovementAnimation::init()
 	distanceX = endPosition.x - begPosition.x;
 	distanceY = endPosition.y - begPosition.y;
 
-	if (stack->hasBonus(Selector::type()(Bonus::FLYING)))
+	if (stack->hasBonus(Selector::type()(BonusType::FLYING)))
 	{
 		float distance = static_cast<float>(sqrt(distanceX * distanceX + distanceY * distanceY));
 		progressPerSecond =  AnimationControls::getFlightDistance(stack->unitType()) / distance;

+ 6 - 6
client/battle/BattleEffectsController.cpp

@@ -65,21 +65,21 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt
 		return;
 	}
 	//don't show animation when no HP is regenerated
-	switch(bte.effect)
+	switch(static_cast<BonusType>(bte.effect))
 	{
-		case Bonus::HP_REGENERATION:
+		case BonusType::HP_REGENERATION:
 			displayEffect(EBattleEffect::REGENERATION, "REGENER", stack->getPosition());
 			break;
-		case Bonus::MANA_DRAIN:
+		case BonusType::MANA_DRAIN:
 			displayEffect(EBattleEffect::MANA_DRAIN, "MANADRAI", stack->getPosition());
 			break;
-		case Bonus::POISON:
+		case BonusType::POISON:
 			displayEffect(EBattleEffect::POISON, "POISON", stack->getPosition());
 			break;
-		case Bonus::FEAR:
+		case BonusType::FEAR:
 			displayEffect(EBattleEffect::FEAR, "FEAR", stack->getPosition());
 			break;
-		case Bonus::MORALE:
+		case BonusType::MORALE:
 		{
 			std::string hlp = CGI->generaltexth->allTexts[33];
 			boost::algorithm::replace_first(hlp,"%s",(stack->getName()));

+ 11 - 11
client/battle/BattleFieldController.cpp

@@ -134,7 +134,7 @@ void BattleFieldController::renderBattlefield(Canvas & canvas)
 
 void BattleFieldController::showBackground(Canvas & canvas)
 {
-	if (owner.stacksController->getActiveStack() != nullptr ) //&& creAnims[stacksController->getActiveStack()->ID]->isIdle() //show everything with range
+	if (owner.stacksController->getActiveStack() != nullptr ) //&& creAnims[stacksController->getActiveStack()->unitId()]->isIdle() //show everything with range
 		showBackgroundImageWithHexes(canvas);
 	else
 		showBackgroundImage(canvas);
@@ -174,7 +174,7 @@ void BattleFieldController::redrawBackgroundWithHexes()
 	const CStack *activeStack = owner.stacksController->getActiveStack();
 	std::vector<BattleHex> attackableHexes;
 	if(activeStack)
-		occupiableHexes = owner.curInt->cb->battleGetAvailableHexes(activeStack, true, true, &attackableHexes);
+		occupiableHexes = owner.curInt->cb->battleGetAvailableHexes(activeStack, false, true, &attackableHexes);
 
 	// prepare background graphic with hexes and shaded hexes
 	backgroundWithHexes->draw(background, Point(0,0));
@@ -243,7 +243,7 @@ std::set<BattleHex> BattleFieldController::getMovementRangeForHoveredStack()
 	if (!owner.stacksController->getActiveStack())
 		return result;
 
-	if (!settings["battle"]["movementHighlightOnHover"].Bool())
+	if (!settings["battle"]["movementHighlightOnHover"].Bool() && !GH.isKeyboardShiftDown())
 		return result;
 
 	auto hoveredHex = getHoveredHex();
@@ -252,7 +252,7 @@ std::set<BattleHex> BattleFieldController::getMovementRangeForHoveredStack()
 	const CStack * const hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
 	if(hoveredStack)
 	{
-		std::vector<BattleHex> v = owner.curInt->cb->battleGetAvailableHexes(hoveredStack, false, true, nullptr);
+		std::vector<BattleHex> v = owner.curInt->cb->battleGetAvailableHexes(hoveredStack, true, true, nullptr);
 		for(BattleHex hex : v)
 			result.insert(hex);
 	}
@@ -289,7 +289,7 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesForSpellRange()
 	return result;
 }
 
-std::set<BattleHex> BattleFieldController::getHighlightedHexesMovementTarget()
+std::set<BattleHex> BattleFieldController::getHighlightedHexesForMovementTarget()
 {
 	const CStack * stack = owner.stacksController->getActiveStack();
 	auto hoveredHex = getHoveredHex();
@@ -297,7 +297,7 @@ std::set<BattleHex> BattleFieldController::getHighlightedHexesMovementTarget()
 	if(!stack)
 		return {};
 
-	std::vector<BattleHex> availableHexes = owner.curInt->cb->battleGetAvailableHexes(stack, true, false, nullptr);
+	std::vector<BattleHex> availableHexes = owner.curInt->cb->battleGetAvailableHexes(stack, false, false, nullptr);
 
 	auto hoveredStack = owner.curInt->cb->battleGetStackByPos(hoveredHex, true);
 	if(owner.curInt->cb->battleCanAttack(stack, hoveredStack, hoveredHex))
@@ -337,7 +337,7 @@ void BattleFieldController::showHighlightedHexes(Canvas & canvas)
 {
 	std::set<BattleHex> hoveredStackMovementRangeHexes = getMovementRangeForHoveredStack();
 	std::set<BattleHex> hoveredSpellHexes = getHighlightedHexesForSpellRange();
-	std::set<BattleHex> hoveredMoveHexes  = getHighlightedHexesMovementTarget();
+	std::set<BattleHex> hoveredMoveHexes  = getHighlightedHexesForMovementTarget();
 
 	if(getHoveredHex() == BattleHex::INVALID)
 		return;
@@ -540,7 +540,7 @@ BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget)
 		case BattleHex::LEFT:
 		case BattleHex::BOTTOM_LEFT:
 		{
-			if ( attacker->side == BattleSide::ATTACKER )
+			if ( attacker->unitSide() == BattleSide::ATTACKER )
 				return attackTarget.cloneInDirection(direction);
 			else
 				return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::LEFT);
@@ -550,7 +550,7 @@ BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget)
 		case BattleHex::RIGHT:
 		case BattleHex::BOTTOM_RIGHT:
 		{
-			if ( attacker->side == BattleSide::ATTACKER )
+			if ( attacker->unitSide() == BattleSide::ATTACKER )
 				return attackTarget.cloneInDirection(direction).cloneInDirection(BattleHex::RIGHT);
 			else
 				return attackTarget.cloneInDirection(direction);
@@ -558,7 +558,7 @@ BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget)
 
 		case BattleHex::TOP:
 		{
-			if ( attacker->side == BattleSide::ATTACKER )
+			if ( attacker->unitSide() == BattleSide::ATTACKER )
 				return attackTarget.cloneInDirection(BattleHex::TOP_RIGHT);
 			else
 				return attackTarget.cloneInDirection(BattleHex::TOP_LEFT);
@@ -566,7 +566,7 @@ BattleHex BattleFieldController::fromWhichHexAttack(BattleHex attackTarget)
 
 		case BattleHex::BOTTOM:
 		{
-			if ( attacker->side == BattleSide::ATTACKER )
+			if ( attacker->unitSide() == BattleSide::ATTACKER )
 				return attackTarget.cloneInDirection(BattleHex::BOTTOM_RIGHT);
 			else
 				return attackTarget.cloneInDirection(BattleHex::BOTTOM_LEFT);

+ 1 - 1
client/battle/BattleFieldController.h

@@ -53,7 +53,7 @@ class BattleFieldController : public CIntObject
 	std::set<BattleHex> getHighlightedHexesForActiveStack();
 	std::set<BattleHex> getMovementRangeForHoveredStack();
 	std::set<BattleHex> getHighlightedHexesForSpellRange();
-	std::set<BattleHex> getHighlightedHexesMovementTarget();
+	std::set<BattleHex> getHighlightedHexesForMovementTarget();
 
 	void showBackground(Canvas & canvas);
 	void showBackgroundImage(Canvas & canvas);

+ 4 - 4
client/battle/BattleInterface.cpp

@@ -29,7 +29,7 @@
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
 #include "../render/Canvas.h"
-#include "../adventureMap/CAdvMapInt.h"
+#include "../adventureMap/CAdventureMapInterface.h"
 
 #include "../../CCallback.h"
 #include "../../lib/CStack.h"
@@ -208,7 +208,7 @@ void BattleInterface::stacksAreAttacked(std::vector<StackAttackedInfo> attackedI
 
 	for(const StackAttackedInfo & attackedInfo : attackedInfos)
 	{
-		ui8 side = attackedInfo.defender->side;
+		ui8 side = attackedInfo.defender->unitSide();
 		killedBySide.at(side) += attackedInfo.amountKilled;
 	}
 
@@ -288,7 +288,7 @@ const CGHeroInstance * BattleInterface::getActiveHero()
 		return nullptr;
 	}
 
-	if(attacker->side == BattleSide::ATTACKER)
+	if(attacker->unitSide() == BattleSide::ATTACKER)
 	{
 		return attackingHeroInstance;
 	}
@@ -636,7 +636,7 @@ void BattleInterface::tacticPhaseEnd()
 
 static bool immobile(const CStack *s)
 {
-	return !s->Speed(0, true); //should bound stacks be immobile?
+	return !s->speed(0, true); //should bound stacks be immobile?
 }
 
 void BattleInterface::tacticNextStack(const CStack * current)

+ 7 - 6
client/battle/BattleInterfaceClasses.cpp

@@ -24,6 +24,7 @@
 #include "../CVideoHandler.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../render/Canvas.h"
 #include "../render/IImage.h"
 #include "../widgets/Buttons.h"
@@ -406,12 +407,12 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface
 	background->colorize(owner.playerID);
 	pos = center(background->pos);
 
-	exit = std::make_shared<CButton>(Point(384, 505), "iok6432.def", std::make_pair("", ""), [&](){ bExitf();}, SDLK_RETURN);
+	exit = std::make_shared<CButton>(Point(384, 505), "iok6432.def", std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT);
 	exit->setBorderColor(Colors::METALLIC_GOLD);
 	
 	if(allowReplay)
 	{
-		repeat = std::make_shared<CButton>(Point(24, 505), "icn6432.def", std::make_pair("", ""), [&](){ bRepeatf();}, SDLK_ESCAPE);
+		repeat = std::make_shared<CButton>(Point(24, 505), "icn6432.def", std::make_pair("", ""), [&](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL);
 		repeat->setBorderColor(Colors::METALLIC_GOLD);
 		labels.push_back(std::make_shared<CLabel>(232, 520, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->translate("vcmi.battleResultsWindow.applyResultsLabel")));
 	}
@@ -455,18 +456,18 @@ BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface
 			auto stacks = owner.cb->battleGetAllStacks();
 			vstd::erase_if(stacks, [i](const CStack * stack) //erase stack of other side and not coming from garrison
 			{
-				return stack->side != i || !stack->base;
+				return stack->unitSide() != i || !stack->base;
 			});
 
 			auto best = vstd::maxElementByFun(stacks, [](const CStack * stack)
 			{
-				return stack->type->getAIValue();
+				return stack->unitType()->getAIValue();
 			});
 
 			if(best != stacks.end()) //should be always but to be safe...
 			{
-				icons.push_back(std::make_shared<CAnimImage>("TWCRPORT", (*best)->type->getIconIndex(), 0, xs[i], 38));
-				sideNames[i] = (*best)->type->getNamePluralTranslated();
+				icons.push_back(std::make_shared<CAnimImage>("TWCRPORT", (*best)->unitType()->getIconIndex(), 0, xs[i], 38));
+				sideNames[i] = (*best)->unitType()->getNamePluralTranslated();
 			}
 		}
 	}

+ 5 - 5
client/battle/BattleProjectileController.cpp

@@ -205,7 +205,7 @@ std::shared_ptr<CAnimation> BattleProjectileController::getProjectileImage(const
 
 void BattleProjectileController::emitStackProjectile(const CStack * stack)
 {
-	int stackID = stack ? stack->ID : -1;
+	int stackID = stack ? stack->unitId() : -1;
 
 	for (auto projectile : projectiles)
 	{
@@ -232,7 +232,7 @@ void BattleProjectileController::showProjectiles(Canvas & canvas)
 
 bool BattleProjectileController::hasActiveProjectile(const CStack * stack, bool emittedOnly) const
 {
-	int stackID = stack ? stack->ID : -1;
+	int stackID = stack ? stack->unitId() : -1;
 
 	for(auto const & instance : projectiles)
 	{
@@ -294,7 +294,7 @@ void BattleProjectileController::createCatapultProjectile(const CStack * shooter
 	catapultProjectile->speed     = computeProjectileFlightTime(from, dest, AnimationControls::getCatapultSpeed());
 	catapultProjectile->from      = from;
 	catapultProjectile->dest      = dest;
-	catapultProjectile->shooterID = shooter->ID;
+	catapultProjectile->shooterID = shooter->unitId();
 	catapultProjectile->playing   = false;
 	catapultProjectile->frameProgress = 0.f;
 
@@ -333,7 +333,7 @@ void BattleProjectileController::createProjectile(const CStack * shooter, Point
 
 	projectile->from      = from;
 	projectile->dest      = dest;
-	projectile->shooterID = shooter->ID;
+	projectile->shooterID = shooter->unitId();
 	projectile->progress  = 0;
 	projectile->playing   = false;
 
@@ -357,7 +357,7 @@ void BattleProjectileController::createSpellProjectile(const CStack * shooter, P
 		projectile->reverse       = from.x > dest.x;
 		projectile->from          = from;
 		projectile->dest          = dest;
-		projectile->shooterID     = shooter ? shooter->ID : -1;
+		projectile->shooterID     = shooter ? shooter->unitId() : -1;
 		projectile->progress      = 0;
 		projectile->speed         = computeProjectileFlightTime(from, dest, AnimationControls::getProjectileSpeed());
 		projectile->playing       = false;

+ 46 - 54
client/battle/BattleStacksController.cpp

@@ -104,10 +104,10 @@ BattleStacksController::BattleStacksController(BattleInterface & owner):
 
 BattleHex BattleStacksController::getStackCurrentPosition(const CStack * stack) const
 {
-	if ( !stackAnimation.at(stack->ID)->isMoving())
+	if ( !stackAnimation.at(stack->unitId())->isMoving())
 		return stack->getPosition();
 
-	if (stack->hasBonusOfType(Bonus::FLYING) && stackAnimation.at(stack->ID)->getType() == ECreatureAnimType::MOVING )
+	if (stack->hasBonusOfType(BonusType::FLYING) && stackAnimation.at(stack->unitId())->getType() == ECreatureAnimType::MOVING )
 		return BattleHex::HEX_AFTER_ALL;
 
 	for (auto & anim : currentAnimations)
@@ -131,14 +131,14 @@ void BattleStacksController::collectRenderableObjects(BattleRenderer & renderer)
 
 	for (auto stack : stacks)
 	{
-		if (stackAnimation.find(stack->ID) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks
+		if (stackAnimation.find(stack->unitId()) == stackAnimation.end()) //e.g. for summoned but not yet handled stacks
 			continue;
 
 		//FIXME: hack to ignore ghost stacks
-		if ((stackAnimation[stack->ID]->getType() == ECreatureAnimType::DEAD || stackAnimation[stack->ID]->getType() == ECreatureAnimType::HOLDING) && stack->isGhost())
+		if ((stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::DEAD || stackAnimation[stack->unitId()]->getType() == ECreatureAnimType::HOLDING) && stack->isGhost())
 			continue;
 
-		auto layer = stackAnimation[stack->ID]->isDead() ? EBattleFieldLayer::CORPSES : EBattleFieldLayer::STACKS;
+		auto layer = stackAnimation[stack->unitId()]->isDead() ? EBattleFieldLayer::CORPSES : EBattleFieldLayer::STACKS;
 		auto location = getStackCurrentPosition(stack);
 
 		renderer.insert(layer, location, [this, stack]( BattleRenderer::RendererRef renderer ){
@@ -159,13 +159,13 @@ void BattleStacksController::stackReset(const CStack * stack)
 	owner.checkForAnimations();
 
 	//reset orientation?
-	//stackFacingRight[stack->ID] = stack->side == BattleSide::ATTACKER;
+	//stackFacingRight[stack->unitId()] = stack->unitSide() == BattleSide::ATTACKER;
 
-	auto iter = stackAnimation.find(stack->ID);
+	auto iter = stackAnimation.find(stack->unitId());
 
 	if(iter == stackAnimation.end())
 	{
-		logGlobal->error("Unit %d have no animation", stack->ID);
+		logGlobal->error("Unit %d have no animation", stack->unitId());
 		return;
 	}
 
@@ -185,7 +185,7 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 	// Tower shooters have only their upper half visible
 	static const int turretCreatureAnimationHeight = 232;
 
-	stackFacingRight[stack->ID] = stack->side == BattleSide::ATTACKER; // must be set before getting stack position
+	stackFacingRight[stack->unitId()] = stack->unitSide() == BattleSide::ATTACKER; // must be set before getting stack position
 
 	Point coords = getStackPositionAtHex(stack->getPosition(), stack);
 
@@ -195,26 +195,26 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 
 		const CCreature *turretCreature = owner.siegeController->getTurretCreature();
 
-		stackAnimation[stack->ID] = AnimationControls::getAnimation(turretCreature);
-		stackAnimation[stack->ID]->pos.h = turretCreatureAnimationHeight;
-		stackAnimation[stack->ID]->pos.w = stackAnimation[stack->ID]->getWidth();
+		stackAnimation[stack->unitId()] = AnimationControls::getAnimation(turretCreature);
+		stackAnimation[stack->unitId()]->pos.h = turretCreatureAnimationHeight;
+		stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth();
 
 		// FIXME: workaround for visible animation of Medusa tails (animation disabled in H3)
 		if (turretCreature->getId() == CreatureID::MEDUSA )
-			stackAnimation[stack->ID]->pos.w = 250;
+			stackAnimation[stack->unitId()]->pos.w = 250;
 
 		coords = owner.siegeController->getTurretCreaturePosition(stack->initialPosition);
 	}
 	else
 	{
-		stackAnimation[stack->ID] = AnimationControls::getAnimation(stack->unitType());
-		stackAnimation[stack->ID]->onAnimationReset += std::bind(&onAnimationFinished, stack, stackAnimation[stack->ID]);
-		stackAnimation[stack->ID]->pos.h = stackAnimation[stack->ID]->getHeight();
-		stackAnimation[stack->ID]->pos.w = stackAnimation[stack->ID]->getWidth();
+		stackAnimation[stack->unitId()] = AnimationControls::getAnimation(stack->unitType());
+		stackAnimation[stack->unitId()]->onAnimationReset += std::bind(&onAnimationFinished, stack, stackAnimation[stack->unitId()]);
+		stackAnimation[stack->unitId()]->pos.h = stackAnimation[stack->unitId()]->getHeight();
+		stackAnimation[stack->unitId()]->pos.w = stackAnimation[stack->unitId()]->getWidth();
 	}
-	stackAnimation[stack->ID]->pos.x = coords.x;
-	stackAnimation[stack->ID]->pos.y = coords.y;
-	stackAnimation[stack->ID]->setType(ECreatureAnimType::HOLDING);
+	stackAnimation[stack->unitId()]->pos.x = coords.x;
+	stackAnimation[stack->unitId()]->pos.y = coords.y;
+	stackAnimation[stack->unitId()]->setType(ECreatureAnimType::HOLDING);
 
 	if (!instant)
 	{
@@ -234,28 +234,20 @@ void BattleStacksController::stackAdded(const CStack * stack, bool instant)
 void BattleStacksController::setActiveStack(const CStack *stack)
 {
 	if (activeStack) // update UI
-		stackAnimation[activeStack->ID]->setBorderColor(AnimationControls::getNoBorder());
+		stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getNoBorder());
 
 	activeStack = stack;
 
 	if (activeStack) // update UI
-		stackAnimation[activeStack->ID]->setBorderColor(AnimationControls::getGoldBorder());
+		stackAnimation[activeStack->unitId()]->setBorderColor(AnimationControls::getGoldBorder());
 
 	owner.windowObject->blockUI(activeStack == nullptr);
 }
 
 bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const
 {
-	BattleHex currentActionTarget;
-	if(owner.curInt->curAction)
-	{
-		auto target = owner.curInt->curAction->getTarget(owner.curInt->cb.get());
-		if(!target.empty())
-			currentActionTarget = target.at(0).hexValue;
-	}
-
 	//do not show box for singular war machines, stacked war machines with box shown are supported as extension feature
-	if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->getCount() == 1)
+	if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->getCount() == 1)
 		return false;
 
 	if(!stack->alive())
@@ -266,7 +258,7 @@ bool BattleStacksController::stackNeedsAmountBox(const CStack * stack) const
 		return false;
 
 	// if stack has any ongoing animation - hide the box
-	if (stackAmountBoxHidden.count(stack->ID))
+	if (stackAmountBoxHidden.count(stack->unitId()))
 		return false;
 
 	return true;
@@ -299,7 +291,7 @@ void BattleStacksController::showStackAmountBox(Canvas & canvas, const CStack *
 
 	bool doubleWide = stack->doubleWide();
 	bool turnedRight = facingRight(stack);
-	bool attacker = stack->side == BattleSide::ATTACKER;
+	bool attacker = stack->unitSide() == BattleSide::ATTACKER;
 
 	BattleHex stackPos = stack->getPosition();
 
@@ -342,8 +334,8 @@ void BattleStacksController::showStack(Canvas & canvas, const CStack * stack)
 			fullFilter = ColorFilter::genCombined(fullFilter, filter.effect);
 	}
 
-	stackAnimation[stack->ID]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit
-	stackAnimation[stack->ID]->incrementFrame(float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000);
+	stackAnimation[stack->unitId()]->nextFrame(canvas, fullFilter, facingRight(stack)); // do actual blit
+	stackAnimation[stack->unitId()]->incrementFrame(float(GH.mainFPSmng->getElapsedMilliseconds()) / 1000);
 }
 
 void BattleStacksController::update()
@@ -398,17 +390,17 @@ void BattleStacksController::addNewAnim(BattleAnimation *anim)
 
 	auto stackAnimation = dynamic_cast<BattleStackAnimation*>(anim);
 	if(stackAnimation)
-		stackAmountBoxHidden.insert(stackAnimation->stack->ID);
+		stackAmountBoxHidden.insert(stackAnimation->stack->unitId());
 }
 
 void BattleStacksController::stackRemoved(uint32_t stackID)
 {
-	if (getActiveStack() && getActiveStack()->ID == stackID)
+	if (getActiveStack() && getActiveStack()->unitId() == stackID)
 	{
 		BattleAction *action = new BattleAction();
 		action->side = owner.defendingHeroInstance ? (owner.curInt->playerID == owner.defendingHeroInstance->tempOwner) : false;
 		action->actionType = EActionType::CANCEL;
-		action->stackNumber = getActiveStack()->ID;
+		action->stackNumber = getActiveStack()->unitId();
 		owner.givenCommand.setn(action);
 		setActiveStack(nullptr);
 	}
@@ -439,7 +431,7 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 
 		// FIXME: this check is better, however not usable since stacksAreAttacked is called after net pack is applyed - petrification is already removed
 		// if (needsReverse && !attackedInfo.defender->isFrozen())
-		if (needsReverse && stackAnimation[attackedInfo.defender->ID]->getType() != ECreatureAnimType::FROZEN)
+		if (needsReverse && stackAnimation[attackedInfo.defender->unitId()]->getType() != ECreatureAnimType::FROZEN)
 		{
 			owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [=]()
 			{
@@ -493,7 +485,7 @@ void BattleStacksController::stacksAreAttacked(std::vector<StackAttackedInfo> at
 		{
 			owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
 				addNewAnim(new ColorTransformAnimation(owner, attackedInfo.defender, "summonFadeOut", nullptr));
-				stackRemoved(attackedInfo.defender->ID);
+				stackRemoved(attackedInfo.defender->unitId());
 			});
 		}
 	}
@@ -511,7 +503,7 @@ void BattleStacksController::stackTeleported(const CStack *stack, std::vector<Ba
 	});
 
 	owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=](){
-		stackAnimation[stack->ID]->pos.moveTo(getStackPositionAtHex(destHex.back(), stack));
+		stackAnimation[stack->unitId()]->pos.moveTo(getStackPositionAtHex(destHex.back(), stack));
 		addNewAnim( new ColorTransformAnimation(owner, stack, "teleportFadeIn", nullptr) );
 	});
 
@@ -536,7 +528,7 @@ void BattleStacksController::stackMoved(const CStack *stack, std::vector<BattleH
 		addNewAnim(new MovementStartAnimation(owner, stack));
 	});
 
-	if (!stack->hasBonus(Selector::typeSubtype(Bonus::FLYING, 1)))
+	if (!stack->hasBonus(Selector::typeSubtype(BonusType::FLYING, 1)))
 	{
 		owner.addToAnimationStage(EAnimationEvents::MOVEMENT, [&]()
 		{
@@ -559,7 +551,7 @@ bool BattleStacksController::shouldAttackFacingRight(const CStack * attacker, co
 				attacker,
 				defender);
 
-	if (attacker->side == BattleSide::ATTACKER)
+	if (attacker->unitSide() == BattleSide::ATTACKER)
 		return !mustReverse;
 	else
 		return mustReverse;
@@ -677,9 +669,9 @@ void BattleStacksController::endAction(const BattleAction* action)
 
 	for (const CStack *s : stacks)
 	{
-		bool shouldFaceRight  = s && s->side == BattleSide::ATTACKER;
+		bool shouldFaceRight  = s && s->unitSide() == BattleSide::ATTACKER;
 
-		if (s && facingRight(s) != shouldFaceRight && s->alive() && stackAnimation[s->ID]->isIdle())
+		if (s && facingRight(s) != shouldFaceRight && s->alive() && stackAnimation[s->unitId()]->isIdle())
 		{
 			addNewAnim(new ReverseAnimation(owner, s, s->getPosition()));
 		}
@@ -723,7 +715,7 @@ void BattleStacksController::activateStack()
 	if ( !stackToActivate)
 		return;
 
-	owner.trySetActivePlayer(stackToActivate->owner);
+	owner.trySetActivePlayer(stackToActivate->unitOwner());
 
 	setActiveStack(stackToActivate);
 	stackToActivate = nullptr;
@@ -740,7 +732,7 @@ const CStack* BattleStacksController::getActiveStack() const
 
 bool BattleStacksController::facingRight(const CStack * stack) const
 {
-	return stackFacingRight.at(stack->ID);
+	return stackFacingRight.at(stack->unitId());
 }
 
 Point BattleStacksController::getStackPositionAtHex(BattleHex hexNum, const CStack * stack) const
@@ -765,7 +757,7 @@ Point BattleStacksController::getStackPositionAtHex(BattleHex hexNum, const CSta
 		//shifting position for double - hex creatures
 		if(stack->doubleWide())
 		{
-			if(stack->side == BattleSide::ATTACKER)
+			if(stack->unitSide() == BattleSide::ATTACKER)
 			{
 				if(facingRight(stack))
 					ret.x -= 44;
@@ -801,7 +793,7 @@ void BattleStacksController::removeExpiredColorFilters()
 	{
 		if (!filter.persistent)
 		{
-			if (filter.source && !filter.target->hasBonus(Selector::source(Bonus::SPELL_EFFECT, filter.source->id), Selector::all))
+			if (filter.source && !filter.target->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, filter.source->id), Selector::all))
 				return true;
 			if (filter.effect == ColorFilter::genEmptyShifter())
 				return true;
@@ -820,9 +812,9 @@ void BattleStacksController::updateHoveredStacks()
 			continue;
 
 		if (stack == activeStack)
-			stackAnimation[stack->ID]->setBorderColor(AnimationControls::getGoldBorder());
+			stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getGoldBorder());
 		else
-			stackAnimation[stack->ID]->setBorderColor(AnimationControls::getNoBorder());
+			stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getNoBorder());
 	}
 
 	for(const auto * stack : newStacks)
@@ -830,9 +822,9 @@ void BattleStacksController::updateHoveredStacks()
 		if (vstd::contains(mouseHoveredStacks, stack))
 			continue;
 
-		stackAnimation[stack->ID]->setBorderColor(AnimationControls::getBlueBorder());
-		if (stackAnimation[stack->ID]->framesInGroup(ECreatureAnimType::MOUSEON) > 0 && stack->alive() && !stack->isFrozen())
-			stackAnimation[stack->ID]->playOnce(ECreatureAnimType::MOUSEON);
+		stackAnimation[stack->unitId()]->setBorderColor(AnimationControls::getBlueBorder());
+		if (stackAnimation[stack->unitId()]->framesInGroup(ECreatureAnimType::MOUSEON) > 0 && stack->alive() && !stack->isFrozen())
+			stackAnimation[stack->unitId()]->playOnce(ECreatureAnimType::MOUSEON);
 	}
 
 	mouseHoveredStacks = newStacks;

+ 34 - 80
client/battle/BattleWindow.cpp

@@ -21,6 +21,7 @@
 #include "../CMusicHandler.h"
 #include "../gui/CursorHandler.h"
 #include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
 #include "../windows/CSpellWindow.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Images.h"
@@ -50,19 +51,23 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 	
 	const JsonNode config(ResourceID("config/widgets/BattleWindow.json"));
 	
-	addCallback("options", std::bind(&BattleWindow::bOptionsf, this));
-	addCallback("surrender", std::bind(&BattleWindow::bSurrenderf, this));
-	addCallback("flee", std::bind(&BattleWindow::bFleef, this));
-	addCallback("autofight", std::bind(&BattleWindow::bAutofightf, this));
-	addCallback("spellbook", std::bind(&BattleWindow::bSpellf, this));
-	addCallback("wait", std::bind(&BattleWindow::bWaitf, this));
-	addCallback("defence", std::bind(&BattleWindow::bDefencef, this));
-	addCallback("consoleUp", std::bind(&BattleWindow::bConsoleUpf, this));
-	addCallback("consoleDown", std::bind(&BattleWindow::bConsoleDownf, this));
-	addCallback("tacticNext", std::bind(&BattleWindow::bTacticNextStack, this));
-	addCallback("tacticEnd", std::bind(&BattleWindow::bTacticPhaseEnd, this));
-	addCallback("alternativeAction", std::bind(&BattleWindow::bSwitchActionf, this));
-	
+	addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this));
+	addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this));
+	addShortcut(EShortcut::BATTLE_RETREAT, std::bind(&BattleWindow::bFleef, this));
+	addShortcut(EShortcut::BATTLE_AUTOCOMBAT, std::bind(&BattleWindow::bAutofightf, this));
+	addShortcut(EShortcut::BATTLE_CAST_SPELL, std::bind(&BattleWindow::bSpellf, this));
+	addShortcut(EShortcut::BATTLE_WAIT, std::bind(&BattleWindow::bWaitf, this));
+	addShortcut(EShortcut::BATTLE_DEFEND, std::bind(&BattleWindow::bDefencef, this));
+	addShortcut(EShortcut::BATTLE_CONSOLE_UP, std::bind(&BattleWindow::bConsoleUpf, this));
+	addShortcut(EShortcut::BATTLE_CONSOLE_DOWN, std::bind(&BattleWindow::bConsoleDownf, this));
+	addShortcut(EShortcut::BATTLE_TACTICS_NEXT, std::bind(&BattleWindow::bTacticNextStack, this));
+	addShortcut(EShortcut::BATTLE_TACTICS_END, std::bind(&BattleWindow::bTacticPhaseEnd, this));
+	addShortcut(EShortcut::BATTLE_SELECT_ACTION, std::bind(&BattleWindow::bSwitchActionf, this));
+
+	addShortcut(EShortcut::BATTLE_TOGGLE_QUEUE, [this](){ this->toggleQueueVisibility();});
+	addShortcut(EShortcut::BATTLE_USE_CREATURE_SPELL, [this](){ this->owner.actionsController->enterCreatureCastingMode(); });
+	addShortcut(EShortcut::GLOBAL_CANCEL, [this](){ this->owner.actionsController->endCastingSpell(); });
+
 	build(config);
 	
 	console = widget<BattleConsole>("console");
@@ -182,43 +187,14 @@ void BattleWindow::deactivate()
 	LOCPLINT->cingconsole->deactivate();
 }
 
-void BattleWindow::keyPressed(const SDL_Keycode & key)
+void BattleWindow::keyPressed(EShortcut key)
 {
 	if (owner.openingPlaying())
 	{
 		owner.openingEnd();
 		return;
 	}
-
-	if(key == SDLK_q)
-	{
-		toggleQueueVisibility();
-	}
-	else if(key == SDLK_f)
-	{
-		owner.actionsController->enterCreatureCastingMode();
-	}
-	else if(key == SDLK_ESCAPE)
-	{
-		owner.actionsController->endCastingSpell();
-	}
-	else if(GH.isKeyboardShiftDown())
-	{
-		// save and activate setting
-		Settings movementHighlightOnHover = settings.write["battle"]["movementHighlightOnHover"];
-		movementHighlightOnHoverCache = movementHighlightOnHover->Bool();
-		movementHighlightOnHover->Bool() = true;
-	}
-}
-
-void BattleWindow::keyReleased(const SDL_Keycode & key)
-{
-	if(!GH.isKeyboardShiftDown())
-	{
-		// set back to initial state
-		Settings movementHighlightOnHover = settings.write["battle"]["movementHighlightOnHover"];
-		movementHighlightOnHover->Bool() = movementHighlightOnHoverCache;
-	}
+	InterfaceObjectConfigurable::keyPressed(key);
 }
 
 void BattleWindow::clickRight(tribool down, bool previousState)
@@ -450,11 +426,11 @@ void BattleWindow::bSpellf()
 	{
 		//TODO: move to spell mechanics, add more information to spell cast problem
 		//Handle Orb of Inhibition-like effects -> we want to display dialog with info, why casting is impossible
-		auto blockingBonus = owner.currentHero()->getBonusLocalFirst(Selector::type()(Bonus::BLOCK_ALL_MAGIC));
+		auto blockingBonus = owner.currentHero()->getBonusLocalFirst(Selector::type()(BonusType::BLOCK_ALL_MAGIC));
 		if (!blockingBonus)
 			return;
 
-		if (blockingBonus->source == Bonus::ARTIFACT)
+		if (blockingBonus->source == BonusSource::ARTIFACT)
 		{
 			const auto artID = ArtifactID(blockingBonus->sid);
 			//If we have artifact, put name of our hero. Otherwise assume it's the enemy.
@@ -554,40 +530,18 @@ void BattleWindow::blockUI(bool on)
 
 	bool canWait = owner.stacksController->getActiveStack() ? !owner.stacksController->getActiveStack()->waitedThisTurn : false;
 
-	if(auto w = widget<CButton>("options"))
-		w->block(on);
-	if(auto w = widget<CButton>("flee"))
-		w->block(on || !owner.curInt->cb->battleCanFlee());
-	if(auto w = widget<CButton>("surrender"))
-		w->block(on || owner.curInt->cb->battleGetSurrenderCost() < 0);
-	if(auto w = widget<CButton>("cast"))
-		w->block(on || owner.tacticsMode || !canCastSpells);
-	if(auto w = widget<CButton>("wait"))
-		w->block(on || owner.tacticsMode || !canWait);
-	if(auto w = widget<CButton>("defence"))
-		w->block(on || owner.tacticsMode);
-	if(auto w = widget<CButton>("alternativeAction"))
-		w->block(on || owner.tacticsMode);
-	if(auto w = widget<CButton>("autofight"))
-		w->block(owner.actionsController->spellcastingModeActive());
-
-	auto btactEnd = widget<CButton>("tacticEnd");
-	auto btactNext = widget<CButton>("tacticNext");
-	if(owner.tacticsMode && btactEnd && btactNext)
-	{
-		btactNext->block(on);
-		btactEnd->block(on);
-	}
-	else
-	{
-		auto bConsoleUp = widget<CButton>("consoleUp");
-		auto bConsoleDown = widget<CButton>("consoleDown");
-		if(bConsoleUp && bConsoleDown)
-		{
-			bConsoleUp->block(on);
-			bConsoleDown->block(on);
-		}
-	}
+	setShortcutBlocked(EShortcut::GLOBAL_OPTIONS, on);
+	setShortcutBlocked(EShortcut::BATTLE_RETREAT, on || !owner.curInt->cb->battleCanFlee());
+	setShortcutBlocked(EShortcut::BATTLE_SURRENDER, on || owner.curInt->cb->battleGetSurrenderCost() < 0);
+	setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells);
+	setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait);
+	setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_SELECT_ACTION, on || owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, owner.actionsController->spellcastingModeActive());
+	setShortcutBlocked(EShortcut::BATTLE_TACTICS_END, on && owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_TACTICS_NEXT, on && owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_DOWN, on && !owner.tacticsMode);
+	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode);
 }
 
 std::optional<uint32_t> BattleWindow::getQueueHoveredUnitId()

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