浏览代码

Merge remote-tracking branch 'upstream/develop' into develop

Xilmi 11 月之前
父节点
当前提交
ffd8758017
共有 69 个文件被更改,包括 1607 次插入1119 次删除
  1. 29 6
      .github/workflows/github.yml
  2. 15 8
      CI/before_install/msvc.sh
  3. 7 0
      CI/install_vcpkg_dependencies.sh
  4. 18 0
      CMakePresets.json
  5. 4 1
      Global.h
  6. 19 1
      Mods/vcmi/config/vcmi/english.json
  7. 66 0
      Mods/vcmi/config/vcmi/spells.json
  8. 1 14
      Mods/vcmi/config/vcmi/ukrainian.json
  9. 1 0
      Mods/vcmi/mod.json
  10. 18 8
      client/ClientCommandManager.cpp
  11. 1 1
      client/ClientCommandManager.h
  12. 3 3
      client/widgets/MiscWidgets.cpp
  13. 19 3
      client/windows/CCreatureWindow.cpp
  14. 1 0
      client/windows/CCreatureWindow.h
  15. 46 12
      client/windows/CExchangeWindow.cpp
  16. 4 16
      client/windows/CKingdomInterface.cpp
  17. 8 0
      config/bonuses.json
  18. 1 0
      config/gameConfig.json
  19. 0 17
      config/objects/generic.json
  20. 29 0
      config/objects/lighthouse.json
  21. 9 9
      config/objects/shrine.json
  22. 730 730
      config/spells/moats.json
  23. 2 2
      config/spells/other.json
  24. 7 7
      config/spells/vcmiAbility.json
  25. 4 0
      docs/modders/Bonus/Bonus_Types.md
  26. 1 1
      docs/modders/Map_Object_Format.md
  27. 39 0
      docs/modders/Map_Objects/Flaggable.md
  28. 1 0
      docs/players/Cheat_Codes.md
  29. 2 0
      docs/translators/Translations.md
  30. 10 3
      launcher/settingsView/csettingsview_moc.cpp
  31. 4 0
      lib/CMakeLists.txt
  32. 1 1
      lib/CSkillHandler.cpp
  33. 46 55
      lib/battle/BattleInfo.cpp
  34. 29 17
      lib/battle/CBattleInfoCallback.cpp
  35. 8 1
      lib/bonuses/Bonus.cpp
  36. 2 1
      lib/bonuses/Bonus.h
  37. 1 0
      lib/bonuses/BonusEnum.h
  38. 3 3
      lib/bonuses/CBonusSystemNode.cpp
  39. 3 0
      lib/filesystem/CCompressedStream.cpp
  40. 40 19
      lib/json/JsonParser.cpp
  41. 9 5
      lib/mapObjectConstructors/CObjectClassesHandler.cpp
  42. 60 0
      lib/mapObjectConstructors/FlaggableInstanceConstructor.cpp
  43. 41 0
      lib/mapObjectConstructors/FlaggableInstanceConstructor.h
  44. 2 2
      lib/mapObjects/CBank.cpp
  45. 2 2
      lib/mapObjects/CGTownInstance.cpp
  46. 105 0
      lib/mapObjects/FlaggableMapObject.cpp
  47. 41 0
      lib/mapObjects/FlaggableMapObject.h
  48. 0 69
      lib/mapObjects/MiscObjects.cpp
  49. 0 22
      lib/mapObjects/MiscObjects.h
  50. 6 3
      lib/mapping/MapFormatH3M.cpp
  51. 1 1
      lib/mapping/MapFormatH3M.h
  52. 0 2
      lib/mapping/MapFormatJson.cpp
  53. 5 3
      lib/mapping/MapReaderH3M.cpp
  54. 2 2
      lib/modding/CModHandler.cpp
  55. 33 19
      lib/rmg/CMapGenerator.cpp
  56. 2 4
      lib/rmg/CMapGenerator.h
  57. 4 1
      lib/serializer/RegisterTypes.h
  58. 1 12
      lib/texts/CGeneralTextHandler.cpp
  59. 0 4
      lib/texts/CGeneralTextHandler.h
  60. 6 0
      lib/texts/MetaString.cpp
  61. 2 0
      lib/texts/MetaString.h
  62. 13 5
      lib/texts/TextLocalizationContainer.cpp
  63. 5 3
      lib/texts/TextLocalizationContainer.h
  64. 0 1
      mapeditor/StdInc.h
  65. 11 9
      mapeditor/inspector/inspector.cpp
  66. 3 2
      mapeditor/inspector/inspector.h
  67. 2 1
      mapeditor/mainwindow.cpp
  68. 4 3
      mapeditor/mapcontroller.cpp
  69. 15 5
      server/battles/BattleProcessor.cpp

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

@@ -38,6 +38,7 @@ jobs:
             os: macos-13
             test: 0
             pack: 1
+            upload: 1
             pack_type: Release
             extension: dmg
             before_install: macos.sh
@@ -50,6 +51,7 @@ jobs:
             os: macos-13
             test: 0
             pack: 1
+            upload: 1
             pack_type: Release
             extension: dmg
             before_install: macos.sh
@@ -62,6 +64,7 @@ jobs:
             os: macos-13
             test: 0
             pack: 1
+            upload: 1
             pack_type: Release
             extension: ipa
             before_install: macos.sh
@@ -69,7 +72,7 @@ jobs:
             conan_profile: ios-arm64
             conan_prebuilts: dependencies-ios
             conan_options: --options with_apple_system_libs=True
-          - platform: msvc
+          - platform: msvc-x64
             os: windows-latest
             test: 0
             pack: 1
@@ -77,10 +80,19 @@ jobs:
             extension: exe
             before_install: msvc.sh
             preset: windows-msvc-release
+          - platform: msvc-x86
+            os: windows-latest
+            test: 0
+            pack: 1
+            pack_type: RelWithDebInfo
+            extension: exe
+            before_install: msvc.sh
+            preset: windows-msvc-release-x86
           - platform: mingw_x86_64
             os: ubuntu-24.04
             test: 0
             pack: 1
+            upload: 1
             pack_type: Release
             extension: exe
             cmake_args: -G Ninja
@@ -101,6 +113,7 @@ jobs:
             conan_prebuilts: dependencies-mingw-x86
           - platform: android-32
             os: ubuntu-24.04
+            upload: 1
             extension: apk
             preset: android-conan-ninja-release
             before_install: android.sh
@@ -109,6 +122,7 @@ jobs:
             artifact_platform: armeabi-v7a
           - platform: android-64
             os: ubuntu-24.04
+            upload: 1
             extension: apk
             preset: android-conan-ninja-release
             before_install: android.sh
@@ -136,6 +150,10 @@ jobs:
       if: "${{ matrix.conan_prebuilts != '' }}"
       run: source '${{github.workspace}}/CI/install_conan_dependencies.sh' '${{matrix.conan_prebuilts}}'
 
+    - name: Install vcpkg Dependencies
+      if: ${{ startsWith(matrix.platform, 'msvc') }}
+      run: source '${{github.workspace}}/CI/install_vcpkg_dependencies.sh' '${{matrix.platform}}'
+
     # ensure the ccache for each PR is separate so they don't interfere with each other
     # fall back to ccache of the vcmi/vcmi repo if no PR-specific ccache is found
     - name: ccache for PRs
@@ -232,11 +250,11 @@ jobs:
         elif [[ (${{matrix.preset}} == android-conan-ninja-release) && (${{github.ref}} != 'refs/heads/master') ]]
         then
             cmake -DENABLE_CCACHE:BOOL=ON -DANDROID_GRADLE_PROPERTIES="applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily" --preset ${{ matrix.preset }}
-        elif [[ ${{matrix.platform}} != msvc ]]
+        elif [[ ${{startsWith(matrix.platform, 'msvc') }} ]]
         then
-            cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }}
-        else
             cmake --preset ${{ matrix.preset }}
+        else
+            cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }}
         fi
 
     - name: Build
@@ -273,12 +291,14 @@ jobs:
             sleep 3
             ((counter++))
         done
+        rm -rf _CPack_Packages
 
     - name: Artifacts
       if: ${{ matrix.pack == 1 }}
       uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
+        compression-level: 0
         path: |
           ${{github.workspace}}/out/build/${{matrix.preset}}/${{ env.VCMI_PACKAGE_FILE_NAME }}.${{ matrix.extension }}
 
@@ -299,6 +319,7 @@ jobs:
       uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
+        compression-level: 0
         path: |
           ${{ env.ANDROID_APK_PATH }}
 
@@ -307,19 +328,21 @@ jobs:
       uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - aab
+        compression-level: 0
         path: |
           ${{ env.ANDROID_AAB_PATH }}
 
     - name: Upload debug symbols
-      if: ${{ matrix.platform == 'msvc' }}
+      if: ${{ startsWith(matrix.platform, 'msvc') }}
       uses: actions/upload-artifact@v4
       with:
         name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols
+        compression-level: 9
         path: |
             ${{github.workspace}}/**/*.pdb
 
     - 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' && matrix.platform != 'mingw-32' }}
+      if: ${{ (matrix.upload == 1) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) }}
       continue-on-error: true
       run: |
         if [ -z '${{ env.ANDROID_APK_PATH }}' ] ; then

+ 15 - 8
CI/before_install/msvc.sh

@@ -1,10 +1,17 @@
-curl -LfsS -o "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" \
-	"https://github.com/vcmi/vcmi-deps-windows/releases/download/v1.7/vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z"
-7z x "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z"
+#!/usr/bin/env bash
 
-#rm -r -f vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug
-#mkdir -p vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
-#cp vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/bin/* vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
+MSVC_INSTALL_PATH=$(vswhere -latest -property installationPath)
+echo "MSVC_INSTALL_PATH = $MSVC_INSTALL_PATH"
+echo "Installed toolset versions:"
+ls -vr "$MSVC_INSTALL_PATH/VC/Tools/MSVC"
 
-DUMPBIN_DIR=$(vswhere -latest -find **/dumpbin.exe | head -n 1)
-dirname "$DUMPBIN_DIR" > $GITHUB_PATH
+TOOLS_DIR=$(ls -vr "$MSVC_INSTALL_PATH/VC/Tools/MSVC/" | head -1)
+DUMPBIN_PATH="$MSVC_INSTALL_PATH/VC/Tools/MSVC/$TOOLS_DIR/bin/Hostx64/x64/dumpbin.exe"
+
+# This command should work as well, but for some reason it is *extremely* slow on the Github CI (~7 minutes)
+#DUMPBIN_PATH=$(vswhere -latest -find **/dumpbin.exe | head -n 1)
+
+echo "TOOLS_DIR = $TOOLS_DIR"
+echo "DUMPBIN_PATH = $DUMPBIN_PATH"
+
+dirname "$DUMPBIN_PATH" > "$GITHUB_PATH"

+ 7 - 0
CI/install_vcpkg_dependencies.sh

@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+RELEASE_TAG="v1.8"
+FILENAME="dependencies-$1"
+DOWNLOAD_URL="https://github.com/vcmi/vcmi-deps-windows/releases/download/$RELEASE_TAG/$FILENAME.txz"
+
+curl -L "$DOWNLOAD_URL" | tar -xf - --xz

+ 18 - 0
CMakePresets.json

@@ -154,6 +154,19 @@
 
             }
         },
+        {
+            "name": "windows-msvc-release-x86",
+            "displayName": "Windows x86 RelWithDebInfo",
+            "description": "VCMI RelWithDebInfo build",
+            "inherits": "default-release",
+            "generator": "Visual Studio 17 2022",
+            "cacheVariables": {
+                "CMAKE_TOOLCHAIN_FILE": "${sourceDir}/vcpkg/scripts/buildsystems/vcpkg.cmake",
+                "CMAKE_POLICY_DEFAULT_CMP0091": "NEW",
+                "FORCE_BUNDLED_MINIZIP": "ON",
+                "CMAKE_GENERATOR_PLATFORM": "WIN32"
+            }
+        },
         {
             "name": "windows-msvc-release-ccache",
             "displayName": "Windows x64 RelWithDebInfo with ccache",
@@ -382,6 +395,11 @@
             "configurePreset": "windows-msvc-release",
             "inherits": "default-release"
         },
+        {
+            "name": "windows-msvc-release-x86",
+            "configurePreset": "windows-msvc-release-x86",
+            "inherits": "default-release"
+        },
         {
             "name": "windows-msvc-release-ccache",
             "configurePreset": "windows-msvc-release-ccache",

+ 4 - 1
Global.h

@@ -154,7 +154,10 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #endif
 #define BOOST_THREAD_DONT_PROVIDE_THREAD_DESTRUCTOR_CALLS_TERMINATE_IF_JOINABLE 1
 //need to link boost thread dynamically to avoid https://stackoverflow.com/questions/35978572/boost-thread-interupt-does-not-work-when-crossing-a-dll-boundary
-#define BOOST_THREAD_USE_DLL //for example VCAI::finish() may freeze on thread join after interrupt when linking this statically
+//for example VCAI::finish() may freeze on thread join after interrupt when linking this statically
+#ifndef BOOST_THREAD_USE_DLL
+#  define BOOST_THREAD_USE_DLL
+#endif
 #define BOOST_BIND_NO_PLACEHOLDERS
 
 #if BOOST_VERSION >= 106600

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

@@ -42,6 +42,12 @@
 	"vcmi.heroOverview.secondarySkills" : "Secondary Skills",
 	"vcmi.heroOverview.spells" : "Spells",
 	
+	"vcmi.quickExchange.moveUnit" : "Move Unit",
+	"vcmi.quickExchange.moveAllUnits" : "Move All Units",
+	"vcmi.quickExchange.swapAllUnits" : "Swap Armies",
+	"vcmi.quickExchange.moveAllArtifacts" : "Move All Artifacts",
+	"vcmi.quickExchange.swapAllArtifacts" : "Swap Artifact",
+	
 	"vcmi.radialWheel.mergeSameUnit" : "Merge same creatures",
 	"vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures",
 	"vcmi.radialWheel.splitSingleUnit" : "Split off single creature",
@@ -60,6 +66,16 @@
 	"vcmi.radialWheel.moveUp" : "Move up",
 	"vcmi.radialWheel.moveDown" : "Move down",
 	"vcmi.radialWheel.moveBottom" : "Move to bottom",
+	
+	"vcmi.randomMap.description" : "Map created by the Random Map Generator.\nTemplate was %s, size %dx%d, levels %d, players %d, computers %d, water %s, monster %s, VCMI map",
+	"vcmi.randomMap.description.isHuman" : ", %s is human",
+	"vcmi.randomMap.description.townChoice" : ", %s town choice is %s",
+	"vcmi.randomMap.description.water.none" : "none",
+	"vcmi.randomMap.description.water.normal" : "normal",
+	"vcmi.randomMap.description.water.islands" : "islands",
+	"vcmi.randomMap.description.monster.weak" : "weak",
+	"vcmi.randomMap.description.monster.normal" : "normal",
+	"vcmi.randomMap.description.monster.strong" : "strong",
 
 	"vcmi.spellBook.search" : "search...",
 
@@ -687,5 +703,7 @@
 	"core.bonus.DISINTEGRATE.name": "Disintegrate",
 	"core.bonus.DISINTEGRATE.description": "No corpse remains after death",
 	"core.bonus.INVINCIBLE.name": "Invincible",
-	"core.bonus.INVINCIBLE.description": "Cannot be affected by anything"
+	"core.bonus.INVINCIBLE.description": "Cannot be affected by anything",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Prism Breath",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Prism Breath Attack (three directions)"
 }

+ 66 - 0
Mods/vcmi/config/vcmi/spells.json

@@ -0,0 +1,66 @@
+{
+	"core:summonDemons" : {
+		"name": "Summon Demons"
+	},
+	"core:firstAid" : {
+		"name": "First Aid"
+	},
+	"core:catapultShot" : {
+		"name": "Catapult shot"
+	},
+	"core:cyclopsShot" : {
+		"name": "Siege shot"
+	},
+	
+	"core:fireWallTrigger" : {
+		"name" : "Fire Wall"
+	},
+	"core:landMineTrigger" : {
+		"name" : "Land Mine",
+	},	
+	"core:castleMoatTrigger" : {
+		"name": "Moat"
+	},
+	"core:castleMoat": {
+		"name": "Moat"
+	},
+	"core:rampartMoatTrigger" : {
+		"name": "Brambles"
+	},
+	"core:rampartMoat": {
+		"name": "Brambles"
+	},
+	"core:towerMoat": {
+		"name": "Land Mine"
+	},
+	"core:infernoMoatTrigger" : {
+		"name": "Lava"
+	},
+	"core:infernoMoat": {
+		"name": "Lava"
+	},
+	"core:necropolisMoatTrigger" : {
+		"name": "Boneyard"
+	},
+	"core:necropolisMoat": {
+		"name": "Boneyard"
+	},
+	"core:dungeonMoatTrigger" : {
+		"name": "Boiling Oil"
+	},
+	"core:dungeonMoat": {
+		"name": "Boiling Oil"
+	},
+	"core:strongholdMoatTrigger" : {
+		"name": "Wooden Spikes"
+	},
+	"core:strongholdMoat": {
+		"name": "Wooden Spikes"
+	},
+	"core:fortressMoatTrigger" : {
+		"name": "Boiling Tar"
+	},
+	"core:fortressMoat": {
+		"name": "Boiling Tar"
+	}
+}

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

@@ -612,18 +612,5 @@
 	"core.bonus.WIDE_BREATH.name" : "Широкий подих",
 	"core.bonus.WIDE_BREATH.description" : "Атака широким подихом",
 	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Обмежена дальність стрільби",
-	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Не може стріляти по цілях на відстані більше ${val} гексів",
-	
-	"vcmi.stackExperience.description" : "» S t a c k   E x p e r i e n c e   D e t a i l s «\n\nCreature Type ................... : %s\nExperience Rank ................. : %s (%i)\nExperience Points ............... : %i\nExperience Points to Next Rank .. : %i\nMaximum Experience per Battle ... : %i%% (%i)\nNumber of Creatures in stack .... : %i\nMaximum New Recruits\n without losing current Rank .... : %i\nExperience Multiplier ........... : %.2f\nUpgrade Multiplier .............. : %.2f\nExperience after Rank 10 ........ : %i\nMaximum New Recruits to remain at\n Rank 10 if at Maximum Experience : %i",
-	"vcmi.stackExperience.rank.0" :  "Початковий",
-	"vcmi.stackExperience.rank.1" :  "Новачок",
-	"vcmi.stackExperience.rank.2" :  "Підготовлений",
-	"vcmi.stackExperience.rank.3" :  "Досвідчений",
-	"vcmi.stackExperience.rank.4" :  "Випробуваний",
-	"vcmi.stackExperience.rank.5" :  "Ветеран",
-	"vcmi.stackExperience.rank.6" :  "Адепт",
-	"vcmi.stackExperience.rank.7" :  "Експерт",
-	"vcmi.stackExperience.rank.8" :  "Еліта",
-	"vcmi.stackExperience.rank.9" : "Майстер",
-	"vcmi.stackExperience.rank.10" : "Профі"
+	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Не може стріляти по цілях на відстані більше ${val} гексів"
 }

+ 1 - 0
Mods/vcmi/mod.json

@@ -127,6 +127,7 @@
 
 	"factions" : [ "config/vcmi/towerFactions" ],
 	"creatures" : [ "config/vcmi/towerCreature" ],
+	"spells" : [ "config/vcmi/spells" ],
 
 	"translations" : [
 		"config/vcmi/english.json"

+ 18 - 8
client/ClientCommandManager.cpp

@@ -185,12 +185,12 @@ void ClientCommandManager::handleRedrawCommand()
 	GH.windows().totalRedraw();
 }
 
-void ClientCommandManager::handleTranslateGameCommand()
+void ClientCommandManager::handleTranslateGameCommand(bool onlyMissing)
 {
 	std::map<std::string, std::map<std::string, std::string>> textsByMod;
-	VLC->generaltexth->exportAllTexts(textsByMod);
+	VLC->generaltexth->exportAllTexts(textsByMod, onlyMissing);
 
-	const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "translation";
+	const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / ( onlyMissing ? "translationMissing" : "translation");
 	boost::filesystem::create_directories(outPath);
 
 	for(const auto & modEntry : textsByMod)
@@ -254,13 +254,20 @@ void ClientCommandManager::handleTranslateMapsCommand()
 	logGlobal->info("Loading campaigns for export");
 	for (auto const & campaignName : campaignList)
 	{
-		loadedCampaigns.push_back(CampaignHandler::getCampaign(campaignName.getName()));
-		for (auto const & part : loadedCampaigns.back()->allScenarios())
-			loadedCampaigns.back()->getMap(part, nullptr);
+		try
+		{
+			loadedCampaigns.push_back(CampaignHandler::getCampaign(campaignName.getName()));
+			for (auto const & part : loadedCampaigns.back()->allScenarios())
+				loadedCampaigns.back()->getMap(part, nullptr);
+		}
+		catch(std::exception & e)
+		{
+			logGlobal->warn("Campaign %s is invalid. Message: %s", campaignName.getName(), e.what());
+		}
 	}
 
 	std::map<std::string, std::map<std::string, std::string>> textsByMod;
-	VLC->generaltexth->exportAllTexts(textsByMod);
+	VLC->generaltexth->exportAllTexts(textsByMod, false);
 
 	const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "translation";
 	boost::filesystem::create_directories(outPath);
@@ -591,7 +598,10 @@ void ClientCommandManager::processCommand(const std::string & message, bool call
 		handleRedrawCommand();
 
 	else if(message=="translate" || message=="translate game")
-		handleTranslateGameCommand();
+		handleTranslateGameCommand(false);
+
+	else if(message=="translate missing")
+		handleTranslateGameCommand(true);
 
 	else if(message=="translate maps")
 		handleTranslateMapsCommand();

+ 1 - 1
client/ClientCommandManager.h

@@ -46,7 +46,7 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a
 	void handleRedrawCommand();
 
 	// Extracts all translateable game texts into Translation directory, separating files on per-mod basis
-	void handleTranslateGameCommand();
+	void handleTranslateGameCommand(bool onlyMissing);
 
 	// Extracts all translateable texts from maps and campaigns into Translation directory, separating files on per-mod basis
 	void handleTranslateMapsCommand();

+ 3 - 3
client/widgets/MiscWidgets.cpp

@@ -581,13 +581,13 @@ void MoraleLuckBox::set(const AFactionMember * node)
 	else if(morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_MORALE))
 	{
 		auto noMorale = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_MORALE));
-		text += "\n" + noMorale->Description();
+		text += "\n" + noMorale->Description(LOCPLINT->cb.get());
 		component.value = 0;
 	}
 	else if (!morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_LUCK))
 	{
 		auto noLuck = node->getBonusBearer()->getBonus(Selector::type()(BonusType::NO_LUCK));
-		text += "\n" + noLuck->Description();
+		text += "\n" + noLuck->Description(LOCPLINT->cb.get());
 		component.value = 0;
 	}
 	else
@@ -596,7 +596,7 @@ void MoraleLuckBox::set(const AFactionMember * node)
 		for(auto & bonus : * modifierList)
 		{
 			if(bonus->val) {
-				const std::string& description = bonus->Description();
+				const std::string& description = bonus->Description(LOCPLINT->cb.get());
 				//arraytxt already contains \n
 				if (description.size() && description[0] != '\n')
 					addInfo += '\n';

+ 19 - 3
client/windows/CCreatureWindow.cpp

@@ -393,7 +393,7 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i
 
 	auto getSkillDescription = [this](int skillIndex) -> std::string
 	{
-		return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (parent->info->commander->secondarySkills[skillIndex] * 2)];
+		return parent->getCommanderSkillDescription(skillIndex, parent->info->commander->secondarySkills[skillIndex]);
 	};
 
 	for(int index = ECommander::ATTACK; index <= ECommander::SPELL_POWER; ++index)
@@ -905,14 +905,30 @@ std::string CStackWindow::generateStackExpDescription()
 	return expText;
 }
 
+std::string CStackWindow::getCommanderSkillDescription(int skillIndex, int skillLevel)
+{
+	constexpr std::array skillNames = {
+		"attack",
+		"defence",
+		"health",
+		"damage",
+		"speed",
+		"magic"
+	};
+
+	std::string textID = TextIdentifier("vcmi", "commander", "skill", skillNames.at(skillIndex), skillLevel).get();
+
+	return CGI->generaltexth->translate(textID);
+}
+
 void CStackWindow::setSelection(si32 newSkill, std::shared_ptr<CCommanderSkillIcon> newIcon)
 {
 	auto getSkillDescription = [this](int skillIndex, bool selected) -> std::string
 	{
 		if(selected)
-			return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + ((info->commander->secondarySkills[skillIndex] + 1) * 2)]; //upgrade description
+			return getCommanderSkillDescription(skillIndex, info->commander->secondarySkills[skillIndex] + 1); //upgrade description
 		else
-			return CGI->generaltexth->znpc00[152 + (12 * skillIndex) + (info->commander->secondarySkills[skillIndex] * 2)];
+			return getCommanderSkillDescription(skillIndex, info->commander->secondarySkills[skillIndex]);
 	};
 
 	auto getSkillImage = [this](int skillIndex)

+ 1 - 0
client/windows/CCreatureWindow.h

@@ -189,6 +189,7 @@ class CStackWindow : public CWindowObject
 	void init();
 
 	std::string generateStackExpDescription();
+	std::string getCommanderSkillDescription(int skillIndex, int skillLevel);
 
 public:
 	// for battles

+ 46 - 12
client/windows/CExchangeWindow.cpp

@@ -192,18 +192,52 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 
 	if(qeLayout)
 	{
-		buttonMoveUnitsFromLeftToRight = std::make_shared<CButton>(Point(325, 118), AnimationPath::builtin("quick-exchange/armRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), [this](){ this->moveUnitsShortcut(true); });
-		buttonMoveUnitsFromRightToLeft = std::make_shared<CButton>(Point(425, 118), AnimationPath::builtin("quick-exchange/armLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[1]), [this](){ this->moveUnitsShortcut(false); });
-		buttonMoveArtifactsFromLeftToRight = std::make_shared<CButton>(Point(325, 154), AnimationPath::builtin("quick-exchange/artRight.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), [this](){ this->moveArtifactsCallback(true);});
-		buttonMoveArtifactsFromRightToLeft = std::make_shared<CButton>(Point(425, 154), AnimationPath::builtin("quick-exchange/artLeft.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[3]), [this](){ this->moveArtifactsCallback(false);});
-
-		exchangeUnitsButton = std::make_shared<CButton>(Point(377, 118), AnimationPath::builtin("quick-exchange/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[2]), [this](){ controller.swapArmy(); });
-		exchangeArtifactsButton  = std::make_shared<CButton>(Point(377, 154), AnimationPath::builtin("quick-exchange/swapAll.DEF"), CButton::tooltip(CGI->generaltexth->qeModCommands[4]), [this](){ this->swapArtifactsCallback(); });
-
-		backpackButtonLeft = std::make_shared<CButton>(Point(325, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
+		buttonMoveUnitsFromLeftToRight = std::make_shared<CButton>(
+			Point(325, 118),
+			AnimationPath::builtin("quick-exchange/armRight.DEF"),
+			CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveAllUnits")),
+			[this](){ this->moveUnitsShortcut(true); });
+
+		buttonMoveUnitsFromRightToLeft = std::make_shared<CButton>(
+			Point(425, 118),
+			AnimationPath::builtin("quick-exchange/armLeft.DEF"),
+			CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveAllUnits")),
+			[this](){ this->moveUnitsShortcut(false); });
+
+		buttonMoveArtifactsFromLeftToRight = std::make_shared<CButton>(
+			Point(325, 154), AnimationPath::builtin("quick-exchange/artRight.DEF"),
+			CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveAllArtifacts")),
+			[this](){ this->moveArtifactsCallback(true);});
+
+		buttonMoveArtifactsFromRightToLeft = std::make_shared<CButton>(
+			Point(425, 154), AnimationPath::builtin("quick-exchange/artLeft.DEF"),
+			CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveAllArtifacts")),
+			[this](){ this->moveArtifactsCallback(false);});
+
+		exchangeUnitsButton = std::make_shared<CButton>(
+			Point(377, 118),
+			AnimationPath::builtin("quick-exchange/swapAll.DEF"),
+			CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.swapAllUnits")),
+			[this](){ controller.swapArmy(); });
+
+		exchangeArtifactsButton  = std::make_shared<CButton>(
+			Point(377, 154),
+			AnimationPath::builtin("quick-exchange/swapAll.DEF"),
+			CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.swapAllArtifacts")),
+			[this](){ this->swapArtifactsCallback(); });
+
+		backpackButtonLeft = std::make_shared<CButton>(
+			Point(325, 518),
+			AnimationPath::builtin("heroBackpack"),
+			CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
 			[this](){ this->backpackShortcut(true); });
-		backpackButtonRight = std::make_shared<CButton>(Point(419, 518), AnimationPath::builtin("heroBackpack"), CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
+
+		backpackButtonRight = std::make_shared<CButton>(
+			Point(419, 518),
+			AnimationPath::builtin("heroBackpack"),
+			CButton::tooltipLocalized("vcmi.heroWindow.openBackpack"),
 			[this](){ this->backpackShortcut(false); });
+
 		backpackButtonLeft->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
 		backpackButtonRight->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
 
@@ -227,7 +261,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 				std::make_shared<CButton>(
 					Point(484 + 35 * i, 154),
 					AnimationPath::builtin("quick-exchange/unitLeft.DEF"),
-					CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
+					CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveUnit")),
 					std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i))));
 			moveUnitFromRightToLeftButtons.back()->block(leftHeroBlock);
 
@@ -235,7 +269,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 				std::make_shared<CButton>(
 					Point(66 + 35 * i, 154),
 					AnimationPath::builtin("quick-exchange/unitRight.DEF"),
-					CButton::tooltip(CGI->generaltexth->qeModCommands[1]),
+					CButton::tooltip(CGI->generaltexth->translate("vcmi.quickExchange.moveUnit")),
 					std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i))));
 			moveUnitFromLeftToRightButtons.back()->block(rightHeroBlock);
 		}

+ 4 - 16
client/windows/CKingdomInterface.cpp

@@ -583,28 +583,16 @@ void CKingdomInterface::generateMinesList(const std::vector<const CGObjectInstan
 		if(object->ID == Obj::MINE || object->ID == Obj::ABANDONED_MINE)
 		{
 			const CGMine * mine = dynamic_cast<const CGMine *>(object);
-			assert(mine);
 			minesCount[mine->producedResource]++;
-			totalIncome += mine->dailyIncome()[EGameResID::GOLD];
 		}
 	}
 
-	//Heroes can produce gold as well - skill, specialty or arts
-	std::vector<const CGHeroInstance*> heroes = LOCPLINT->cb->getHeroesInfo(true);
-	auto * playerSettings = LOCPLINT->cb->getPlayerSettings(LOCPLINT->playerID);
-	for(auto & hero : heroes)
-	{
-		totalIncome += hero->dailyIncome()[EGameResID::GOLD];
-	}
-
-	//Add town income of all towns
-	std::vector<const CGTownInstance*> towns = LOCPLINT->cb->getTownsInfo(true);
-	for(auto & town : towns)
-	{
-		totalIncome += town->dailyIncome()[EGameResID::GOLD];
-	}
+	for(auto & mapObject : ownedObjects)
+		totalIncome += mapObject->asOwnable()->dailyIncome()[EGameResID::GOLD];
 
 	//if player has some modded boosts we want to show that as well
+	const auto * playerSettings = LOCPLINT->cb->getPlayerSettings(LOCPLINT->playerID);
+	const auto & towns = LOCPLINT->cb->getTownsInfo(true);
 	totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_CONSTANT_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))) * playerSettings->handicap.percentIncome / 100;
 	totalIncome += LOCPLINT->cb->getPlayerState(LOCPLINT->playerID)->valOfBonuses(BonusType::RESOURCES_TOWN_MULTIPLYING_BOOST, BonusSubtypeID(GameResID(EGameResID::GOLD))) * towns.size() * playerSettings->handicap.percentIncome / 100;
 

+ 8 - 0
config/bonuses.json

@@ -548,6 +548,14 @@
 		}
 	},
 
+	"PRISM_HEX_ATTACK_BREATH":
+	{
+		"graphics":
+		{
+			"icon":  "zvs/Lib1.res/PrismBreath"
+		}
+	},
+
 	"THREE_HEADED_ATTACK":
 	{
 		"graphics":

+ 1 - 0
config/gameConfig.json

@@ -53,6 +53,7 @@
 		"config/objects/creatureBanks.json",
 		"config/objects/dwellings.json",
 		"config/objects/generic.json",
+		"config/objects/lighthouse.json",
 		"config/objects/magicSpring.json",
 		"config/objects/magicWell.json",
 		"config/objects/moddables.json",

+ 0 - 17
config/objects/generic.json

@@ -270,23 +270,6 @@
 			}
 		}
 	},
-	"lighthouse" : {
-		"index" :42,
-		"handler" : "lighthouse",
-		"base" : {
-			"sounds" : {
-				"visit" : ["LIGHTHOUSE"]
-			}
-		},
-		"types" : {
-			"object" : {
-				"index" : 0,
-				"aiValue" : 500,
-				"rmg" : {
-				}
-			}
-		}
-	},
 	"obelisk" : {
 		"index" :57,
 		"handler" : "obelisk",

+ 29 - 0
config/objects/lighthouse.json

@@ -0,0 +1,29 @@
+{
+	"lighthouse" : {
+		"index" :42,
+		"handler" : "flaggable",
+		"base" : {
+			"sounds" : {
+				"visit" : ["LIGHTHOUSE"]
+			}
+		},
+		"types" : {
+			"lighthouse" : {
+				"compatibilityIdentifiers" : [ "object" ],
+				"index" : 0,
+				"aiValue" : 500,
+				"rmg" : {
+				},
+				
+				"message" : "@core.advevent.69",
+				"bonuses" : {
+					"seaMovement" : {
+						"type" : "MOVEMENT",
+						"subtype" : "heroMovementSea",
+						"val" : 500
+					}
+				}
+			}
+		}
+	}
+}

+ 9 - 9
config/objects/shrine.json

@@ -49,7 +49,7 @@
 						"message" : [ 127, "%s." ] // You learn new spell
 					}
 				],
-				"onVisitedMessage" : [ 127, "%s.", 174 ], // You already known this spell
+				"onVisitedMessage" : [ 127, "%s. ", 174 ], // You already known this spell
 				"onEmpty" : [
 					{
 						"limiter" : {
@@ -59,10 +59,10 @@
 								}
 							]
 						},
-						"message" : [ 127, "%s.", 130 ] // No Wisdom
+						"message" : [ 127, "%s. ", 130 ] // No Wisdom
 					},
 					{
-						"message" : [ 127, "%s.", 131 ] // No spellbook
+						"message" : [ 127, "%s. ", 131 ] // No spellbook
 					}
 				]
 			}
@@ -118,7 +118,7 @@
 						"message" : [ 128, "%s." ] // You learn new spell
 					}
 				],
-				"onVisitedMessage" : [ 128, "%s.", 174 ], // You already known this spell
+				"onVisitedMessage" : [ 128, "%s. ", 174 ], // You already known this spell
 				"onEmpty" : [
 					{
 						"limiter" : {
@@ -128,10 +128,10 @@
 								}
 							]
 						},
-						"message" : [ 128, "%s.", 130 ] // No Wisdom
+						"message" : [ 128, "%s. ", 130 ] // No Wisdom
 					},
 					{
-						"message" : [ 128, "%s.", 131 ] // No spellbook
+						"message" : [ 128, "%s. ", 131 ] // No spellbook
 					}
 				]
 			}
@@ -187,7 +187,7 @@
 						"message" : [ 129, "%s." ] // You learn new spell
 					}
 				],
-				"onVisitedMessage" : [ 129, "%s.", 174 ], // You already known this spell
+				"onVisitedMessage" : [ 129, "%s. ", 174 ], // You already known this spell
 				"onEmpty" : [
 					{
 						"limiter" : {
@@ -197,10 +197,10 @@
 								}
 							]
 						},
-						"message" : [ 129, "%s.", 130 ] // No Wisdom
+						"message" : [ 129, "%s. ", 130 ] // No Wisdom
 					},
 					{
-						"message" : [ 129, "%s.", 131 ] // No spellbook
+						"message" : [ 129, "%s. ", 131 ] // No spellbook
 					}
 				]
 			}

+ 730 - 730
config/spells/moats.json

@@ -1,732 +1,732 @@
 {
-    "castleMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Moat",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "castleMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Moat",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:castleMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 70,
-                        "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "rampartMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Brambles",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "rampartMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Brambles",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:rampartMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 70,
-                        "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "towerMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Land Mine",
-        "school" : {},
-        "level": 3,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : true,
-                        "trap" : false,
-                        "triggerAbility" : "core:landMineTrigger",
-                        "dispellable" : true,
-                        "removeOnTrigger" : true,
-                        "moatDamage" : 150,
-                        "moatHexes" : [[11], [28], [44], [61], [77], [111], [129], [146], [164], [181]],
-                        "defender" :{
-                            "animation" : "C09SPF1",
-                            "appearAnimation" : "C09SPF0",
-                            "appearSound" : "LANDMINE"
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "infernoMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Lava",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "infernoMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Lava",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:infernoMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 90,
-                        "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "necropolisMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Boneyard",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "necropolisMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Boneyard",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:necropolisMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 70,
-                        "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "dungeonMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Boiling Oil",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "dungeonMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Boiling Oil",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:dungeonMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 90,
-                        "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "strongholdMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Wooden Spikes",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "strongholdMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Wooden Spikes",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:strongholdMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 70,
-                        "moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "fortressMoatTrigger" :
-    {
-        "targetType" : "CREATURE",
-        "type": "ability",
-        "name": "Boiling Tar",
-        "school": {},
-        "level": 0,
-        "power": 0,
-        "gainChance": {},
-        "levels" : {
-            "base": {
-                "power" : 0,
-                "range" : "0",
-                "description" : "", //For validation
-                "cost" : 0, //For validation
-                "aiValue" : 0, //For validation
-                "battleEffects" : {
-                    "directDamage" : {
-                        "type":"core:damage"
-                    }
-                },
-                "targetModifier":{"smart":false}
-            },
-            "none" : {
-            },
-            "basic" : {
-            },
-            "advanced" : {
-            },
-            "expert" : {
-            }
-        },
-        "flags" : {
-            "damage": true,
-            "negative": true,
-            "nonMagical" : true,
-            "special": true
-        },
-        "targetCondition" : {
-        }
-    },
-    "fortressMoat": {
-        "targetType" : "NO_TARGET",
-        "type": "ability",
-        "name": "Boiling Tar",
-        "school" : {},
-        "level": 0,
-        "power": 0,
-        "defaultGainChance": 0,
-        "gainChance": {},
-        "levels" : {
-            "base":{
-                "description" : "",
-                "aiValue" : 0,
-                "power" : 0,
-                "cost" : 0,
-                "targetModifier":{"smart":false},
-                "battleEffects":{
-                    "moat":{
-                        "type":"core:moat",
-                        "hidden" : false,
-                        "trap" : true,
-                        "triggerAbility" : "core:fortressMoatTrigger",
-                        "dispellable" : false,
-                        "removeOnTrigger" : false,
-                        "moatDamage" : 90,
-                        "moatHexes" : [[10, 11, 27, 28, 43, 44, 60, 61, 76, 77, 94, 110, 111, 128, 129, 145, 146, 163, 164, 180, 181]],
-                        "defender" :{
-                        },
-                        "bonus" :{
-                            "primarySkill" : {
-                                "val" : -3,
-                                "type" : "PRIMARY_SKILL",
-                                "subtype" : "primarySkill.defence",
-                                "valueType" : "ADDITIVE_VALUE"
-                            }
-                        }
-                    }
-                },
-                "range" : "X"
-            },
-            "none" :{
-            },
-            "basic" :{
-            },
-            "advanced" :{
-            },
-            "expert" :{
-            }
-        },
-        "flags" : {
-            "nonMagical" : true,
-            "indifferent": true
-        },
-        "targetCondition" : {
-        }
-    }
+	"castleMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"castleMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:castleMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 70,
+						"moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"rampartMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"rampartMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:rampartMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 70,
+						"moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"towerMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 3,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : true,
+						"trap" : false,
+						"triggerAbility" : "core:landMineTrigger",
+						"dispellable" : true,
+						"removeOnTrigger" : true,
+						"moatDamage" : 150,
+						"moatHexes" : [[11], [28], [44], [61], [77], [111], [129], [146], [164], [181]],
+						"defender" :{
+							"animation" : "C09SPF1",
+							"appearAnimation" : "C09SPF0",
+							"appearSound" : "LANDMINE"
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"infernoMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"infernoMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:infernoMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 90,
+						"moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"necropolisMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"necropolisMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:necropolisMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 70,
+						"moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"dungeonMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"dungeonMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:dungeonMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 90,
+						"moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"strongholdMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"strongholdMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:strongholdMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 70,
+						"moatHexes" : [[11, 28, 44, 61, 77, 111, 129, 146, 164, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"fortressMoatTrigger" :
+	{
+		"targetType" : "CREATURE",
+		"type": "ability",
+		"name": "",
+		"school": {},
+		"level": 0,
+		"power": 0,
+		"gainChance": {},
+		"levels" : {
+			"base": {
+				"power" : 0,
+				"range" : "0",
+				"description" : "", //For validation
+				"cost" : 0, //For validation
+				"aiValue" : 0, //For validation
+				"battleEffects" : {
+					"directDamage" : {
+						"type":"core:damage"
+					}
+				},
+				"targetModifier":{"smart":false}
+			},
+			"none" : {
+			},
+			"basic" : {
+			},
+			"advanced" : {
+			},
+			"expert" : {
+			}
+		},
+		"flags" : {
+			"damage": true,
+			"negative": true,
+			"nonMagical" : true,
+			"special": true
+		},
+		"targetCondition" : {
+		}
+	},
+	"fortressMoat": {
+		"targetType" : "NO_TARGET",
+		"type": "ability",
+		"name": "",
+		"school" : {},
+		"level": 0,
+		"power": 0,
+		"defaultGainChance": 0,
+		"gainChance": {},
+		"levels" : {
+			"base":{
+				"description" : "",
+				"aiValue" : 0,
+				"power" : 0,
+				"cost" : 0,
+				"targetModifier":{"smart":false},
+				"battleEffects":{
+					"moat":{
+						"type":"core:moat",
+						"hidden" : false,
+						"trap" : true,
+						"triggerAbility" : "core:fortressMoatTrigger",
+						"dispellable" : false,
+						"removeOnTrigger" : false,
+						"moatDamage" : 90,
+						"moatHexes" : [[10, 11, 27, 28, 43, 44, 60, 61, 76, 77, 94, 110, 111, 128, 129, 145, 146, 163, 164, 180, 181]],
+						"defender" :{
+						},
+						"bonus" :{
+							"primarySkill" : {
+								"val" : -3,
+								"type" : "PRIMARY_SKILL",
+								"subtype" : "primarySkill.defence",
+								"valueType" : "ADDITIVE_VALUE"
+							}
+						}
+					}
+				},
+				"range" : "X"
+			},
+			"none" :{
+			},
+			"basic" :{
+			},
+			"advanced" :{
+			},
+			"expert" :{
+			}
+		},
+		"flags" : {
+			"nonMagical" : true,
+			"indifferent": true
+		},
+		"targetCondition" : {
+		}
+	}
 }

+ 2 - 2
config/spells/other.json

@@ -55,7 +55,7 @@
 	{
 		"targetType" : "CREATURE",
 		"type": "combat",
-		"name": "Land Mine",
+		"name": "",
 		"school":
 		{
 			"air": false,
@@ -237,7 +237,7 @@
 	"fireWallTrigger" : {
 		"targetType" : "CREATURE",
 		"type": "combat",
-		"name": "Fire Wall",
+		"name": "",
 		"school":
 		{
 			"air": false,

+ 7 - 7
config/spells/vcmiAbility.json

@@ -1,8 +1,8 @@
 {
-    "summonDemons" : {
+	"summonDemons" : {
 		"type": "ability",
 		"targetType" : "CREATURE",
-		"name": "Summon Demons",
+		"name": "",
 		"school" : {},
 		"level": 2,
 		"power": 50,
@@ -46,11 +46,11 @@
 				"bonus.GARGOYLE" : "absolute"
 			}
 		}
-    },
-    "firstAid" : {
+	},
+	"firstAid" : {
 		"targetType" : "CREATURE",
 		"type": "ability",
-		"name": "First Aid",
+		"name": "",
 		"school" : {},
 		"level": 1,
 		"power": 10,
@@ -106,7 +106,7 @@
 	"catapultShot" : {
 		"targetType" : "LOCATION",
 		"type": "ability",
-		"name": "Catapult shot",
+		"name": "",
 		"school" : {},
 		"level": 1,
 		"power": 1,
@@ -187,7 +187,7 @@
 	"cyclopsShot" : {
 		"targetType" : "LOCATION",
 		"type": "ability",
-		"name": "Siege shot",
+		"name": "",
 		"school" : {},
 		"level": 1,
 		"power": 1,

+ 4 - 0
docs/modders/Bonus/Bonus_Types.md

@@ -502,6 +502,10 @@ Affected unit attacks all adjacent creatures (Hydra). Only directly targeted cre
 
 Affected unit attacks creature located directly behind targeted tile (Dragons). Only directly targeted creature will attempt to retaliate
 
+### PRISM_HEX_ATTACK_BREATH
+
+Like `TWO_HEX_ATTACK_BREATH` but affects also two additional cratures (in triangle form from target tile)
+
 ### RETURN_AFTER_STRIKE
 
 Affected unit can return to his starting location after attack (Harpies)

+ 1 - 1
docs/modders/Map_Object_Format.md

@@ -49,6 +49,7 @@ These are object types that are available for modding and have configurable prop
 - `dwelling` - see [Dwelling](Map_Objects/Dwelling.md). Object that allows recruitments of units outside of towns
 - `market` - see [Market](Map_Objects/Market.md). Trading resources, artifacts, creatures and such
 - `boat` - see [Boat](Map_Objects/Boat.md). Object to move across different terrains, such as water
+- `flaggable` - see [Flaggable](Map_Objects/Flaggable.md). Object that can be flagged by a player to provide [Bonus](Bonus_Format.md) or resources
 - `hillFort` - TODO: documentation. See config files in vcmi installation for reference
 - `shipyard` - TODO: documentation. See config files in vcmi installation for reference
 - `terrain` - Defines terrain overlays such as magic grounds. TODO: documentation. See config files in vcmi installation for reference
@@ -60,7 +61,6 @@ These are types that don't have configurable properties, however it is possible
 - `generic` - Defines empty object type that provides no functionality. Note that unlike `static`, objects of this type are never used by RMG
 - `borderGate`
 - `borderGuard`
-- `lighthouse`
 - `magi`
 - `mine`
 - `obelisk`

+ 39 - 0
docs/modders/Map_Objects/Flaggable.md

@@ -0,0 +1,39 @@
+# Flaggable objects
+
+Flaggable object are those that can be captured by a visiting hero. H3 examples are mines, dwellings, or lighthouse.
+
+Currently, it is possible to make flaggable objects that provide player with:
+- Any [Bonus](Bonus_Format.md) supported by bonus system
+- Daily resources income (wood, ore, gold, etc)
+
+## Format description
+
+```jsonc
+{
+  "baseObjectName" : {
+    "name" : "Object name",
+    "handler" : "flaggable", 
+    "types" : {
+      "objectName" : {
+        
+        // Text for message that player will get on capturing this object with a hero
+        // Alternatively, it is possible to reuse existing string from H3 using form '@core.advevent.69'
+        "onVisit" : "{Object Name}\r\n\r\nText of messages that player will see on visit.",
+        
+        // List of bonuses that will be granted to player that owns this object
+        "bonuses" : {
+          "firstBonus" : { BONUS FORMAT },
+          "secondBonus" : { BONUS FORMAT },
+        },
+        
+        // Resources that will be given to owner on every day
+        "dailyIncome" : {
+          "wood" : 2,
+          "ore"  : 2,
+          "gold" : 1000
+        }
+      }
+    }
+  }
+}
+```

+ 1 - 0
docs/players/Cheat_Codes.md

@@ -121,6 +121,7 @@ Below a list of supported commands, with their arguments wrapped in `<>`
 
 #### Extract commands
 `translate` - save game texts into json files
+`translate missing` - save untranslated game texts into json files
 `translate maps` - save map and campaign texts into json files
 `get config` - save game objects data into json files
 `get scripts` - dumps lua script stuff into files (currently inactive due to scripting disabled for default builds)

+ 2 - 0
docs/translators/Translations.md

@@ -136,6 +136,8 @@ After that, start Launcher, switch to Help tab and open "log files directory". Y
 
 If your mod also contains maps or campaigns that you want to translate, then use '/translate maps' command instead.
 
+If you want to update existing translation, you can use '/translate missing' command that will export only strings that were not translated
+
 ### Translating mod information
 In order to display information in Launcher in language selected by user add following block into your `mod.json`:
 ```

+ 10 - 3
launcher/settingsView/csettingsview_moc.cpp

@@ -838,9 +838,16 @@ void CSettingsView::on_sliderScalingCursor_valueChanged(int value)
 
 void CSettingsView::on_buttonScalingAuto_toggled(bool checked)
 {
-	ui->spinBoxInterfaceScaling->setDisabled(checked);
-	ui->spinBoxInterfaceScaling->setValue(100);
-
+	if (checked)
+	{
+		ui->spinBoxInterfaceScaling->hide();
+	}
+	else
+	{
+		ui->spinBoxInterfaceScaling->show();
+		ui->spinBoxInterfaceScaling->setValue(100);
+	}
+	
 	Settings node = settings.write["video"]["resolution"]["scaling"];
 	node->Integer() = checked ? 0 : 100;
 }

+ 4 - 0
lib/CMakeLists.txt

@@ -116,6 +116,7 @@ set(lib_MAIN_SRCS
 	mapObjectConstructors/CommonConstructors.cpp
 	mapObjectConstructors/CRewardableConstructor.cpp
 	mapObjectConstructors/DwellingInstanceConstructor.cpp
+	mapObjectConstructors/FlaggableInstanceConstructor.cpp
 	mapObjectConstructors/HillFortInstanceConstructor.cpp
 	mapObjectConstructors/ShipyardInstanceConstructor.cpp
 
@@ -132,6 +133,7 @@ set(lib_MAIN_SRCS
 	mapObjects/CObjectHandler.cpp
 	mapObjects/CQuest.cpp
 	mapObjects/CRewardableObject.cpp
+	mapObjects/FlaggableMapObject.cpp
 	mapObjects/IMarket.cpp
 	mapObjects/IObjectInterface.cpp
 	mapObjects/MiscObjects.cpp
@@ -497,6 +499,7 @@ set(lib_MAIN_HEADERS
 	mapObjectConstructors/CRewardableConstructor.h
 	mapObjectConstructors/DwellingInstanceConstructor.h
 	mapObjectConstructors/HillFortInstanceConstructor.h
+	mapObjectConstructors/FlaggableInstanceConstructor.h
 	mapObjectConstructors/IObjectInfo.h
 	mapObjectConstructors/RandomMapInfo.h
 	mapObjectConstructors/ShipyardInstanceConstructor.h
@@ -515,6 +518,7 @@ set(lib_MAIN_HEADERS
 	mapObjects/CObjectHandler.h
 	mapObjects/CQuest.h
 	mapObjects/CRewardableObject.h
+	mapObjects/FlaggableMapObject.h
 	mapObjects/IMarket.h
 	mapObjects/IObjectInterface.h
 	mapObjects/IOwnableObject.h

+ 1 - 1
lib/CSkillHandler.cpp

@@ -122,7 +122,7 @@ CSkill::LevelInfo & CSkill::at(int level)
 DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInfo & info)
 {
 	for(int i=0; i < info.effects.size(); i++)
-		out << (i ? "," : "") << info.effects[i]->Description();
+		out << (i ? "," : "") << info.effects[i]->Description(nullptr);
 	return out << "])";
 }
 

+ 46 - 55
lib/battle/BattleInfo.cpp

@@ -161,54 +161,45 @@ struct RangeGenerator
 BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const BattleField & battlefieldType, BattleSideArray<const CArmedInstance *> armies, BattleSideArray<const CGHeroInstance *> heroes, const BattleLayout & layout, const CGTownInstance * town)
 {
 	CMP_stack cmpst;
-	auto * curB = new BattleInfo(layout);
+	auto * currentBattle = new BattleInfo(layout);
 
 	for(auto i : { BattleSide::LEFT_SIDE, BattleSide::RIGHT_SIDE})
-		curB->sides[i].init(heroes[i], armies[i]);
+		currentBattle->sides[i].init(heroes[i], armies[i]);
 
-	std::vector<CStack*> & stacks = (curB->stacks);
+	std::vector<CStack*> & stacks = (currentBattle->stacks);
 
-	curB->tile = tile;
-	curB->battlefieldType = battlefieldType;
-	curB->round = -2;
-	curB->activeStack = -1;
-	curB->replayAllowed = false;
-
-	if(town)
-	{
-		curB->town = town;
-		curB->terrainType = town->getNativeTerrain();
-	}
-	else
-	{
-		curB->town = nullptr;
-		curB->terrainType = terrain;
-	}
+	currentBattle->tile = tile;
+	currentBattle->terrainType = terrain;
+	currentBattle->battlefieldType = battlefieldType;
+	currentBattle->round = -2;
+	currentBattle->activeStack = -1;
+	currentBattle->replayAllowed = false;
+	currentBattle->town = town;
 
 	//setting up siege obstacles
 	if (town && town->fortificationsLevel().wallsHealth != 0)
 	{
 		auto fortification = town->fortificationsLevel();
 
-		curB->si.gateState = EGateState::CLOSED;
+		currentBattle->si.gateState = EGateState::CLOSED;
 
-		curB->si.wallState[EWallPart::GATE] = EWallState::INTACT;
+		currentBattle->si.wallState[EWallPart::GATE] = EWallState::INTACT;
 
 		for(const auto wall : {EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL})
-			curB->si.wallState[wall] = static_cast<EWallState>(fortification.wallsHealth);
+			currentBattle->si.wallState[wall] = static_cast<EWallState>(fortification.wallsHealth);
 
 		if (fortification.citadelHealth != 0)
-			curB->si.wallState[EWallPart::KEEP] = static_cast<EWallState>(fortification.citadelHealth);
+			currentBattle->si.wallState[EWallPart::KEEP] = static_cast<EWallState>(fortification.citadelHealth);
 
 		if (fortification.upperTowerHealth != 0)
-			curB->si.wallState[EWallPart::UPPER_TOWER] = static_cast<EWallState>(fortification.upperTowerHealth);
+			currentBattle->si.wallState[EWallPart::UPPER_TOWER] = static_cast<EWallState>(fortification.upperTowerHealth);
 
 		if (fortification.lowerTowerHealth != 0)
-			curB->si.wallState[EWallPart::BOTTOM_TOWER] = static_cast<EWallState>(fortification.lowerTowerHealth);
+			currentBattle->si.wallState[EWallPart::BOTTOM_TOWER] = static_cast<EWallState>(fortification.lowerTowerHealth);
 	}
 
 	//randomize obstacles
-	if (layout.obstaclesAllowed && !town)
+	if (layout.obstaclesAllowed && (!town || !town->hasFort()))
  	{
 		RandGen r{};
 		auto ourRand = [&](){ return r.rand(); };
@@ -221,12 +212,12 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 		auto appropriateAbsoluteObstacle = [&](int id)
 		{
 			const auto * info = Obstacle(id).getInfo();
-			return info && info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType);
+			return info && info->isAbsoluteObstacle && info->isAppropriate(currentBattle->terrainType, battlefieldType);
 		};
 		auto appropriateUsualObstacle = [&](int id)
 		{
 			const auto * info = Obstacle(id).getInfo();
-			return info && !info->isAbsoluteObstacle && info->isAppropriate(curB->terrainType, battlefieldType);
+			return info && !info->isAbsoluteObstacle && info->isAppropriate(currentBattle->terrainType, battlefieldType);
 		};
 
 		if(r.rand(1,100) <= 40) //put cliff-like obstacle
@@ -237,8 +228,8 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 				auto obstPtr = std::make_shared<CObstacleInstance>();
 				obstPtr->obstacleType = CObstacleInstance::ABSOLUTE_OBSTACLE;
 				obstPtr->ID = obidgen.getSuchNumber(appropriateAbsoluteObstacle);
-				obstPtr->uniqueID = static_cast<si32>(curB->obstacles.size());
-				curB->obstacles.push_back(obstPtr);
+				obstPtr->uniqueID = static_cast<si32>(currentBattle->obstacles.size());
+				currentBattle->obstacles.push_back(obstPtr);
 
 				for(BattleHex blocked : obstPtr->getBlockedTiles())
 					blockedTiles.push_back(blocked);
@@ -256,7 +247,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 			while(tilesToBlock > 0)
 			{
 				RangeGenerator obidgen(0, VLC->obstacleHandler->size() - 1, ourRand);
-				auto tileAccessibility = curB->getAccessibility();
+				auto tileAccessibility = currentBattle->getAccessibility();
 				const int obid = obidgen.getSuchNumber(appropriateUsualObstacle);
 				const ObstacleInfo &obi = *Obstacle(obid).getInfo();
 
@@ -290,8 +281,8 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 				auto obstPtr = std::make_shared<CObstacleInstance>();
 				obstPtr->ID = obid;
 				obstPtr->pos = posgenerator.getSuchNumber(validPosition);
-				obstPtr->uniqueID = static_cast<si32>(curB->obstacles.size());
-				curB->obstacles.push_back(obstPtr);
+				obstPtr->uniqueID = static_cast<si32>(currentBattle->obstacles.size());
+				currentBattle->obstacles.push_back(obstPtr);
 
 				for(BattleHex blocked : obstPtr->getBlockedTiles())
 					blockedTiles.push_back(blocked);
@@ -315,7 +306,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 			CreatureID cre = warMachineArt->artType->getWarMachine();
 
 			if(cre != CreatureID::NONE)
-				curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(cre, 1), side, SlotID::WAR_MACHINES_SLOT, hex);
+				currentBattle->generateNewStack(currentBattle->nextUnitId(), CStackBasicDescriptor(cre, 1), side, SlotID::WAR_MACHINES_SLOT, hex);
 		}
 	};
 
@@ -353,7 +344,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 			const BattleHex & pos = layout.units.at(side).at(k);
 
 			if (pos.isValid())
-				curB->generateNewStack(curB->nextUnitId(), *i->second, side, i->first, pos);
+				currentBattle->generateNewStack(currentBattle->nextUnitId(), *i->second, side, i->first, pos);
 		}
 	}
 
@@ -362,20 +353,20 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 	{
 		if (heroes[i] && heroes[i]->commander && heroes[i]->commander->alive)
 		{
-			curB->generateNewStack(curB->nextUnitId(), *heroes[i]->commander, i, SlotID::COMMANDER_SLOT_PLACEHOLDER, layout.commanders.at(i));
+			currentBattle->generateNewStack(currentBattle->nextUnitId(), *heroes[i]->commander, i, SlotID::COMMANDER_SLOT_PLACEHOLDER, layout.commanders.at(i));
 		}
 	}
 
-	if (curB->town)
+	if (currentBattle->town)
 	{
-		if (curB->town->fortificationsLevel().citadelHealth != 0)
-			curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_CENTRAL_TOWER);
+		if (currentBattle->town->fortificationsLevel().citadelHealth != 0)
+			currentBattle->generateNewStack(currentBattle->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_CENTRAL_TOWER);
 
-		if (curB->town->fortificationsLevel().upperTowerHealth != 0)
-			curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_UPPER_TOWER);
+		if (currentBattle->town->fortificationsLevel().upperTowerHealth != 0)
+			currentBattle->generateNewStack(currentBattle->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_UPPER_TOWER);
 
-		if (curB->town->fortificationsLevel().lowerTowerHealth != 0)
-			curB->generateNewStack(curB->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_BOTTOM_TOWER);
+		if (currentBattle->town->fortificationsLevel().lowerTowerHealth != 0)
+			currentBattle->generateNewStack(currentBattle->nextUnitId(), CStackBasicDescriptor(CreatureID::ARROW_TOWERS, 1), BattleSide::DEFENDER, SlotID::ARROW_TOWERS_SLOT, BattleHex::CASTLE_BOTTOM_TOWER);
 
 		//Moat generating is done on server
 	}
@@ -390,15 +381,15 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 
 	for(const std::shared_ptr<Bonus> & bonus : bgInfo->bonuses)
 	{
-		curB->addNewBonus(bonus);
+		currentBattle->addNewBonus(bonus);
 	}
 
 	//native terrain bonuses
 	auto nativeTerrain = std::make_shared<CreatureTerrainLimiter>();
 	
-	curB->addNewBonus(std::make_shared<Bonus>(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1,  BonusSourceID())->addLimiter(nativeTerrain));
-	curB->addNewBonus(std::make_shared<Bonus>(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID(), BonusSubtypeID(PrimarySkill::ATTACK))->addLimiter(nativeTerrain));
-	curB->addNewBonus(std::make_shared<Bonus>(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID(), BonusSubtypeID(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain));
+	currentBattle->addNewBonus(std::make_shared<Bonus>(BonusDuration::ONE_BATTLE, BonusType::STACKS_SPEED, BonusSource::TERRAIN_NATIVE, 1,  BonusSourceID())->addLimiter(nativeTerrain));
+	currentBattle->addNewBonus(std::make_shared<Bonus>(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID(), BonusSubtypeID(PrimarySkill::ATTACK))->addLimiter(nativeTerrain));
+	currentBattle->addNewBonus(std::make_shared<Bonus>(BonusDuration::ONE_BATTLE, BonusType::PRIMARY_SKILL, BonusSource::TERRAIN_NATIVE, 1, BonusSourceID(), BonusSubtypeID(PrimarySkill::DEFENSE))->addLimiter(nativeTerrain));
 	//////////////////////////////////////////////////////////////////////////
 
 	//tactics
@@ -428,21 +419,21 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 			logGlobal->warn("Double tactics is not implemented, only attacker will have tactics!");
 		if(tacticsSkillDiffAttacker > 0)
 		{
-			curB->tacticsSide = BattleSide::ATTACKER;
+			currentBattle->tacticsSide = BattleSide::ATTACKER;
 			//bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics
-			curB->tacticDistance = 1 + tacticsSkillDiffAttacker;
+			currentBattle->tacticDistance = 1 + tacticsSkillDiffAttacker;
 		}
 		else if(tacticsSkillDiffDefender > 0)
 		{
-			curB->tacticsSide = BattleSide::DEFENDER;
+			currentBattle->tacticsSide = BattleSide::DEFENDER;
 			//bonus specifies distance you can move beyond base row; this allows 100% compatibility with HMM3 mechanics
-			curB->tacticDistance = 1 + tacticsSkillDiffDefender;
+			currentBattle->tacticDistance = 1 + tacticsSkillDiffDefender;
 		}
 		else
-			curB->tacticDistance = 0;
+			currentBattle->tacticDistance = 0;
 	}
 
-	return curB;
+	return currentBattle;
 }
 
 const CGHeroInstance * BattleInfo::getHero(const PlayerColor & player) const
@@ -885,12 +876,12 @@ void BattleInfo::addOrUpdateUnitBonus(CStack * sta, const Bonus & value, bool fo
 	if(forceAdd || !sta->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, value.sid).And(Selector::typeSubtypeValueType(value.type, value.subtype, value.valType))))
 	{
 		//no such effect or cumulative - add new
-		logBonus->trace("%s receives a new bonus: %s", sta->nodeName(), value.Description());
+		logBonus->trace("%s receives a new bonus: %s", sta->nodeName(), value.Description(nullptr));
 		sta->addNewBonus(std::make_shared<Bonus>(value));
 	}
 	else
 	{
-		logBonus->trace("%s updated bonus: %s", sta->nodeName(), value.Description());
+		logBonus->trace("%s updated bonus: %s", sta->nodeName(), value.Description(nullptr));
 
 		for(const auto & stackBonus : sta->getExportedBonusList()) //TODO: optimize
 		{

+ 29 - 17
lib/battle/CBattleInfoCallback.cpp

@@ -1392,7 +1392,7 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(
 				at.friendlyCreaturePositions.insert(tile);
 		}
 	}
-	else if(attacker->hasBonusOfType(BonusType::TWO_HEX_ATTACK_BREATH))
+	else if(attacker->hasBonusOfType(BonusType::TWO_HEX_ATTACK_BREATH) || attacker->hasBonusOfType(BonusType::PRISM_HEX_ATTACK_BREATH))
 	{
 		auto direction = BattleHex::mutualPosition(attackOriginHex, destinationTile);
 		
@@ -1404,27 +1404,39 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(
 			direction = BattleHex::mutualPosition(attackOriginHex, defender->occupiedHex(defenderPos));
 		}
 
-		if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation
+		for(int i = 0; i < 3; i++)
 		{
-			BattleHex nextHex = destinationTile.cloneInDirection(direction, false);
-
-			if ( defender->doubleWide() )
+			if(direction != BattleHex::NONE) //only adjacent hexes are subject of dragon breath calculation
 			{
-				auto secondHex = destinationTile == defenderPos ? defender->occupiedHex(defenderPos) : defenderPos;
+				BattleHex nextHex = destinationTile.cloneInDirection(direction, false);
 
-				// if targeted double-wide creature is attacked from above or below ( -> second hex is also adjacent to attack origin)
-				// then dragon breath should target tile on the opposite side of targeted creature
-				if(BattleHex::mutualPosition(attackOriginHex, secondHex) != BattleHex::NONE)
-					nextHex = secondHex.cloneInDirection(direction, false);
-			}
+				if ( defender->doubleWide() )
+				{
+					auto secondHex = destinationTile == defenderPos ? defender->occupiedHex(defenderPos) : defenderPos;
 
-			if (nextHex.isValid())
-			{
-				//friendly stacks can also be damaged by Dragon Breath
-				const auto * st = battleGetUnitByPos(nextHex, true);
-				if(st != nullptr)
-					at.friendlyCreaturePositions.insert(nextHex);
+					// if targeted double-wide creature is attacked from above or below ( -> second hex is also adjacent to attack origin)
+					// then dragon breath should target tile on the opposite side of targeted creature
+					if(BattleHex::mutualPosition(attackOriginHex, secondHex) != BattleHex::NONE)
+						nextHex = secondHex.cloneInDirection(direction, false);
+				}
+
+				if (nextHex.isValid())
+				{
+					//friendly stacks can also be damaged by Dragon Breath
+					const auto * st = battleGetUnitByPos(nextHex, true);
+					if(st != nullptr)
+						at.friendlyCreaturePositions.insert(nextHex);
+				}
 			}
+
+			if(!attacker->hasBonusOfType(BonusType::PRISM_HEX_ATTACK_BREATH))
+				break;
+
+			// only needed for prism
+			int tmpDirection = static_cast<int>(direction) + 2;
+			if(tmpDirection > static_cast<int>(BattleHex::EDir::LEFT))
+				tmpDirection -= static_cast<int>(BattleHex::EDir::TOP);
+			direction = static_cast<BattleHex::EDir>(tmpDirection);
 		}
 	}
 	return at;

+ 8 - 1
lib/bonuses/Bonus.cpp

@@ -18,8 +18,11 @@
 #include "../CCreatureHandler.h"
 #include "../CCreatureSet.h"
 #include "../CSkillHandler.h"
+#include "../IGameCallback.h"
 #include "../TerrainHandler.h"
 #include "../VCMI_Lib.h"
+#include "../mapObjects/CGObjectInstance.h"
+#include "../mapObjectConstructors/CObjectClassesHandler.h"
 #include "../battle/BattleInfo.h"
 #include "../constants/StringConstants.h"
 #include "../entities/hero/CHero.h"
@@ -87,7 +90,7 @@ JsonNode CAddInfo::toJsonNode() const
 		return node;
 	}
 }
-std::string Bonus::Description(std::optional<si32> customValue) const
+std::string Bonus::Description(const IGameInfoCallback * cb, std::optional<si32> customValue) const
 {
 	MetaString descriptionHelper = description;
 	auto valueToShow = customValue.value_or(val);
@@ -112,6 +115,10 @@ std::string Bonus::Description(std::optional<si32> customValue) const
 			case BonusSource::HERO_SPECIAL:
 				descriptionHelper.appendTextID(sid.as<HeroTypeID>().toEntity(VLC)->getNameTextID());
 				break;
+			case BonusSource::OBJECT_INSTANCE:
+				const auto * object = cb->getObj(sid.as<ObjectInstanceID>());
+				if (object)
+					descriptionHelper.appendTextID(VLC->objtypeh->getObjectName(object->ID, object->subID));
 		}
 	}
 

+ 2 - 1
lib/bonuses/Bonus.h

@@ -26,6 +26,7 @@ class IPropagator;
 class IUpdater;
 class BonusList;
 class CSelector;
+class IGameInfoCallback;
 
 using BonusSubtypeID = VariantIdentifier<BonusCustomSubtype, SpellID, CreatureID, PrimarySkill, TerrainId, GameResID, SpellSchool>;
 using BonusSourceID = VariantIdentifier<BonusCustomSource, SpellID, CreatureID, ArtifactID, CampaignScenarioID, SecondarySkill, HeroTypeID, Obj, ObjectInstanceID, BuildingTypeUniqueID, BattleField>;
@@ -177,7 +178,7 @@ struct DLL_LINKAGE Bonus : public std::enable_shared_from_this<Bonus>, public Se
 		val += Val;
 	}
 
-	std::string Description(std::optional<si32> customValue = {}) const;
+	std::string Description(const IGameInfoCallback * cb, std::optional<si32> customValue = {}) const;
 	JsonNode toJsonNode() const;
 
 	std::shared_ptr<Bonus> addLimiter(const TLimiterPtr & Limiter); //returns this for convenient chain-calls

+ 1 - 0
lib/bonuses/BonusEnum.h

@@ -180,6 +180,7 @@ class JsonNode;
 	BONUS_NAME(RESOURCES_TOWN_MULTIPLYING_BOOST) /*Bonus that does not account for propagation and gives extra resources per day with amount multiplied by number of owned towns. val - base resource amount to be multiplied times number of owned towns, subtype - resource type*/ \
 	BONUS_NAME(DISINTEGRATE) /* after death no corpse remains */ \
 	BONUS_NAME(INVINCIBLE) /* cannot be target of attacks or spells */ \
+	BONUS_NAME(PRISM_HEX_ATTACK_BREATH) /*eg. dragons*/	\
 	/* end of list */
 
 

+ 3 - 3
lib/bonuses/CBonusSystemNode.cpp

@@ -378,7 +378,7 @@ void CBonusSystemNode::propagateBonus(const std::shared_ptr<Bonus> & b, const CB
 			? source.getUpdatedBonus(b, b->propagationUpdater)
 			: b;
 		bonuses.push_back(propagated);
-		logBonus->trace("#$# %s #propagated to# %s",  propagated->Description(), nodeName());
+		logBonus->trace("#$# %s #propagated to# %s",  propagated->Description(nullptr), nodeName());
 	}
 
 	TNodes lchildren;
@@ -392,9 +392,9 @@ void CBonusSystemNode::unpropagateBonus(const std::shared_ptr<Bonus> & b)
 	if(b->propagator->shouldBeAttached(this))
 	{
 		if (bonuses -= b)
-			logBonus->trace("#$# %s #is no longer propagated to# %s",  b->Description(), nodeName());
+			logBonus->trace("#$# %s #is no longer propagated to# %s",  b->Description(nullptr), nodeName());
 		else
-			logBonus->warn("Attempt to remove #$# %s, which is not propagated to %s", b->Description(), nodeName());
+			logBonus->warn("Attempt to remove #$# %s, which is not propagated to %s", b->Description(nullptr), nodeName());
 
 		bonuses.remove_if([b](const auto & bonus)
 		{

+ 3 - 0
lib/filesystem/CCompressedStream.cpp

@@ -136,6 +136,9 @@ si64 CCompressedStream::readMore(ui8 *data, si64 size)
 	{
 		if (inflateState->avail_in == 0)
 		{
+			if (gzipStream == nullptr)
+				throw std::runtime_error("Potentially truncated gzip file");
+
 			//inflate ran out of available data or was not initialized yet
 			// get new input data and update state accordingly
 			si64 availSize = gzipStream->read(compressedBuffer.data(), compressedBuffer.size());

+ 40 - 19
lib/json/JsonParser.cpp

@@ -158,40 +158,58 @@ bool JsonParser::extractEscaping(std::string & str)
 
 	switch(input[pos])
 	{
+		case '\r':
+			if(settings.mode == JsonParsingSettings::JsonFormatMode::JSON5 && input.size() > pos && input[pos+1] == '\n')
+			{
+				pos += 2;
+				return true;
+			}
+			break;
+		case '\n':
+			if(settings.mode == JsonParsingSettings::JsonFormatMode::JSON5)
+			{
+				pos += 1;
+				return true;
+			}
+			break;
 		case '\"':
 			str += '\"';
-			break;
+			pos++;
+			return true;
 		case '\\':
 			str += '\\';
-			break;
+			pos++;
+			return true;
 		case 'b':
 			str += '\b';
-			break;
+			pos++;
+			return true;
 		case 'f':
 			str += '\f';
-			break;
+			pos++;
+			return true;
 		case 'n':
 			str += '\n';
-			break;
+			pos++;
+			return true;
 		case 'r':
 			str += '\r';
-			break;
+			pos++;
+			return true;
 		case 't':
 			str += '\t';
-			break;
+			pos++;
+			return true;
 		case '/':
 			str += '/';
-			break;
-		default:
-			return error("Unknown escape sequence!", true);
+			pos++;
+			return true;
 	}
-	return true;
+	return error("Unknown escape sequence!", true);
 }
 
 bool JsonParser::extractString(std::string & str)
 {
-	//TODO: JSON5 - line breaks escaping
-
 	if(settings.mode < JsonParsingSettings::JsonFormatMode::JSON5)
 	{
 		if(input[pos] != '\"')
@@ -216,27 +234,30 @@ bool JsonParser::extractString(std::string & str)
 			pos++;
 			return true;
 		}
-		if(input[pos] == '\\') // Escaping
+		else if(input[pos] == '\\') // Escaping
 		{
 			str.append(&input[first], pos - first);
 			pos++;
 			if(pos == input.size())
 				break;
+
 			extractEscaping(str);
-			first = pos + 1;
+			first = pos;
 		}
-		if(input[pos] == '\n') // end-of-line
+		else if(input[pos] == '\n') // end-of-line
 		{
 			str.append(&input[first], pos - first);
 			return error("Closing quote not found!", true);
 		}
-		if(static_cast<unsigned char>(input[pos]) < ' ') // control character
+		else if(static_cast<unsigned char>(input[pos]) < ' ') // control character
 		{
 			str.append(&input[first], pos - first);
-			first = pos + 1;
+			pos++;
+			first = pos;
 			error("Illegal character in the string!", true);
 		}
-		pos++;
+		else
+			pos++;
 	}
 	return error("Unterminated string!");
 }

+ 9 - 5
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -23,17 +23,21 @@
 #include "../mapObjectConstructors/CRewardableConstructor.h"
 #include "../mapObjectConstructors/CommonConstructors.h"
 #include "../mapObjectConstructors/DwellingInstanceConstructor.h"
+#include "../mapObjectConstructors/FlaggableInstanceConstructor.h"
 #include "../mapObjectConstructors/HillFortInstanceConstructor.h"
 #include "../mapObjectConstructors/ShipyardInstanceConstructor.h"
+
 #include "../mapObjects/CGCreature.h"
+#include "../mapObjects/CGHeroInstance.h"
+#include "../mapObjects/CGMarket.h"
 #include "../mapObjects/CGPandoraBox.h"
+#include "../mapObjects/CGTownInstance.h"
 #include "../mapObjects/CQuest.h"
-#include "../mapObjects/ObjectTemplate.h"
-#include "../mapObjects/CGMarket.h"
+#include "../mapObjects/FlaggableMapObject.h"
 #include "../mapObjects/MiscObjects.h"
-#include "../mapObjects/CGHeroInstance.h"
-#include "../mapObjects/CGTownInstance.h"
+#include "../mapObjects/ObjectTemplate.h"
 #include "../mapObjects/ObstacleSetHandler.h"
+
 #include "../modding/IdentifierStorage.h"
 #include "../modding/CModHandler.h"
 #include "../modding/ModScope.h"
@@ -57,6 +61,7 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER_CLASS("town", CTownInstanceConstructor);
 	SET_HANDLER_CLASS("bank", CBankInstanceConstructor);
 	SET_HANDLER_CLASS("boat", BoatInstanceConstructor);
+	SET_HANDLER_CLASS("flaggable", FlaggableInstanceConstructor);
 	SET_HANDLER_CLASS("market", MarketInstanceConstructor);
 	SET_HANDLER_CLASS("hillFort", HillFortInstanceConstructor);
 	SET_HANDLER_CLASS("shipyard", ShipyardInstanceConstructor);
@@ -82,7 +87,6 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER("garrison", CGGarrison);
 	SET_HANDLER("heroPlaceholder", CGHeroPlaceholder);
 	SET_HANDLER("keymaster", CGKeymasterTent);
-	SET_HANDLER("lighthouse", CGLighthouse);
 	SET_HANDLER("magi", CGMagi);
 	SET_HANDLER("mine", CGMine);
 	SET_HANDLER("obelisk", CGObelisk);

+ 60 - 0
lib/mapObjectConstructors/FlaggableInstanceConstructor.cpp

@@ -0,0 +1,60 @@
+/*
+* FlaggableInstanceConstructor.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 "FlaggableInstanceConstructor.h"
+
+#include "../json/JsonBonus.h"
+#include "../texts/CGeneralTextHandler.h"
+#include "../VCMI_Lib.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+void FlaggableInstanceConstructor::initTypeData(const JsonNode & config)
+{
+	for (const auto & bonusJson : config["bonuses"].Struct())
+		providedBonuses.push_back(JsonUtils::parseBonus(bonusJson.second));
+
+	if (!config["message"].isNull())
+	{
+		std::string message = config["message"].String();
+		if (!message.empty() && message.at(0) == '@')
+		{
+			visitMessageTextID = message.substr(1);
+		}
+		else
+		{
+			visitMessageTextID = TextIdentifier(getBaseTextID(), "onVisit").get();
+			VLC->generaltexth->registerString( config.getModScope(), visitMessageTextID, config["message"]);
+		}
+	}
+
+	dailyIncome = ResourceSet(config["dailyIncome"]);
+}
+
+void FlaggableInstanceConstructor::initializeObject(FlaggableMapObject * flaggable) const
+{
+}
+
+const std::string & FlaggableInstanceConstructor::getVisitMessageTextID() const
+{
+	return visitMessageTextID;
+}
+
+const std::vector<std::shared_ptr<Bonus>> & FlaggableInstanceConstructor::getProvidedBonuses() const
+{
+	return providedBonuses;
+}
+
+const ResourceSet & FlaggableInstanceConstructor::getDailyIncome() const
+{
+	return dailyIncome;
+}
+
+VCMI_LIB_NAMESPACE_END

+ 41 - 0
lib/mapObjectConstructors/FlaggableInstanceConstructor.h

@@ -0,0 +1,41 @@
+/*
+* FlaggableInstanceConstructor.h, part of VCMI engine
+*
+* Authors: listed in file AUTHORS in main folder
+*
+* License: GNU General Public License v2.0 or later
+* Full text of license available in license.txt file, in main folder
+*
+*/
+#pragma once
+
+#include "CDefaultObjectTypeHandler.h"
+
+#include "../ResourceSet.h"
+#include "../bonuses/Bonus.h"
+#include "../mapObjects/FlaggableMapObject.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class FlaggableInstanceConstructor final : public CDefaultObjectTypeHandler<FlaggableMapObject>
+{
+	/// List of bonuses that are provided by every map object of this type
+	std::vector<std::shared_ptr<Bonus>> providedBonuses;
+
+	/// ID of message to show on hero visit
+	std::string visitMessageTextID;
+
+	/// Amount of resources granted by this object to owner every day
+	ResourceSet dailyIncome;
+
+protected:
+	void initTypeData(const JsonNode & config) override;
+	void initializeObject(FlaggableMapObject * object) const override;
+
+public:
+	const std::string & getVisitMessageTextID() const;
+	const std::vector<std::shared_ptr<Bonus>> & getProvidedBonuses() const;
+	const ResourceSet & getDailyIncome() const;
+};
+
+VCMI_LIB_NAMESPACE_END

+ 2 - 2
lib/mapObjects/CBank.cpp

@@ -144,7 +144,7 @@ void CBank::onHeroVisit(const CGHeroInstance * h) const
 	bd.player = h->getOwner();
 	bd.text.appendLocalString(EMetaText::ADVOB_TXT, 32);
 	bd.components = getPopupComponents(h->getOwner());
-	bd.text.replaceRawString(getObjectName());
+	bd.text.replaceTextID(getObjectHandler()->getNameTextID());
 	cb->showBlockingDialog(this, &bd);
 }
 
@@ -158,7 +158,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 	if (!bankConfig)
 	{
 		iw.text.appendRawString(VLC->generaltexth->advobtxt[33]);// This was X, now is completely empty
-		iw.text.replaceRawString(getObjectName());
+		iw.text.replaceTextID(getObjectHandler()->getNameTextID());
 		cb->showInfoDialog(&iw);
 	}
 

+ 2 - 2
lib/mapObjects/CGTownInstance.cpp

@@ -166,7 +166,7 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const
 		const auto growth = b->val * (base + castleBonus) / 100;
 		if (growth)
 		{
-			ret.entries.emplace_back(growth, b->Description(growth));
+			ret.entries.emplace_back(growth, b->Description(cb, growth));
 		}
 	}
 
@@ -174,7 +174,7 @@ GrowthInfo CGTownInstance::getGrowthInfo(int level) const
 	// Note: bonus uses 1-based levels (Pikeman is level 1), town list uses 0-based (Pikeman in 0-th creatures entry)
 	TConstBonusListPtr bonuses = getBonuses(Selector::typeSubtype(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(level+1)));
 	for(const auto & b : *bonuses)
-		ret.entries.emplace_back(b->val, b->Description());
+		ret.entries.emplace_back(b->val, b->Description(cb));
 
 	int dwellingBonus = 0;
 	if(const PlayerState *p = cb->getPlayerState(tempOwner, false))

+ 105 - 0
lib/mapObjects/FlaggableMapObject.cpp

@@ -0,0 +1,105 @@
+/*
+ * FlaggableMapObject.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 "FlaggableMapObject.h"
+
+#include "../IGameCallback.h"
+#include "CGHeroInstance.h"
+#include "../networkPacks/PacksForClient.h"
+#include "../mapObjectConstructors/FlaggableInstanceConstructor.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+const IOwnableObject * FlaggableMapObject::asOwnable() const
+{
+	return this;
+}
+
+ResourceSet FlaggableMapObject::dailyIncome() const
+{
+	return getFlaggableHandler()->getDailyIncome();
+}
+
+std::vector<CreatureID> FlaggableMapObject::providedCreatures() const
+{
+	return {};
+}
+
+void FlaggableMapObject::onHeroVisit( const CGHeroInstance * h ) const
+{
+	if (cb->getPlayerRelations(h->getOwner(), getOwner()) != PlayerRelations::ENEMIES)
+		return; // H3 behavior - revisiting owned Lighthouse is a no-op
+
+	if (getOwner().isValidPlayer())
+		takeBonusFrom(getOwner());
+
+	cb->setOwner(this, h->getOwner()); //not ours? flag it!
+
+	InfoWindow iw;
+	iw.player = h->getOwner();
+	iw.text.appendTextID(getFlaggableHandler()->getVisitMessageTextID());
+	cb->showInfoDialog(&iw);
+
+	giveBonusTo(h->getOwner());
+}
+
+void FlaggableMapObject::initObj(vstd::RNG & rand)
+{
+	if(getOwner().isValidPlayer())
+	{
+		// FIXME: This is dirty hack
+		giveBonusTo(getOwner(), true);
+	}
+}
+
+std::shared_ptr<FlaggableInstanceConstructor> FlaggableMapObject::getFlaggableHandler() const
+{
+	return std::dynamic_pointer_cast<FlaggableInstanceConstructor>(getObjectHandler());
+}
+
+void FlaggableMapObject::giveBonusTo(const PlayerColor & player, bool onInit) const
+{
+	for (auto const & bonus : getFlaggableHandler()->getProvidedBonuses())
+	{
+		GiveBonus gb(GiveBonus::ETarget::PLAYER);
+		gb.id = player;
+		gb.bonus = *bonus;
+
+		// FIXME: better place for this code?
+		gb.bonus.duration = BonusDuration::PERMANENT;
+		gb.bonus.source = BonusSource::OBJECT_INSTANCE;
+		gb.bonus.sid = BonusSourceID(id);
+
+		// FIXME: This is really dirty hack
+		// Proper fix would be to make FlaggableMapObject into bonus system node
+		// Unfortunately this will cause saves breakage
+		if(onInit)
+			gb.applyGs(cb->gameState());
+		else
+			cb->sendAndApply(gb);
+	}
+}
+
+void FlaggableMapObject::takeBonusFrom(const PlayerColor & player) const
+{
+	RemoveBonus rb(GiveBonus::ETarget::PLAYER);
+	rb.whoID = player;
+	rb.source = BonusSource::OBJECT_INSTANCE;
+	rb.id = BonusSourceID(id);
+	cb->sendAndApply(rb);
+}
+
+void FlaggableMapObject::serializeJsonOptions(JsonSerializeFormat& handler)
+{
+	serializeJsonOwner(handler);
+}
+
+VCMI_LIB_NAMESPACE_END

+ 41 - 0
lib/mapObjects/FlaggableMapObject.h

@@ -0,0 +1,41 @@
+/*
+ * FlaggableMapObject.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "CGObjectInstance.h"
+#include "IOwnableObject.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct Bonus;
+class FlaggableInstanceConstructor;
+
+class DLL_LINKAGE FlaggableMapObject : public CGObjectInstance, public IOwnableObject
+{
+	std::shared_ptr<FlaggableInstanceConstructor> getFlaggableHandler() const;
+
+	void giveBonusTo(const PlayerColor & player, bool onInit = false) const;
+	void takeBonusFrom(const PlayerColor & player) const;
+
+public:
+	using CGObjectInstance::CGObjectInstance;
+
+	void onHeroVisit(const CGHeroInstance * h) const override;
+	void initObj(vstd::RNG & rand) override;
+
+	const IOwnableObject * asOwnable() const final;
+	ResourceSet dailyIncome() const override;
+	std::vector<CreatureID> providedCreatures() const override;
+
+protected:
+	void serializeJsonOptions(JsonSerializeFormat & handler) override;
+};
+
+VCMI_LIB_NAMESPACE_END

+ 0 - 69
lib/mapObjects/MiscObjects.cpp

@@ -1311,75 +1311,6 @@ void CGObelisk::setPropertyDer(ObjProperty what, ObjPropertyID identifier)
 	}
 }
 
-const IOwnableObject * CGLighthouse::asOwnable() const
-{
-	return this;
-}
-
-ResourceSet CGLighthouse::dailyIncome() const
-{
-	return {};
-}
-
-std::vector<CreatureID> CGLighthouse::providedCreatures() const
-{
-	return {};
-}
-
-void CGLighthouse::onHeroVisit( const CGHeroInstance * h ) const
-{
-	if(h->tempOwner != tempOwner)
-	{
-		PlayerColor oldOwner = tempOwner;
-		cb->setOwner(this,h->tempOwner); //not ours? flag it!
-		h->showInfoDialog(69);
-		giveBonusTo(h->tempOwner);
-
-		if(oldOwner.isValidPlayer()) //remove bonus from old owner
-		{
-			RemoveBonus rb(GiveBonus::ETarget::PLAYER);
-			rb.whoID = oldOwner;
-			rb.source = BonusSource::OBJECT_INSTANCE;
-			rb.id = BonusSourceID(id);
-			cb->sendAndApply(rb);
-		}
-	}
-}
-
-void CGLighthouse::initObj(vstd::RNG & rand)
-{
-	if(tempOwner.isValidPlayer())
-	{
-		// FIXME: This is dirty hack
-		giveBonusTo(tempOwner, true);
-	}
-}
-
-void CGLighthouse::giveBonusTo(const PlayerColor & player, bool onInit) const
-{
-	GiveBonus gb(GiveBonus::ETarget::PLAYER);
-	gb.bonus.type = BonusType::MOVEMENT;
-	gb.bonus.val = 500;
-	gb.id = player;
-	gb.bonus.duration = BonusDuration::PERMANENT;
-	gb.bonus.source = BonusSource::OBJECT_INSTANCE;
-	gb.bonus.sid = BonusSourceID(id);
-	gb.bonus.subtype = BonusCustomSubtype::heroMovementSea;
-
-	// FIXME: This is really dirty hack
-	// Proper fix would be to make CGLighthouse into bonus system node
-	// Unfortunately this will cause saves breakage
-	if(onInit)
-		gb.applyGs(cb->gameState());
-	else
-		cb->sendAndApply(gb);
-}
-
-void CGLighthouse::serializeJsonOptions(JsonSerializeFormat& handler)
-{
-	serializeJsonOwner(handler);
-}
-
 void HillFort::onHeroVisit(const CGHeroInstance * h) const
 {
 	cb->showObjectWindow(this, EOpenWindowMode::HILL_FORT_WINDOW, h, false);

+ 0 - 22
lib/mapObjects/MiscObjects.h

@@ -413,28 +413,6 @@ protected:
 	void setPropertyDer(ObjProperty what, ObjPropertyID identifier) override;
 };
 
-class DLL_LINKAGE CGLighthouse : public CGObjectInstance, public IOwnableObject
-{
-public:
-	using CGObjectInstance::CGObjectInstance;
-
-	void onHeroVisit(const CGHeroInstance * h) const override;
-	void initObj(vstd::RNG & rand) override;
-
-	const IOwnableObject * asOwnable() const final;
-	ResourceSet dailyIncome() const override;
-	std::vector<CreatureID> providedCreatures() const override;
-
-	template <typename Handler> void serialize(Handler &h)
-	{
-		h & static_cast<CGObjectInstance&>(*this);
-	}
-	void giveBonusTo(const PlayerColor & player, bool onInit = false) const;
-
-protected:
-	void serializeJsonOptions(JsonSerializeFormat & handler) override;
-};
-
 class DLL_LINKAGE CGTerrainPatch : public CGObjectInstance
 {
 public:

+ 6 - 3
lib/mapping/MapFormatH3M.cpp

@@ -208,6 +208,9 @@ void CMapLoaderH3M::readHeader()
 
 	// optimization - load mappings only once to avoid slow parsing of map headers for map list
 	static const std::map<EMapFormat, MapIdentifiersH3M> identifierMappers = generateMappings();
+	if (!identifierMappers.count(mapHeader->version))
+		throw std::runtime_error("Unsupported map format! Format ID " + std::to_string(static_cast<int>(mapHeader->version)));
+
 	const MapIdentifiersH3M & identifierMapper = identifierMappers.at(mapHeader->version);
 
 	reader->setIdentifierRemapper(identifierMapper);
@@ -1478,9 +1481,9 @@ CGObjectInstance * CMapLoaderH3M::readShipyard(const int3 & mapPosition, std::sh
 	return object;
 }
 
-CGObjectInstance * CMapLoaderH3M::readLighthouse(const int3 & mapPosition)
+CGObjectInstance * CMapLoaderH3M::readLighthouse(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate)
 {
-	auto * object = new CGLighthouse(map->cb);
+	auto * object = readGeneric(mapPosition, objectTemplate);
 	setOwnerAndValidate(mapPosition, object, reader->readPlayer32());
 	return object;
 }
@@ -1618,7 +1621,7 @@ CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr<const ObjectTemplat
 			return readPyramid(mapPosition, objectTemplate);
 
 		case Obj::LIGHTHOUSE:
-			return readLighthouse(mapPosition);
+			return readLighthouse(mapPosition, objectTemplate);
 
 		case Obj::CREATURE_BANK:
 		case Obj::DERELICT_SHIP:

+ 1 - 1
lib/mapping/MapFormatH3M.h

@@ -208,7 +208,7 @@ private:
 	CGObjectInstance * readPyramid(const int3 & position, std::shared_ptr<const ObjectTemplate> objTempl);
 	CGObjectInstance * readQuestGuard(const int3 & position);
 	CGObjectInstance * readShipyard(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate);
-	CGObjectInstance * readLighthouse(const int3 & mapPosition);
+	CGObjectInstance * readLighthouse(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate);
 	CGObjectInstance * readGeneric(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate);
 	CGObjectInstance * readBank(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate);
 

+ 0 - 2
lib/mapping/MapFormatJson.cpp

@@ -1014,8 +1014,6 @@ void CMapLoaderJson::readTerrain()
 		const JsonNode underground = getFromArchive(TERRAIN_FILE_NAMES[1]);
 		readTerrainLevel(underground, 1);
 	}
-
-	map->calculateWaterContent();
 }
 
 CMapLoaderJson::MapObjectLoader::MapObjectLoader(CMapLoaderJson * _owner, JsonMap::value_type & json):

+ 5 - 3
lib/mapping/MapReaderH3M.cpp

@@ -410,9 +410,11 @@ bool MapReaderH3M::readBool()
 int8_t MapReaderH3M::readInt8Checked(int8_t lowerLimit, int8_t upperLimit)
 {
 	int8_t result = readInt8();
-	assert(result >= lowerLimit);
-	assert(result <= upperLimit);
-	return std::clamp(result, lowerLimit, upperLimit);
+	int8_t resultClamped = std::clamp(result, lowerLimit, upperLimit);
+	if (result != resultClamped)
+		logGlobal->warn("Map contains out of range value %d! Expected %d-%d", static_cast<int>(result), static_cast<int>(lowerLimit), static_cast<int>(upperLimit));
+
+	return resultClamped;
 }
 
 uint8_t MapReaderH3M::readUInt8()

+ 2 - 2
lib/modding/CModHandler.cpp

@@ -446,8 +446,8 @@ void CModHandler::loadTranslation(const TModID & modName)
 	JsonNode baseTranslation = JsonUtils::assembleFromFiles(mod.config["translations"]);
 	JsonNode extraTranslation = JsonUtils::assembleFromFiles(mod.config[preferredLanguage]["translations"]);
 
-	VLC->generaltexth->loadTranslationOverrides(modName, baseTranslation);
-	VLC->generaltexth->loadTranslationOverrides(modName, extraTranslation);
+	VLC->generaltexth->loadTranslationOverrides(modName, modBaseLanguage, baseTranslation);
+	VLC->generaltexth->loadTranslationOverrides(modName, preferredLanguage, extraTranslation);
 }
 
 void CModHandler::load()

+ 33 - 19
lib/rmg/CMapGenerator.cpp

@@ -152,41 +152,55 @@ std::unique_ptr<CMap> CMapGenerator::generate()
 	return std::move(map->mapInstance);
 }
 
-std::string CMapGenerator::getMapDescription() const
+MetaString CMapGenerator::getMapDescription() const
 {
-	assert(map);
+	const TextIdentifier mainPattern("vcmi", "randomMap", "description");
+	const TextIdentifier isHuman("vcmi", "randomMap", "description", "isHuman");
+	const TextIdentifier townChoiceIs("vcmi", "randomMap", "description", "townChoice");
+	const std::array waterContent = {
+		TextIdentifier("vcmi", "randomMap", "description", "water", "none"),
+		TextIdentifier("vcmi", "randomMap", "description", "water", "normal"),
+		TextIdentifier("vcmi", "randomMap", "description", "water", "islands")
+	};
+	const std::array monsterStrength = {
+		TextIdentifier("vcmi", "randomMap", "description", "monster", "weak"),
+		TextIdentifier("vcmi", "randomMap", "description", "monster", "normal"),
+		TextIdentifier("vcmi", "randomMap", "description", "monster", "strong")
+	};
 
-	const std::string waterContentStr[3] = { "none", "normal", "islands" };
-	const std::string monsterStrengthStr[3] = { "weak", "normal", "strong" };
-
-	int monsterStrengthIndex = mapGenOptions.getMonsterStrength() - EMonsterStrength::GLOBAL_WEAK; //does not start from 0
 	const auto * mapTemplate = mapGenOptions.getMapTemplate();
+	int monsterStrengthIndex = mapGenOptions.getMonsterStrength() - EMonsterStrength::GLOBAL_WEAK; //does not start from 0
 
-	if(!mapTemplate)
-		throw rmgException("Map template for Random Map Generator is not found. Could not start the game.");
+	MetaString result = MetaString::createFromTextID(mainPattern.get());
 
-    std::stringstream ss;
-    ss << boost::str(boost::format(std::string("Map created by the Random Map Generator.\nTemplate was %s, size %dx%d") +
-        ", levels %d, players %d, computers %d, water %s, monster %s, VCMI map") % mapTemplate->getName() %
-		map->width() % map->height() % static_cast<int>(map->levels()) % static_cast<int>(mapGenOptions.getHumanOrCpuPlayerCount()) %
-		static_cast<int>(mapGenOptions.getCompOnlyPlayerCount()) % waterContentStr[mapGenOptions.getWaterContent()] %
-		monsterStrengthStr[monsterStrengthIndex]);
+	result.replaceRawString(mapTemplate->getName());
+	result.replaceNumber(map->width());
+	result.replaceNumber(map->height());
+	result.replaceNumber(map->levels());
+	result.replaceNumber(mapGenOptions.getHumanOrCpuPlayerCount());
+	result.replaceNumber(mapGenOptions.getCompOnlyPlayerCount());
+	result.replaceTextID(waterContent.at(mapGenOptions.getWaterContent()).get());
+	result.replaceTextID(monsterStrength.at(monsterStrengthIndex).get());
 
 	for(const auto & pair : mapGenOptions.getPlayersSettings())
 	{
 		const auto & pSettings = pair.second;
+
 		if(pSettings.getPlayerType() == EPlayerType::HUMAN)
 		{
-			ss << ", " << GameConstants::PLAYER_COLOR_NAMES[pSettings.getColor().getNum()] << " is human";
+			result.appendTextID(isHuman.get());
+			result.replaceName(pSettings.getColor());
 		}
+
 		if(pSettings.getStartingTown() != FactionID::RANDOM)
 		{
-			ss << ", " << GameConstants::PLAYER_COLOR_NAMES[pSettings.getColor().getNum()]
-			   << " town choice is " << (*VLC->townh)[pSettings.getStartingTown()]->getNameTranslated();
+			result.appendTextID(townChoiceIs.get());
+			result.replaceName(pSettings.getColor());
+			result.replaceName(pSettings.getStartingTown());
 		}
 	}
 
-	return ss.str();
+	return result;
 }
 
 void CMapGenerator::addPlayerInfo()
@@ -451,7 +465,7 @@ void CMapGenerator::addHeaderInfo()
 	m.height = mapGenOptions.getHeight();
 	m.twoLevel = mapGenOptions.getHasTwoLevels();
 	m.name.appendLocalString(EMetaText::GENERAL_TXT, 740);
-	m.description.appendRawString(getMapDescription());
+	m.description = getMapDescription();
 	m.difficulty = EMapDifficulty::NORMAL;
 	addPlayerInfo();
 	m.waterMap = (mapGenOptions.getWaterContent() != EWaterContent::EWaterContent::NONE);

+ 2 - 4
lib/rmg/CMapGenerator.h

@@ -10,14 +10,12 @@
 
 #pragma once
 
-#include "../GameConstants.h"
 #include "CMapGenOptions.h"
-#include "../int3.h"
-#include "CRmgTemplate.h"
 #include "../LoadProgress.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+class MetaString;
 class CRmgTemplate;
 class CMapGenOptions;
 class JsonNode;
@@ -93,7 +91,7 @@ private:
 	/// Generation methods
 	void loadConfig();
 	
-	std::string getMapDescription() const;
+	MetaString getMapDescription() const;
 
 	void initPrisonsRemaining();
 	void initQuestArtsRemaining();

+ 4 - 1
lib/serializer/RegisterTypes.h

@@ -20,14 +20,17 @@
 #include "../gameState/CGameState.h"
 #include "../gameState/CGameStateCampaign.h"
 #include "../gameState/TavernHeroesPool.h"
+
 #include "../mapObjects/CGCreature.h"
 #include "../mapObjects/CGDwelling.h"
 #include "../mapObjects/CGMarket.h"
 #include "../mapObjects/CGPandoraBox.h"
 #include "../mapObjects/CGTownInstance.h"
 #include "../mapObjects/CQuest.h"
+#include "../mapObjects/FlaggableMapObject.h"
 #include "../mapObjects/MiscObjects.h"
 #include "../mapObjects/TownBuildingInstance.h"
+
 #include "../mapping/CMap.h"
 #include "../networkPacks/PacksForClient.h"
 #include "../networkPacks/PacksForClientBattle.h"
@@ -73,7 +76,7 @@ void registerTypes(Serializer &s)
 	s.template registerType<CGSirens>(15);
 	s.template registerType<CGShipyard>(16);
 	s.template registerType<CGDenOfthieves>(17);
-	s.template registerType<CGLighthouse>(18);
+	s.template registerType<FlaggableMapObject>(18);
 	s.template registerType<CGTerrainPatch>(19);
 	s.template registerType<HillFort>(20);
 	s.template registerType<CGMarket>(21);

+ 1 - 12
lib/texts/CGeneralTextHandler.cpp

@@ -139,9 +139,7 @@ CGeneralTextHandler::CGeneralTextHandler():
 	// pseudo-array, that don't have H3 file with same name
 	seerEmpty        (*this, "core.seerhut.empty"  ),
 	seerNames        (*this, "core.seerhut.names"  ),
-	capColors        (*this, "vcmi.capitalColors"  ),
-	znpc00           (*this, "vcmi.znpc00"  ), // technically - wog
-	qeModCommands    (*this, "vcmi.quickExchange" )
+	capColors        (*this, "vcmi.capitalColors"  )
 {
 	readToVector("core.vcdesc",   "DATA/VCDESC.TXT"   );
 	readToVector("core.lcdesc",   "DATA/LCDESC.TXT"   );
@@ -166,10 +164,6 @@ CGeneralTextHandler::CGeneralTextHandler():
 	readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" );
 	readToVector("core.xtrainfo", "DATA/XTRAINFO.TXT" );
 
-	static const std::string QE_MOD_COMMANDS = "DATA/QECOMMANDS.TXT";
-	if (CResourceHandler::get()->existsResource(TextPath::builtin(QE_MOD_COMMANDS)))
-		readToVector("vcmi.quickExchange", QE_MOD_COMMANDS);
-
 	{
 		CLegacyConfigParser parser(TextPath::builtin("DATA/RANDTVRN.TXT"));
 		parser.endLine();
@@ -298,11 +292,6 @@ CGeneralTextHandler::CGeneralTextHandler():
 			scenariosCountPerCampaign.push_back(region);
 		}
 	}
-	if (VLC->engineSettings()->getBoolean(EGameSettings::MODULE_COMMANDERS))
-	{
-		if(CResourceHandler::get()->existsResource(TextPath::builtin("DATA/ZNPC00.TXT")))
-			readToVector("vcmi.znpc00", "DATA/ZNPC00.TXT" );
-	}
 }
 
 int32_t CGeneralTextHandler::pluralText(const int32_t textIndex, const int32_t count) const

+ 0 - 4
lib/texts/CGeneralTextHandler.h

@@ -62,8 +62,6 @@ public:
 	LegacyTextContainer fcommands; // fort screen
 	LegacyTextContainer tavernInfo;
 
-	LegacyTextContainer qeModCommands;
-
 	LegacyHelpContainer zelp;
 
 	//objects
@@ -75,8 +73,6 @@ public:
 
 	//sec skills
 	LegacyTextContainer levels;
-	//commanders
-	LegacyTextContainer znpc00; //more or less useful content of that file
 
 	std::vector<std::string> findStringsWithPrefix(const std::string & prefix);
 

+ 6 - 0
lib/texts/MetaString.cpp

@@ -13,6 +13,7 @@
 #include "CArtHandler.h"
 #include "CCreatureHandler.h"
 #include "CCreatureSet.h"
+#include "entities/faction/CFaction.h"
 #include "texts/CGeneralTextHandler.h"
 #include "CSkillHandler.h"
 #include "GameConstants.h"
@@ -387,6 +388,11 @@ void MetaString::replaceName(const ArtifactID & id)
 	replaceTextID(id.toEntity(VLC)->getNameTextID());
 }
 
+void MetaString::replaceName(const FactionID & id)
+{
+	replaceTextID(id.toEntity(VLC)->getNameTextID());
+}
+
 void MetaString::replaceName(const MapObjectID& id)
 {
 	replaceTextID(VLC->objtypeh->getObjectName(id, 0));

+ 2 - 0
lib/texts/MetaString.h

@@ -21,6 +21,7 @@ class MapObjectSubID;
 class PlayerColor;
 class SecondarySkill;
 class SpellID;
+class FactionID;
 class GameResID;
 using TQuantity = si32;
 
@@ -97,6 +98,7 @@ public:
 	void replacePositiveNumber(int64_t txt);
 
 	void replaceName(const ArtifactID & id);
+	void replaceName(const FactionID& id);
 	void replaceName(const MapObjectID& id);
 	void replaceName(const PlayerColor& id);
 	void replaceName(const SecondarySkill& id);

+ 13 - 5
lib/texts/TextLocalizationContainer.cpp

@@ -22,7 +22,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 
 std::recursive_mutex TextLocalizationContainer::globalTextMutex;
 
-void TextLocalizationContainer::registerStringOverride(const std::string & modContext, const TextIdentifier & UID, const std::string & localized)
+void TextLocalizationContainer::registerStringOverride(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language)
 {
 	std::lock_guard globalLock(globalTextMutex);
 
@@ -42,6 +42,11 @@ void TextLocalizationContainer::registerStringOverride(const std::string & modCo
 			entry.identifierModContext = modContext;
 			entry.baseStringModContext = modContext;
 		}
+		else
+		{
+			if (language == VLC->generaltexth->getPreferredLanguage())
+				entry.overriden = true;
+		}
 	}
 	else
 	{
@@ -127,10 +132,10 @@ void TextLocalizationContainer::registerString(const std::string & identifierMod
 	}
 }
 
-void TextLocalizationContainer::loadTranslationOverrides(const std::string & modContext, const JsonNode & config)
+void TextLocalizationContainer::loadTranslationOverrides(const std::string & modContext, const std::string & language, const JsonNode & config)
 {
 	for(const auto & node : config.Struct())
-		registerStringOverride(modContext, node.first, node.second.String());
+		registerStringOverride(modContext, node.first, node.second.String(), language);
 }
 
 bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) const
@@ -140,15 +145,18 @@ bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) con
 	return stringsLocalizations.count(UID.get());
 }
 
-void TextLocalizationContainer::exportAllTexts(std::map<std::string, std::map<std::string, std::string>> & storage) const
+void TextLocalizationContainer::exportAllTexts(std::map<std::string, std::map<std::string, std::string>> & storage, bool onlyMissing) const
 {
 	std::lock_guard globalLock(globalTextMutex);
 
 	for (auto const & subContainer : subContainers)
-		subContainer->exportAllTexts(storage);
+		subContainer->exportAllTexts(storage, onlyMissing);
 
 	for (auto const & entry : stringsLocalizations)
 	{
+		if (onlyMissing && entry.second.overriden)
+			continue;
+
 		std::string textToWrite;
 		std::string modName = entry.second.baseStringModContext;
 

+ 5 - 3
lib/texts/TextLocalizationContainer.h

@@ -32,6 +32,8 @@ protected:
 		/// Different from identifierModContext if mod has modified object from another mod (e.g. rebalance mods)
 		std::string baseStringModContext;
 
+		bool overriden = false;
+
 		template <typename Handler>
 		void serialize(Handler & h)
 		{
@@ -47,7 +49,7 @@ protected:
 	std::vector<const TextLocalizationContainer *> subContainers;
 
 	/// add selected string to internal storage as high-priority strings
-	void registerStringOverride(const std::string & modContext, const TextIdentifier & UID, const std::string & localized);
+	void registerStringOverride(const std::string & modContext, const TextIdentifier & UID, const std::string & localized, const std::string & language);
 
 	std::string getModLanguage(const std::string & modContext);
 
@@ -57,7 +59,7 @@ protected:
 public:
 	/// Loads translation from provided json
 	/// Any entries loaded by this will have priority over texts registered normally
-	void loadTranslationOverrides(const std::string & modContext, JsonNode const & file);
+	void loadTranslationOverrides(const std::string & modContext, const std::string & language, JsonNode const & file);
 
 	/// add selected string to internal storage
 	void registerString(const std::string & modContext, const TextIdentifier & UID, const JsonNode & localized);
@@ -77,7 +79,7 @@ public:
 
 	/// Debug method, returns all currently stored texts
 	/// Format: [mod ID][string ID] -> human-readable text
-	void exportAllTexts(std::map<std::string, std::map<std::string, std::string>> & storage) const;
+	void exportAllTexts(std::map<std::string, std::map<std::string, std::string>> & storage, bool onlyMissing) const;
 
 	/// Add or override subcontainer which can store identifiers
 	void addSubContainer(const TextLocalizationContainer & container);

+ 0 - 1
mapeditor/StdInc.h

@@ -11,7 +11,6 @@
 
 #include "../Global.h"
 
-#define VCMI_EDITOR_VERSION "0.2"
 #define VCMI_EDITOR_NAME "VCMI Map Editor"
 
 #include <QtWidgets>

+ 11 - 9
mapeditor/inspector/inspector.cpp

@@ -60,7 +60,7 @@ Initializer::Initializer(CGObjectInstance * o, const PlayerColor & pl) : default
 	INIT_OBJ_TYPE(CGHeroPlaceholder);
 	INIT_OBJ_TYPE(CGHeroInstance);
 	INIT_OBJ_TYPE(CGSignBottle);
-	INIT_OBJ_TYPE(CGLighthouse);
+	INIT_OBJ_TYPE(FlaggableMapObject);
 	//INIT_OBJ_TYPE(CRewardableObject);
 	//INIT_OBJ_TYPE(CGPandoraBox);
 	//INIT_OBJ_TYPE(CGEvent);
@@ -108,7 +108,7 @@ void Initializer::initialize(CGShipyard * o)
 	o->tempOwner = defaultPlayer;
 }
 
-void Initializer::initialize(CGLighthouse * o)
+void Initializer::initialize(FlaggableMapObject * o)
 {
 	if(!o) return;
 	
@@ -172,10 +172,12 @@ void Initializer::initialize(CGTownInstance * o)
 	if(lvl > 2) o->addBuilding(BuildingID::CASTLE);
 	if(lvl > 3) o->addBuilding(BuildingID::CAPITOL);
 
-	for(auto const & spell : VLC->spellh->objects) //add all regular spells to town
+	if(o->possibleSpells.empty())
 	{
-		if(!spell->isSpecial() && !spell->isCreatureAbility())
-			o->possibleSpells.push_back(spell->id);
+		for(auto const & spellId : VLC->spellh->getDefaultAllowed()) //add all regular spells to town
+		{
+			o->possibleSpells.push_back(spellId);
+		}
 	}
 }
 
@@ -244,7 +246,7 @@ void Inspector::updateProperties(CGDwelling * o)
 	}
 }
 
-void Inspector::updateProperties(CGLighthouse * o)
+void Inspector::updateProperties(FlaggableMapObject * o)
 {
 	if(!o) return;
 
@@ -492,7 +494,7 @@ void Inspector::updateProperties()
 	UPDATE_OBJ_PROPERTIES(CGHeroPlaceholder);
 	UPDATE_OBJ_PROPERTIES(CGHeroInstance);
 	UPDATE_OBJ_PROPERTIES(CGSignBottle);
-	UPDATE_OBJ_PROPERTIES(CGLighthouse);
+	UPDATE_OBJ_PROPERTIES(FlaggableMapObject);
 	UPDATE_OBJ_PROPERTIES(CRewardableObject);
 	UPDATE_OBJ_PROPERTIES(CGPandoraBox);
 	UPDATE_OBJ_PROPERTIES(CGEvent);
@@ -540,7 +542,7 @@ void Inspector::setProperty(const QString & key, const QVariant & value)
 	SET_PROPERTIES(CGHeroInstance);
 	SET_PROPERTIES(CGShipyard);
 	SET_PROPERTIES(CGSignBottle);
-	SET_PROPERTIES(CGLighthouse);
+	SET_PROPERTIES(FlaggableMapObject);
 	SET_PROPERTIES(CRewardableObject);
 	SET_PROPERTIES(CGPandoraBox);
 	SET_PROPERTIES(CGEvent);
@@ -553,7 +555,7 @@ void Inspector::setProperty(CArmedInstance * o, const QString & key, const QVari
 	if(!o) return;
 }
 
-void Inspector::setProperty(CGLighthouse * o, const QString & key, const QVariant & value)
+void Inspector::setProperty(FlaggableMapObject * o, const QString & key, const QVariant & value)
 {
 	if(!o) return;
 }

+ 3 - 2
mapeditor/inspector/inspector.h

@@ -17,6 +17,7 @@
 #include "../lib/GameConstants.h"
 #include "../lib/mapObjects/CGCreature.h"
 #include "../lib/mapObjects/MapObjects.h"
+#include "../lib/mapObjects/FlaggableMapObject.h"
 #include "../lib/mapObjects/CRewardableObject.h"
 #include "../lib/texts/CGeneralTextHandler.h"
 #include "../lib/ResourceSet.h"
@@ -48,7 +49,7 @@ public:
 	DECLARE_OBJ_TYPE(CGHeroInstance);
 	DECLARE_OBJ_TYPE(CGCreature);
 	DECLARE_OBJ_TYPE(CGSignBottle);
-	DECLARE_OBJ_TYPE(CGLighthouse);
+	DECLARE_OBJ_TYPE(FlaggableMapObject);
 	//DECLARE_OBJ_TYPE(CRewardableObject);
 	//DECLARE_OBJ_TYPE(CGEvent);
 	//DECLARE_OBJ_TYPE(CGPandoraBox);
@@ -78,7 +79,7 @@ protected:
 	DECLARE_OBJ_PROPERTY_METHODS(CGHeroInstance);
 	DECLARE_OBJ_PROPERTY_METHODS(CGCreature);
 	DECLARE_OBJ_PROPERTY_METHODS(CGSignBottle);
-	DECLARE_OBJ_PROPERTY_METHODS(CGLighthouse);
+	DECLARE_OBJ_PROPERTY_METHODS(FlaggableMapObject);
 	DECLARE_OBJ_PROPERTY_METHODS(CRewardableObject);
 	DECLARE_OBJ_PROPERTY_METHODS(CGPandoraBox);
 	DECLARE_OBJ_PROPERTY_METHODS(CGEvent);

+ 2 - 1
mapeditor/mainwindow.cpp

@@ -182,6 +182,7 @@ MainWindow::MainWindow(QWidget* parent) :
 	console = new CConsoleHandler();
 	logConfig = new CBasicLogConfigurator(logPath, console);
 	logConfig->configureDefault();
+	logGlobal->info("Starting map editor of '%s'", GameConstants::VCMI_VERSION);
 	logGlobal->info("The log file will be saved to %s", logPath);
 
 	//init
@@ -317,7 +318,7 @@ void MainWindow::setStatusMessage(const QString & status)
 
 void MainWindow::setTitle()
 {
-	QString title = QString("%1%2 - %3 (v%4)").arg(filename, unsaved ? "*" : "", VCMI_EDITOR_NAME, VCMI_EDITOR_VERSION);
+	QString title = QString("%1%2 - %3 (%4)").arg(filename, unsaved ? "*" : "", VCMI_EDITOR_NAME, GameConstants::VCMI_VERSION.c_str());
 	setWindowTitle(title);
 }
 

+ 4 - 3
mapeditor/mapcontroller.cpp

@@ -121,7 +121,7 @@ void MapController::repairMap(CMap * map) const
 			   dynamic_cast<CGTownInstance*>(obj.get()) ||
 			   dynamic_cast<CGGarrison*>(obj.get()) ||
 			   dynamic_cast<CGShipyard*>(obj.get()) ||
-			   dynamic_cast<CGLighthouse*>(obj.get()) ||
+			   dynamic_cast<FlaggableMapObject*>(obj.get()) ||
 			   dynamic_cast<CGHeroInstance*>(obj.get()))
 				obj->tempOwner = PlayerColor::NEUTRAL;
 		}
@@ -369,6 +369,7 @@ void MapController::pasteFromClipboard(int level)
 		if (!canPlaceObject(level, obj, errorMsg))
 		{
 			errors.push_back(std::move(errorMsg));
+			continue;
 		}
 		auto newPos = objUniquePtr->pos + shift;
 		if(_map->isInTheMap(newPos))
@@ -380,8 +381,8 @@ void MapController::pasteFromClipboard(int level)
 		_scenes[level]->selectionObjectsView.selectObject(obj);
 		_mapHandler->invalidate(obj);
 	}
-
-	QMessageBox::warning(main, QObject::tr("Can't place object"), errors.join('\n'));
+	if(!errors.isEmpty())
+		QMessageBox::warning(main, QObject::tr("Can't place object"), errors.join('\n'));
 	
 	_scenes[level]->objectsView.draw();
 	_scenes[level]->passabilityView.update();

+ 15 - 5
server/battles/BattleProcessor.cpp

@@ -28,10 +28,12 @@
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/mapping/CMap.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/modding/IdentifierStorage.h"
 #include "../../lib/networkPacks/PacksForClient.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
 #include "../../lib/CPlayerState.h"
+#include <vstd/RNG.h>
 
 BattleProcessor::BattleProcessor(CGameHandler * gameHandler)
 	: gameHandler(gameHandler)
@@ -156,16 +158,24 @@ BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArray<const CArmedIns
 {
 	const auto & t = *gameHandler->getTile(tile);
 	TerrainId terrain = t.terType->getId();
-	if (gameHandler->gameState()->map->isCoastalTile(tile)) //coastal tile is always ground
+	if (town)
+		terrain = town->getNativeTerrain();
+	else if (gameHandler->gameState()->map->isCoastalTile(tile)) //coastal tile is always ground
 		terrain = ETerrainId::SAND;
 
-	BattleField terType = gameHandler->gameState()->battleGetBattlefieldType(tile, gameHandler->getRandomGenerator());
-	if (heroes[BattleSide::ATTACKER] && heroes[BattleSide::ATTACKER]->boat && heroes[BattleSide::DEFENDER] && heroes[BattleSide::DEFENDER]->boat)
-		terType = BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield.ship_to_ship"));
+	BattleField battlefieldType = gameHandler->gameState()->battleGetBattlefieldType(tile, gameHandler->getRandomGenerator());
+
+	if (town)
+	{
+		const TerrainType* terrainData = VLC->terrainTypeHandler->getById(terrain);
+		battlefieldType = BattleField(*RandomGeneratorUtil::nextItem(terrainData->battleFields, gameHandler->getRandomGenerator()));
+	}
+	else if (heroes[BattleSide::ATTACKER] && heroes[BattleSide::ATTACKER]->boat && heroes[BattleSide::DEFENDER] && heroes[BattleSide::DEFENDER]->boat)
+		battlefieldType = BattleField(*VLC->identifiers()->getIdentifier("core", "battlefield.ship_to_ship"));
 
 	//send info about battles
 	BattleStart bs;
-	bs.info = BattleInfo::setupBattle(tile, terrain, terType, armies, heroes, layout, town);
+	bs.info = BattleInfo::setupBattle(tile, terrain, battlefieldType, armies, heroes, layout, town);
 	bs.battleID = gameHandler->gameState()->nextBattleID;
 
 	engageIntoBattle(bs.info->getSide(BattleSide::ATTACKER).color);