浏览代码

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

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

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

@@ -42,6 +42,12 @@
 	"vcmi.heroOverview.secondarySkills" : "Secondary Skills",
 	"vcmi.heroOverview.secondarySkills" : "Secondary Skills",
 	"vcmi.heroOverview.spells" : "Spells",
 	"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.mergeSameUnit" : "Merge same creatures",
 	"vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures",
 	"vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures",
 	"vcmi.radialWheel.splitSingleUnit" : "Split off single creature",
 	"vcmi.radialWheel.splitSingleUnit" : "Split off single creature",
@@ -60,6 +66,16 @@
 	"vcmi.radialWheel.moveUp" : "Move up",
 	"vcmi.radialWheel.moveUp" : "Move up",
 	"vcmi.radialWheel.moveDown" : "Move down",
 	"vcmi.radialWheel.moveDown" : "Move down",
 	"vcmi.radialWheel.moveBottom" : "Move to bottom",
 	"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...",
 	"vcmi.spellBook.search" : "search...",
 
 
@@ -687,5 +703,7 @@
 	"core.bonus.DISINTEGRATE.name": "Disintegrate",
 	"core.bonus.DISINTEGRATE.name": "Disintegrate",
 	"core.bonus.DISINTEGRATE.description": "No corpse remains after death",
 	"core.bonus.DISINTEGRATE.description": "No corpse remains after death",
 	"core.bonus.INVINCIBLE.name": "Invincible",
 	"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.name" : "Широкий подих",
 	"core.bonus.WIDE_BREATH.description" : "Атака широким подихом",
 	"core.bonus.WIDE_BREATH.description" : "Атака широким подихом",
 	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Обмежена дальність стрільби",
 	"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" ],
 	"factions" : [ "config/vcmi/towerFactions" ],
 	"creatures" : [ "config/vcmi/towerCreature" ],
 	"creatures" : [ "config/vcmi/towerCreature" ],
+	"spells" : [ "config/vcmi/spells" ],
 
 
 	"translations" : [
 	"translations" : [
 		"config/vcmi/english.json"
 		"config/vcmi/english.json"

+ 18 - 8
client/ClientCommandManager.cpp

@@ -185,12 +185,12 @@ void ClientCommandManager::handleRedrawCommand()
 	GH.windows().totalRedraw();
 	GH.windows().totalRedraw();
 }
 }
 
 
-void ClientCommandManager::handleTranslateGameCommand()
+void ClientCommandManager::handleTranslateGameCommand(bool onlyMissing)
 {
 {
 	std::map<std::string, std::map<std::string, std::string>> textsByMod;
 	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);
 	boost::filesystem::create_directories(outPath);
 
 
 	for(const auto & modEntry : textsByMod)
 	for(const auto & modEntry : textsByMod)
@@ -254,13 +254,20 @@ void ClientCommandManager::handleTranslateMapsCommand()
 	logGlobal->info("Loading campaigns for export");
 	logGlobal->info("Loading campaigns for export");
 	for (auto const & campaignName : campaignList)
 	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;
 	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";
 	const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "translation";
 	boost::filesystem::create_directories(outPath);
 	boost::filesystem::create_directories(outPath);
@@ -591,7 +598,10 @@ void ClientCommandManager::processCommand(const std::string & message, bool call
 		handleRedrawCommand();
 		handleRedrawCommand();
 
 
 	else if(message=="translate" || message=="translate game")
 	else if(message=="translate" || message=="translate game")
-		handleTranslateGameCommand();
+		handleTranslateGameCommand(false);
+
+	else if(message=="translate missing")
+		handleTranslateGameCommand(true);
 
 
 	else if(message=="translate maps")
 	else if(message=="translate maps")
 		handleTranslateMapsCommand();
 		handleTranslateMapsCommand();

+ 1 - 1
client/ClientCommandManager.h

@@ -46,7 +46,7 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a
 	void handleRedrawCommand();
 	void handleRedrawCommand();
 
 
 	// Extracts all translateable game texts into Translation directory, separating files on per-mod basis
 	// 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
 	// Extracts all translateable texts from maps and campaigns into Translation directory, separating files on per-mod basis
 	void handleTranslateMapsCommand();
 	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))
 	else if(morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_MORALE))
 	{
 	{
 		auto noMorale = node->getBonusBearer()->getBonus(Selector::type()(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;
 		component.value = 0;
 	}
 	}
 	else if (!morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_LUCK))
 	else if (!morale && node && node->getBonusBearer()->hasBonusOfType(BonusType::NO_LUCK))
 	{
 	{
 		auto noLuck = node->getBonusBearer()->getBonus(Selector::type()(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;
 		component.value = 0;
 	}
 	}
 	else
 	else
@@ -596,7 +596,7 @@ void MoraleLuckBox::set(const AFactionMember * node)
 		for(auto & bonus : * modifierList)
 		for(auto & bonus : * modifierList)
 		{
 		{
 			if(bonus->val) {
 			if(bonus->val) {
-				const std::string& description = bonus->Description();
+				const std::string& description = bonus->Description(LOCPLINT->cb.get());
 				//arraytxt already contains \n
 				//arraytxt already contains \n
 				if (description.size() && description[0] != '\n')
 				if (description.size() && description[0] != '\n')
 					addInfo += '\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
 	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)
 	for(int index = ECommander::ATTACK; index <= ECommander::SPELL_POWER; ++index)
@@ -905,14 +905,30 @@ std::string CStackWindow::generateStackExpDescription()
 	return expText;
 	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)
 void CStackWindow::setSelection(si32 newSkill, std::shared_ptr<CCommanderSkillIcon> newIcon)
 {
 {
 	auto getSkillDescription = [this](int skillIndex, bool selected) -> std::string
 	auto getSkillDescription = [this](int skillIndex, bool selected) -> std::string
 	{
 	{
 		if(selected)
 		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
 		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)
 	auto getSkillImage = [this](int skillIndex)

+ 1 - 0
client/windows/CCreatureWindow.h

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

+ 46 - 12
client/windows/CExchangeWindow.cpp

@@ -192,18 +192,52 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 
 
 	if(qeLayout)
 	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); });
 			[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); });
 			[this](){ this->backpackShortcut(false); });
+
 		backpackButtonLeft->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
 		backpackButtonLeft->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/backpackButtonIcon")));
 		backpackButtonRight->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>(
 				std::make_shared<CButton>(
 					Point(484 + 35 * i, 154),
 					Point(484 + 35 * i, 154),
 					AnimationPath::builtin("quick-exchange/unitLeft.DEF"),
 					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))));
 					std::bind(&CExchangeController::moveStack, &controller, false, SlotID(i))));
 			moveUnitFromRightToLeftButtons.back()->block(leftHeroBlock);
 			moveUnitFromRightToLeftButtons.back()->block(leftHeroBlock);
 
 
@@ -235,7 +269,7 @@ CExchangeWindow::CExchangeWindow(ObjectInstanceID hero1, ObjectInstanceID hero2,
 				std::make_shared<CButton>(
 				std::make_shared<CButton>(
 					Point(66 + 35 * i, 154),
 					Point(66 + 35 * i, 154),
 					AnimationPath::builtin("quick-exchange/unitRight.DEF"),
 					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))));
 					std::bind(&CExchangeController::moveStack, &controller, true, SlotID(i))));
 			moveUnitFromLeftToRightButtons.back()->block(rightHeroBlock);
 			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)
 		if(object->ID == Obj::MINE || object->ID == Obj::ABANDONED_MINE)
 		{
 		{
 			const CGMine * mine = dynamic_cast<const CGMine *>(object);
 			const CGMine * mine = dynamic_cast<const CGMine *>(object);
-			assert(mine);
 			minesCount[mine->producedResource]++;
 			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
 	//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_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;
 	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":
 	"THREE_HEADED_ATTACK":
 	{
 	{
 		"graphics":
 		"graphics":

+ 1 - 0
config/gameConfig.json

@@ -53,6 +53,7 @@
 		"config/objects/creatureBanks.json",
 		"config/objects/creatureBanks.json",
 		"config/objects/dwellings.json",
 		"config/objects/dwellings.json",
 		"config/objects/generic.json",
 		"config/objects/generic.json",
+		"config/objects/lighthouse.json",
 		"config/objects/magicSpring.json",
 		"config/objects/magicSpring.json",
 		"config/objects/magicWell.json",
 		"config/objects/magicWell.json",
 		"config/objects/moddables.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" : {
 	"obelisk" : {
 		"index" :57,
 		"index" :57,
 		"handler" : "obelisk",
 		"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
 						"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" : [
 				"onEmpty" : [
 					{
 					{
 						"limiter" : {
 						"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
 						"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" : [
 				"onEmpty" : [
 					{
 					{
 						"limiter" : {
 						"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
 						"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" : [
 				"onEmpty" : [
 					{
 					{
 						"limiter" : {
 						"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",
 		"targetType" : "CREATURE",
 		"type": "combat",
 		"type": "combat",
-		"name": "Land Mine",
+		"name": "",
 		"school":
 		"school":
 		{
 		{
 			"air": false,
 			"air": false,
@@ -237,7 +237,7 @@
 	"fireWallTrigger" : {
 	"fireWallTrigger" : {
 		"targetType" : "CREATURE",
 		"targetType" : "CREATURE",
 		"type": "combat",
 		"type": "combat",
-		"name": "Fire Wall",
+		"name": "",
 		"school":
 		"school":
 		{
 		{
 			"air": false,
 			"air": false,

+ 7 - 7
config/spells/vcmiAbility.json

@@ -1,8 +1,8 @@
 {
 {
-    "summonDemons" : {
+	"summonDemons" : {
 		"type": "ability",
 		"type": "ability",
 		"targetType" : "CREATURE",
 		"targetType" : "CREATURE",
-		"name": "Summon Demons",
+		"name": "",
 		"school" : {},
 		"school" : {},
 		"level": 2,
 		"level": 2,
 		"power": 50,
 		"power": 50,
@@ -46,11 +46,11 @@
 				"bonus.GARGOYLE" : "absolute"
 				"bonus.GARGOYLE" : "absolute"
 			}
 			}
 		}
 		}
-    },
-    "firstAid" : {
+	},
+	"firstAid" : {
 		"targetType" : "CREATURE",
 		"targetType" : "CREATURE",
 		"type": "ability",
 		"type": "ability",
-		"name": "First Aid",
+		"name": "",
 		"school" : {},
 		"school" : {},
 		"level": 1,
 		"level": 1,
 		"power": 10,
 		"power": 10,
@@ -106,7 +106,7 @@
 	"catapultShot" : {
 	"catapultShot" : {
 		"targetType" : "LOCATION",
 		"targetType" : "LOCATION",
 		"type": "ability",
 		"type": "ability",
-		"name": "Catapult shot",
+		"name": "",
 		"school" : {},
 		"school" : {},
 		"level": 1,
 		"level": 1,
 		"power": 1,
 		"power": 1,
@@ -187,7 +187,7 @@
 	"cyclopsShot" : {
 	"cyclopsShot" : {
 		"targetType" : "LOCATION",
 		"targetType" : "LOCATION",
 		"type": "ability",
 		"type": "ability",
-		"name": "Siege shot",
+		"name": "",
 		"school" : {},
 		"school" : {},
 		"level": 1,
 		"level": 1,
 		"power": 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
 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
 ### RETURN_AFTER_STRIKE
 
 
 Affected unit can return to his starting location after attack (Harpies)
 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
 - `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
 - `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
 - `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
 - `hillFort` - TODO: documentation. See config files in vcmi installation for reference
 - `shipyard` - 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
 - `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
 - `generic` - Defines empty object type that provides no functionality. Note that unlike `static`, objects of this type are never used by RMG
 - `borderGate`
 - `borderGate`
 - `borderGuard`
 - `borderGuard`
-- `lighthouse`
 - `magi`
 - `magi`
 - `mine`
 - `mine`
 - `obelisk`
 - `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
 #### Extract commands
 `translate` - save game texts into json files
 `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
 `translate maps` - save map and campaign texts into json files
 `get config` - save game objects data 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)
 `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 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
 ### Translating mod information
 In order to display information in Launcher in language selected by user add following block into your `mod.json`:
 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)
 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"];
 	Settings node = settings.write["video"]["resolution"]["scaling"];
 	node->Integer() = checked ? 0 : 100;
 	node->Integer() = checked ? 0 : 100;
 }
 }

+ 4 - 0
lib/CMakeLists.txt

@@ -116,6 +116,7 @@ set(lib_MAIN_SRCS
 	mapObjectConstructors/CommonConstructors.cpp
 	mapObjectConstructors/CommonConstructors.cpp
 	mapObjectConstructors/CRewardableConstructor.cpp
 	mapObjectConstructors/CRewardableConstructor.cpp
 	mapObjectConstructors/DwellingInstanceConstructor.cpp
 	mapObjectConstructors/DwellingInstanceConstructor.cpp
+	mapObjectConstructors/FlaggableInstanceConstructor.cpp
 	mapObjectConstructors/HillFortInstanceConstructor.cpp
 	mapObjectConstructors/HillFortInstanceConstructor.cpp
 	mapObjectConstructors/ShipyardInstanceConstructor.cpp
 	mapObjectConstructors/ShipyardInstanceConstructor.cpp
 
 
@@ -132,6 +133,7 @@ set(lib_MAIN_SRCS
 	mapObjects/CObjectHandler.cpp
 	mapObjects/CObjectHandler.cpp
 	mapObjects/CQuest.cpp
 	mapObjects/CQuest.cpp
 	mapObjects/CRewardableObject.cpp
 	mapObjects/CRewardableObject.cpp
+	mapObjects/FlaggableMapObject.cpp
 	mapObjects/IMarket.cpp
 	mapObjects/IMarket.cpp
 	mapObjects/IObjectInterface.cpp
 	mapObjects/IObjectInterface.cpp
 	mapObjects/MiscObjects.cpp
 	mapObjects/MiscObjects.cpp
@@ -497,6 +499,7 @@ set(lib_MAIN_HEADERS
 	mapObjectConstructors/CRewardableConstructor.h
 	mapObjectConstructors/CRewardableConstructor.h
 	mapObjectConstructors/DwellingInstanceConstructor.h
 	mapObjectConstructors/DwellingInstanceConstructor.h
 	mapObjectConstructors/HillFortInstanceConstructor.h
 	mapObjectConstructors/HillFortInstanceConstructor.h
+	mapObjectConstructors/FlaggableInstanceConstructor.h
 	mapObjectConstructors/IObjectInfo.h
 	mapObjectConstructors/IObjectInfo.h
 	mapObjectConstructors/RandomMapInfo.h
 	mapObjectConstructors/RandomMapInfo.h
 	mapObjectConstructors/ShipyardInstanceConstructor.h
 	mapObjectConstructors/ShipyardInstanceConstructor.h
@@ -515,6 +518,7 @@ set(lib_MAIN_HEADERS
 	mapObjects/CObjectHandler.h
 	mapObjects/CObjectHandler.h
 	mapObjects/CQuest.h
 	mapObjects/CQuest.h
 	mapObjects/CRewardableObject.h
 	mapObjects/CRewardableObject.h
+	mapObjects/FlaggableMapObject.h
 	mapObjects/IMarket.h
 	mapObjects/IMarket.h
 	mapObjects/IObjectInterface.h
 	mapObjects/IObjectInterface.h
 	mapObjects/IOwnableObject.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)
 DLL_LINKAGE std::ostream & operator<<(std::ostream & out, const CSkill::LevelInfo & info)
 {
 {
 	for(int i=0; i < info.effects.size(); i++)
 	for(int i=0; i < info.effects.size(); i++)
-		out << (i ? "," : "") << info.effects[i]->Description();
+		out << (i ? "," : "") << info.effects[i]->Description(nullptr);
 	return out << "])";
 	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)
 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;
 	CMP_stack cmpst;
-	auto * curB = new BattleInfo(layout);
+	auto * currentBattle = new BattleInfo(layout);
 
 
 	for(auto i : { BattleSide::LEFT_SIDE, BattleSide::RIGHT_SIDE})
 	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
 	//setting up siege obstacles
 	if (town && town->fortificationsLevel().wallsHealth != 0)
 	if (town && town->fortificationsLevel().wallsHealth != 0)
 	{
 	{
 		auto fortification = town->fortificationsLevel();
 		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})
 		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)
 		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)
 		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)
 		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
 	//randomize obstacles
-	if (layout.obstaclesAllowed && !town)
+	if (layout.obstaclesAllowed && (!town || !town->hasFort()))
  	{
  	{
 		RandGen r{};
 		RandGen r{};
 		auto ourRand = [&](){ return r.rand(); };
 		auto ourRand = [&](){ return r.rand(); };
@@ -221,12 +212,12 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 		auto appropriateAbsoluteObstacle = [&](int id)
 		auto appropriateAbsoluteObstacle = [&](int id)
 		{
 		{
 			const auto * info = Obstacle(id).getInfo();
 			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)
 		auto appropriateUsualObstacle = [&](int id)
 		{
 		{
 			const auto * info = Obstacle(id).getInfo();
 			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
 		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>();
 				auto obstPtr = std::make_shared<CObstacleInstance>();
 				obstPtr->obstacleType = CObstacleInstance::ABSOLUTE_OBSTACLE;
 				obstPtr->obstacleType = CObstacleInstance::ABSOLUTE_OBSTACLE;
 				obstPtr->ID = obidgen.getSuchNumber(appropriateAbsoluteObstacle);
 				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())
 				for(BattleHex blocked : obstPtr->getBlockedTiles())
 					blockedTiles.push_back(blocked);
 					blockedTiles.push_back(blocked);
@@ -256,7 +247,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 			while(tilesToBlock > 0)
 			while(tilesToBlock > 0)
 			{
 			{
 				RangeGenerator obidgen(0, VLC->obstacleHandler->size() - 1, ourRand);
 				RangeGenerator obidgen(0, VLC->obstacleHandler->size() - 1, ourRand);
-				auto tileAccessibility = curB->getAccessibility();
+				auto tileAccessibility = currentBattle->getAccessibility();
 				const int obid = obidgen.getSuchNumber(appropriateUsualObstacle);
 				const int obid = obidgen.getSuchNumber(appropriateUsualObstacle);
 				const ObstacleInfo &obi = *Obstacle(obid).getInfo();
 				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>();
 				auto obstPtr = std::make_shared<CObstacleInstance>();
 				obstPtr->ID = obid;
 				obstPtr->ID = obid;
 				obstPtr->pos = posgenerator.getSuchNumber(validPosition);
 				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())
 				for(BattleHex blocked : obstPtr->getBlockedTiles())
 					blockedTiles.push_back(blocked);
 					blockedTiles.push_back(blocked);
@@ -315,7 +306,7 @@ BattleInfo * BattleInfo::setupBattle(const int3 & tile, TerrainId terrain, const
 			CreatureID cre = warMachineArt->artType->getWarMachine();
 			CreatureID cre = warMachineArt->artType->getWarMachine();
 
 
 			if(cre != CreatureID::NONE)
 			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);
 			const BattleHex & pos = layout.units.at(side).at(k);
 
 
 			if (pos.isValid())
 			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)
 		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
 		//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)
 	for(const std::shared_ptr<Bonus> & bonus : bgInfo->bonuses)
 	{
 	{
-		curB->addNewBonus(bonus);
+		currentBattle->addNewBonus(bonus);
 	}
 	}
 
 
 	//native terrain bonuses
 	//native terrain bonuses
 	auto nativeTerrain = std::make_shared<CreatureTerrainLimiter>();
 	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
 	//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!");
 			logGlobal->warn("Double tactics is not implemented, only attacker will have tactics!");
 		if(tacticsSkillDiffAttacker > 0)
 		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
 			//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)
 		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
 			//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
 		else
-			curB->tacticDistance = 0;
+			currentBattle->tacticDistance = 0;
 	}
 	}
 
 
-	return curB;
+	return currentBattle;
 }
 }
 
 
 const CGHeroInstance * BattleInfo::getHero(const PlayerColor & player) const
 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))))
 	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
 		//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));
 		sta->addNewBonus(std::make_shared<Bonus>(value));
 	}
 	}
 	else
 	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
 		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);
 				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);
 		auto direction = BattleHex::mutualPosition(attackOriginHex, destinationTile);
 		
 		
@@ -1404,27 +1404,39 @@ AttackableTiles CBattleInfoCallback::getPotentiallyAttackableHexes(
 			direction = BattleHex::mutualPosition(attackOriginHex, defender->occupiedHex(defenderPos));
 			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;
 	return at;

+ 8 - 1
lib/bonuses/Bonus.cpp

@@ -18,8 +18,11 @@
 #include "../CCreatureHandler.h"
 #include "../CCreatureHandler.h"
 #include "../CCreatureSet.h"
 #include "../CCreatureSet.h"
 #include "../CSkillHandler.h"
 #include "../CSkillHandler.h"
+#include "../IGameCallback.h"
 #include "../TerrainHandler.h"
 #include "../TerrainHandler.h"
 #include "../VCMI_Lib.h"
 #include "../VCMI_Lib.h"
+#include "../mapObjects/CGObjectInstance.h"
+#include "../mapObjectConstructors/CObjectClassesHandler.h"
 #include "../battle/BattleInfo.h"
 #include "../battle/BattleInfo.h"
 #include "../constants/StringConstants.h"
 #include "../constants/StringConstants.h"
 #include "../entities/hero/CHero.h"
 #include "../entities/hero/CHero.h"
@@ -87,7 +90,7 @@ JsonNode CAddInfo::toJsonNode() const
 		return node;
 		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;
 	MetaString descriptionHelper = description;
 	auto valueToShow = customValue.value_or(val);
 	auto valueToShow = customValue.value_or(val);
@@ -112,6 +115,10 @@ std::string Bonus::Description(std::optional<si32> customValue) const
 			case BonusSource::HERO_SPECIAL:
 			case BonusSource::HERO_SPECIAL:
 				descriptionHelper.appendTextID(sid.as<HeroTypeID>().toEntity(VLC)->getNameTextID());
 				descriptionHelper.appendTextID(sid.as<HeroTypeID>().toEntity(VLC)->getNameTextID());
 				break;
 				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 IUpdater;
 class BonusList;
 class BonusList;
 class CSelector;
 class CSelector;
+class IGameInfoCallback;
 
 
 using BonusSubtypeID = VariantIdentifier<BonusCustomSubtype, SpellID, CreatureID, PrimarySkill, TerrainId, GameResID, SpellSchool>;
 using BonusSubtypeID = VariantIdentifier<BonusCustomSubtype, SpellID, CreatureID, PrimarySkill, TerrainId, GameResID, SpellSchool>;
 using BonusSourceID = VariantIdentifier<BonusCustomSource, SpellID, CreatureID, ArtifactID, CampaignScenarioID, SecondarySkill, HeroTypeID, Obj, ObjectInstanceID, BuildingTypeUniqueID, BattleField>;
 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;
 		val += Val;
 	}
 	}
 
 
-	std::string Description(std::optional<si32> customValue = {}) const;
+	std::string Description(const IGameInfoCallback * cb, std::optional<si32> customValue = {}) const;
 	JsonNode toJsonNode() const;
 	JsonNode toJsonNode() const;
 
 
 	std::shared_ptr<Bonus> addLimiter(const TLimiterPtr & Limiter); //returns this for convenient chain-calls
 	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(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(DISINTEGRATE) /* after death no corpse remains */ \
 	BONUS_NAME(INVINCIBLE) /* cannot be target of attacks or spells */ \
 	BONUS_NAME(INVINCIBLE) /* cannot be target of attacks or spells */ \
+	BONUS_NAME(PRISM_HEX_ATTACK_BREATH) /*eg. dragons*/	\
 	/* end of list */
 	/* 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)
 			? source.getUpdatedBonus(b, b->propagationUpdater)
 			: b;
 			: b;
 		bonuses.push_back(propagated);
 		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;
 	TNodes lchildren;
@@ -392,9 +392,9 @@ void CBonusSystemNode::unpropagateBonus(const std::shared_ptr<Bonus> & b)
 	if(b->propagator->shouldBeAttached(this))
 	if(b->propagator->shouldBeAttached(this))
 	{
 	{
 		if (bonuses -= b)
 		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
 		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)
 		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 (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
 			//inflate ran out of available data or was not initialized yet
 			// get new input data and update state accordingly
 			// get new input data and update state accordingly
 			si64 availSize = gzipStream->read(compressedBuffer.data(), compressedBuffer.size());
 			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])
 	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 '\"':
 		case '\"':
 			str += '\"';
 			str += '\"';
-			break;
+			pos++;
+			return true;
 		case '\\':
 		case '\\':
 			str += '\\';
 			str += '\\';
-			break;
+			pos++;
+			return true;
 		case 'b':
 		case 'b':
 			str += '\b';
 			str += '\b';
-			break;
+			pos++;
+			return true;
 		case 'f':
 		case 'f':
 			str += '\f';
 			str += '\f';
-			break;
+			pos++;
+			return true;
 		case 'n':
 		case 'n':
 			str += '\n';
 			str += '\n';
-			break;
+			pos++;
+			return true;
 		case 'r':
 		case 'r':
 			str += '\r';
 			str += '\r';
-			break;
+			pos++;
+			return true;
 		case 't':
 		case 't':
 			str += '\t';
 			str += '\t';
-			break;
+			pos++;
+			return true;
 		case '/':
 		case '/':
 			str += '/';
 			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)
 bool JsonParser::extractString(std::string & str)
 {
 {
-	//TODO: JSON5 - line breaks escaping
-
 	if(settings.mode < JsonParsingSettings::JsonFormatMode::JSON5)
 	if(settings.mode < JsonParsingSettings::JsonFormatMode::JSON5)
 	{
 	{
 		if(input[pos] != '\"')
 		if(input[pos] != '\"')
@@ -216,27 +234,30 @@ bool JsonParser::extractString(std::string & str)
 			pos++;
 			pos++;
 			return true;
 			return true;
 		}
 		}
-		if(input[pos] == '\\') // Escaping
+		else if(input[pos] == '\\') // Escaping
 		{
 		{
 			str.append(&input[first], pos - first);
 			str.append(&input[first], pos - first);
 			pos++;
 			pos++;
 			if(pos == input.size())
 			if(pos == input.size())
 				break;
 				break;
+
 			extractEscaping(str);
 			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);
 			str.append(&input[first], pos - first);
 			return error("Closing quote not found!", true);
 			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);
 			str.append(&input[first], pos - first);
-			first = pos + 1;
+			pos++;
+			first = pos;
 			error("Illegal character in the string!", true);
 			error("Illegal character in the string!", true);
 		}
 		}
-		pos++;
+		else
+			pos++;
 	}
 	}
 	return error("Unterminated string!");
 	return error("Unterminated string!");
 }
 }

+ 9 - 5
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -23,17 +23,21 @@
 #include "../mapObjectConstructors/CRewardableConstructor.h"
 #include "../mapObjectConstructors/CRewardableConstructor.h"
 #include "../mapObjectConstructors/CommonConstructors.h"
 #include "../mapObjectConstructors/CommonConstructors.h"
 #include "../mapObjectConstructors/DwellingInstanceConstructor.h"
 #include "../mapObjectConstructors/DwellingInstanceConstructor.h"
+#include "../mapObjectConstructors/FlaggableInstanceConstructor.h"
 #include "../mapObjectConstructors/HillFortInstanceConstructor.h"
 #include "../mapObjectConstructors/HillFortInstanceConstructor.h"
 #include "../mapObjectConstructors/ShipyardInstanceConstructor.h"
 #include "../mapObjectConstructors/ShipyardInstanceConstructor.h"
+
 #include "../mapObjects/CGCreature.h"
 #include "../mapObjects/CGCreature.h"
+#include "../mapObjects/CGHeroInstance.h"
+#include "../mapObjects/CGMarket.h"
 #include "../mapObjects/CGPandoraBox.h"
 #include "../mapObjects/CGPandoraBox.h"
+#include "../mapObjects/CGTownInstance.h"
 #include "../mapObjects/CQuest.h"
 #include "../mapObjects/CQuest.h"
-#include "../mapObjects/ObjectTemplate.h"
-#include "../mapObjects/CGMarket.h"
+#include "../mapObjects/FlaggableMapObject.h"
 #include "../mapObjects/MiscObjects.h"
 #include "../mapObjects/MiscObjects.h"
-#include "../mapObjects/CGHeroInstance.h"
-#include "../mapObjects/CGTownInstance.h"
+#include "../mapObjects/ObjectTemplate.h"
 #include "../mapObjects/ObstacleSetHandler.h"
 #include "../mapObjects/ObstacleSetHandler.h"
+
 #include "../modding/IdentifierStorage.h"
 #include "../modding/IdentifierStorage.h"
 #include "../modding/CModHandler.h"
 #include "../modding/CModHandler.h"
 #include "../modding/ModScope.h"
 #include "../modding/ModScope.h"
@@ -57,6 +61,7 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER_CLASS("town", CTownInstanceConstructor);
 	SET_HANDLER_CLASS("town", CTownInstanceConstructor);
 	SET_HANDLER_CLASS("bank", CBankInstanceConstructor);
 	SET_HANDLER_CLASS("bank", CBankInstanceConstructor);
 	SET_HANDLER_CLASS("boat", BoatInstanceConstructor);
 	SET_HANDLER_CLASS("boat", BoatInstanceConstructor);
+	SET_HANDLER_CLASS("flaggable", FlaggableInstanceConstructor);
 	SET_HANDLER_CLASS("market", MarketInstanceConstructor);
 	SET_HANDLER_CLASS("market", MarketInstanceConstructor);
 	SET_HANDLER_CLASS("hillFort", HillFortInstanceConstructor);
 	SET_HANDLER_CLASS("hillFort", HillFortInstanceConstructor);
 	SET_HANDLER_CLASS("shipyard", ShipyardInstanceConstructor);
 	SET_HANDLER_CLASS("shipyard", ShipyardInstanceConstructor);
@@ -82,7 +87,6 @@ CObjectClassesHandler::CObjectClassesHandler()
 	SET_HANDLER("garrison", CGGarrison);
 	SET_HANDLER("garrison", CGGarrison);
 	SET_HANDLER("heroPlaceholder", CGHeroPlaceholder);
 	SET_HANDLER("heroPlaceholder", CGHeroPlaceholder);
 	SET_HANDLER("keymaster", CGKeymasterTent);
 	SET_HANDLER("keymaster", CGKeymasterTent);
-	SET_HANDLER("lighthouse", CGLighthouse);
 	SET_HANDLER("magi", CGMagi);
 	SET_HANDLER("magi", CGMagi);
 	SET_HANDLER("mine", CGMine);
 	SET_HANDLER("mine", CGMine);
 	SET_HANDLER("obelisk", CGObelisk);
 	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.player = h->getOwner();
 	bd.text.appendLocalString(EMetaText::ADVOB_TXT, 32);
 	bd.text.appendLocalString(EMetaText::ADVOB_TXT, 32);
 	bd.components = getPopupComponents(h->getOwner());
 	bd.components = getPopupComponents(h->getOwner());
-	bd.text.replaceRawString(getObjectName());
+	bd.text.replaceTextID(getObjectHandler()->getNameTextID());
 	cb->showBlockingDialog(this, &bd);
 	cb->showBlockingDialog(this, &bd);
 }
 }
 
 
@@ -158,7 +158,7 @@ void CBank::doVisit(const CGHeroInstance * hero) const
 	if (!bankConfig)
 	if (!bankConfig)
 	{
 	{
 		iw.text.appendRawString(VLC->generaltexth->advobtxt[33]);// This was X, now is completely empty
 		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);
 		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;
 		const auto growth = b->val * (base + castleBonus) / 100;
 		if (growth)
 		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)
 	// 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)));
 	TConstBonusListPtr bonuses = getBonuses(Selector::typeSubtype(BonusType::CREATURE_GROWTH, BonusCustomSubtype::creatureLevel(level+1)));
 	for(const auto & b : *bonuses)
 	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;
 	int dwellingBonus = 0;
 	if(const PlayerState *p = cb->getPlayerState(tempOwner, false))
 	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
 void HillFort::onHeroVisit(const CGHeroInstance * h) const
 {
 {
 	cb->showObjectWindow(this, EOpenWindowMode::HILL_FORT_WINDOW, h, false);
 	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;
 	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
 class DLL_LINKAGE CGTerrainPatch : public CGObjectInstance
 {
 {
 public:
 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
 	// optimization - load mappings only once to avoid slow parsing of map headers for map list
 	static const std::map<EMapFormat, MapIdentifiersH3M> identifierMappers = generateMappings();
 	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);
 	const MapIdentifiersH3M & identifierMapper = identifierMappers.at(mapHeader->version);
 
 
 	reader->setIdentifierRemapper(identifierMapper);
 	reader->setIdentifierRemapper(identifierMapper);
@@ -1478,9 +1481,9 @@ CGObjectInstance * CMapLoaderH3M::readShipyard(const int3 & mapPosition, std::sh
 	return object;
 	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());
 	setOwnerAndValidate(mapPosition, object, reader->readPlayer32());
 	return object;
 	return object;
 }
 }
@@ -1618,7 +1621,7 @@ CGObjectInstance * CMapLoaderH3M::readObject(std::shared_ptr<const ObjectTemplat
 			return readPyramid(mapPosition, objectTemplate);
 			return readPyramid(mapPosition, objectTemplate);
 
 
 		case Obj::LIGHTHOUSE:
 		case Obj::LIGHTHOUSE:
-			return readLighthouse(mapPosition);
+			return readLighthouse(mapPosition, objectTemplate);
 
 
 		case Obj::CREATURE_BANK:
 		case Obj::CREATURE_BANK:
 		case Obj::DERELICT_SHIP:
 		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 * readPyramid(const int3 & position, std::shared_ptr<const ObjectTemplate> objTempl);
 	CGObjectInstance * readQuestGuard(const int3 & position);
 	CGObjectInstance * readQuestGuard(const int3 & position);
 	CGObjectInstance * readShipyard(const int3 & mapPosition, std::shared_ptr<const ObjectTemplate> objectTemplate);
 	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 * readGeneric(const int3 & position, std::shared_ptr<const ObjectTemplate> objectTemplate);
 	CGObjectInstance * readBank(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]);
 		const JsonNode underground = getFromArchive(TERRAIN_FILE_NAMES[1]);
 		readTerrainLevel(underground, 1);
 		readTerrainLevel(underground, 1);
 	}
 	}
-
-	map->calculateWaterContent();
 }
 }
 
 
 CMapLoaderJson::MapObjectLoader::MapObjectLoader(CMapLoaderJson * _owner, JsonMap::value_type & json):
 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 MapReaderH3M::readInt8Checked(int8_t lowerLimit, int8_t upperLimit)
 {
 {
 	int8_t result = readInt8();
 	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()
 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 baseTranslation = JsonUtils::assembleFromFiles(mod.config["translations"]);
 	JsonNode extraTranslation = JsonUtils::assembleFromFiles(mod.config[preferredLanguage]["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()
 void CModHandler::load()

+ 33 - 19
lib/rmg/CMapGenerator.cpp

@@ -152,41 +152,55 @@ std::unique_ptr<CMap> CMapGenerator::generate()
 	return std::move(map->mapInstance);
 	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();
 	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())
 	for(const auto & pair : mapGenOptions.getPlayersSettings())
 	{
 	{
 		const auto & pSettings = pair.second;
 		const auto & pSettings = pair.second;
+
 		if(pSettings.getPlayerType() == EPlayerType::HUMAN)
 		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)
 		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()
 void CMapGenerator::addPlayerInfo()
@@ -451,7 +465,7 @@ void CMapGenerator::addHeaderInfo()
 	m.height = mapGenOptions.getHeight();
 	m.height = mapGenOptions.getHeight();
 	m.twoLevel = mapGenOptions.getHasTwoLevels();
 	m.twoLevel = mapGenOptions.getHasTwoLevels();
 	m.name.appendLocalString(EMetaText::GENERAL_TXT, 740);
 	m.name.appendLocalString(EMetaText::GENERAL_TXT, 740);
-	m.description.appendRawString(getMapDescription());
+	m.description = getMapDescription();
 	m.difficulty = EMapDifficulty::NORMAL;
 	m.difficulty = EMapDifficulty::NORMAL;
 	addPlayerInfo();
 	addPlayerInfo();
 	m.waterMap = (mapGenOptions.getWaterContent() != EWaterContent::EWaterContent::NONE);
 	m.waterMap = (mapGenOptions.getWaterContent() != EWaterContent::EWaterContent::NONE);

+ 2 - 4
lib/rmg/CMapGenerator.h

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

+ 4 - 1
lib/serializer/RegisterTypes.h

@@ -20,14 +20,17 @@
 #include "../gameState/CGameState.h"
 #include "../gameState/CGameState.h"
 #include "../gameState/CGameStateCampaign.h"
 #include "../gameState/CGameStateCampaign.h"
 #include "../gameState/TavernHeroesPool.h"
 #include "../gameState/TavernHeroesPool.h"
+
 #include "../mapObjects/CGCreature.h"
 #include "../mapObjects/CGCreature.h"
 #include "../mapObjects/CGDwelling.h"
 #include "../mapObjects/CGDwelling.h"
 #include "../mapObjects/CGMarket.h"
 #include "../mapObjects/CGMarket.h"
 #include "../mapObjects/CGPandoraBox.h"
 #include "../mapObjects/CGPandoraBox.h"
 #include "../mapObjects/CGTownInstance.h"
 #include "../mapObjects/CGTownInstance.h"
 #include "../mapObjects/CQuest.h"
 #include "../mapObjects/CQuest.h"
+#include "../mapObjects/FlaggableMapObject.h"
 #include "../mapObjects/MiscObjects.h"
 #include "../mapObjects/MiscObjects.h"
 #include "../mapObjects/TownBuildingInstance.h"
 #include "../mapObjects/TownBuildingInstance.h"
+
 #include "../mapping/CMap.h"
 #include "../mapping/CMap.h"
 #include "../networkPacks/PacksForClient.h"
 #include "../networkPacks/PacksForClient.h"
 #include "../networkPacks/PacksForClientBattle.h"
 #include "../networkPacks/PacksForClientBattle.h"
@@ -73,7 +76,7 @@ void registerTypes(Serializer &s)
 	s.template registerType<CGSirens>(15);
 	s.template registerType<CGSirens>(15);
 	s.template registerType<CGShipyard>(16);
 	s.template registerType<CGShipyard>(16);
 	s.template registerType<CGDenOfthieves>(17);
 	s.template registerType<CGDenOfthieves>(17);
-	s.template registerType<CGLighthouse>(18);
+	s.template registerType<FlaggableMapObject>(18);
 	s.template registerType<CGTerrainPatch>(19);
 	s.template registerType<CGTerrainPatch>(19);
 	s.template registerType<HillFort>(20);
 	s.template registerType<HillFort>(20);
 	s.template registerType<CGMarket>(21);
 	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
 	// pseudo-array, that don't have H3 file with same name
 	seerEmpty        (*this, "core.seerhut.empty"  ),
 	seerEmpty        (*this, "core.seerhut.empty"  ),
 	seerNames        (*this, "core.seerhut.names"  ),
 	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.vcdesc",   "DATA/VCDESC.TXT"   );
 	readToVector("core.lcdesc",   "DATA/LCDESC.TXT"   );
 	readToVector("core.lcdesc",   "DATA/LCDESC.TXT"   );
@@ -166,10 +164,6 @@ CGeneralTextHandler::CGeneralTextHandler():
 	readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" );
 	readToVector("core.mineevnt", "DATA/MINEEVNT.TXT" );
 	readToVector("core.xtrainfo", "DATA/XTRAINFO.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"));
 		CLegacyConfigParser parser(TextPath::builtin("DATA/RANDTVRN.TXT"));
 		parser.endLine();
 		parser.endLine();
@@ -298,11 +292,6 @@ CGeneralTextHandler::CGeneralTextHandler():
 			scenariosCountPerCampaign.push_back(region);
 			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
 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 fcommands; // fort screen
 	LegacyTextContainer tavernInfo;
 	LegacyTextContainer tavernInfo;
 
 
-	LegacyTextContainer qeModCommands;
-
 	LegacyHelpContainer zelp;
 	LegacyHelpContainer zelp;
 
 
 	//objects
 	//objects
@@ -75,8 +73,6 @@ public:
 
 
 	//sec skills
 	//sec skills
 	LegacyTextContainer levels;
 	LegacyTextContainer levels;
-	//commanders
-	LegacyTextContainer znpc00; //more or less useful content of that file
 
 
 	std::vector<std::string> findStringsWithPrefix(const std::string & prefix);
 	std::vector<std::string> findStringsWithPrefix(const std::string & prefix);
 
 

+ 6 - 0
lib/texts/MetaString.cpp

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

+ 2 - 0
lib/texts/MetaString.h

@@ -21,6 +21,7 @@ class MapObjectSubID;
 class PlayerColor;
 class PlayerColor;
 class SecondarySkill;
 class SecondarySkill;
 class SpellID;
 class SpellID;
+class FactionID;
 class GameResID;
 class GameResID;
 using TQuantity = si32;
 using TQuantity = si32;
 
 
@@ -97,6 +98,7 @@ public:
 	void replacePositiveNumber(int64_t txt);
 	void replacePositiveNumber(int64_t txt);
 
 
 	void replaceName(const ArtifactID & id);
 	void replaceName(const ArtifactID & id);
+	void replaceName(const FactionID& id);
 	void replaceName(const MapObjectID& id);
 	void replaceName(const MapObjectID& id);
 	void replaceName(const PlayerColor& id);
 	void replaceName(const PlayerColor& id);
 	void replaceName(const SecondarySkill& 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;
 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);
 	std::lock_guard globalLock(globalTextMutex);
 
 
@@ -42,6 +42,11 @@ void TextLocalizationContainer::registerStringOverride(const std::string & modCo
 			entry.identifierModContext = modContext;
 			entry.identifierModContext = modContext;
 			entry.baseStringModContext = modContext;
 			entry.baseStringModContext = modContext;
 		}
 		}
+		else
+		{
+			if (language == VLC->generaltexth->getPreferredLanguage())
+				entry.overriden = true;
+		}
 	}
 	}
 	else
 	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())
 	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
 bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) const
@@ -140,15 +145,18 @@ bool TextLocalizationContainer::identifierExists(const TextIdentifier & UID) con
 	return stringsLocalizations.count(UID.get());
 	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);
 	std::lock_guard globalLock(globalTextMutex);
 
 
 	for (auto const & subContainer : subContainers)
 	for (auto const & subContainer : subContainers)
-		subContainer->exportAllTexts(storage);
+		subContainer->exportAllTexts(storage, onlyMissing);
 
 
 	for (auto const & entry : stringsLocalizations)
 	for (auto const & entry : stringsLocalizations)
 	{
 	{
+		if (onlyMissing && entry.second.overriden)
+			continue;
+
 		std::string textToWrite;
 		std::string textToWrite;
 		std::string modName = entry.second.baseStringModContext;
 		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)
 		/// Different from identifierModContext if mod has modified object from another mod (e.g. rebalance mods)
 		std::string baseStringModContext;
 		std::string baseStringModContext;
 
 
+		bool overriden = false;
+
 		template <typename Handler>
 		template <typename Handler>
 		void serialize(Handler & h)
 		void serialize(Handler & h)
 		{
 		{
@@ -47,7 +49,7 @@ protected:
 	std::vector<const TextLocalizationContainer *> subContainers;
 	std::vector<const TextLocalizationContainer *> subContainers;
 
 
 	/// add selected string to internal storage as high-priority strings
 	/// 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);
 	std::string getModLanguage(const std::string & modContext);
 
 
@@ -57,7 +59,7 @@ protected:
 public:
 public:
 	/// Loads translation from provided json
 	/// Loads translation from provided json
 	/// Any entries loaded by this will have priority over texts registered normally
 	/// 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
 	/// add selected string to internal storage
 	void registerString(const std::string & modContext, const TextIdentifier & UID, const JsonNode & localized);
 	void registerString(const std::string & modContext, const TextIdentifier & UID, const JsonNode & localized);
@@ -77,7 +79,7 @@ public:
 
 
 	/// Debug method, returns all currently stored texts
 	/// Debug method, returns all currently stored texts
 	/// Format: [mod ID][string ID] -> human-readable text
 	/// 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
 	/// Add or override subcontainer which can store identifiers
 	void addSubContainer(const TextLocalizationContainer & container);
 	void addSubContainer(const TextLocalizationContainer & container);

+ 0 - 1
mapeditor/StdInc.h

@@ -11,7 +11,6 @@
 
 
 #include "../Global.h"
 #include "../Global.h"
 
 
-#define VCMI_EDITOR_VERSION "0.2"
 #define VCMI_EDITOR_NAME "VCMI Map Editor"
 #define VCMI_EDITOR_NAME "VCMI Map Editor"
 
 
 #include <QtWidgets>
 #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(CGHeroPlaceholder);
 	INIT_OBJ_TYPE(CGHeroInstance);
 	INIT_OBJ_TYPE(CGHeroInstance);
 	INIT_OBJ_TYPE(CGSignBottle);
 	INIT_OBJ_TYPE(CGSignBottle);
-	INIT_OBJ_TYPE(CGLighthouse);
+	INIT_OBJ_TYPE(FlaggableMapObject);
 	//INIT_OBJ_TYPE(CRewardableObject);
 	//INIT_OBJ_TYPE(CRewardableObject);
 	//INIT_OBJ_TYPE(CGPandoraBox);
 	//INIT_OBJ_TYPE(CGPandoraBox);
 	//INIT_OBJ_TYPE(CGEvent);
 	//INIT_OBJ_TYPE(CGEvent);
@@ -108,7 +108,7 @@ void Initializer::initialize(CGShipyard * o)
 	o->tempOwner = defaultPlayer;
 	o->tempOwner = defaultPlayer;
 }
 }
 
 
-void Initializer::initialize(CGLighthouse * o)
+void Initializer::initialize(FlaggableMapObject * o)
 {
 {
 	if(!o) return;
 	if(!o) return;
 	
 	
@@ -172,10 +172,12 @@ void Initializer::initialize(CGTownInstance * o)
 	if(lvl > 2) o->addBuilding(BuildingID::CASTLE);
 	if(lvl > 2) o->addBuilding(BuildingID::CASTLE);
 	if(lvl > 3) o->addBuilding(BuildingID::CAPITOL);
 	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;
 	if(!o) return;
 
 
@@ -492,7 +494,7 @@ void Inspector::updateProperties()
 	UPDATE_OBJ_PROPERTIES(CGHeroPlaceholder);
 	UPDATE_OBJ_PROPERTIES(CGHeroPlaceholder);
 	UPDATE_OBJ_PROPERTIES(CGHeroInstance);
 	UPDATE_OBJ_PROPERTIES(CGHeroInstance);
 	UPDATE_OBJ_PROPERTIES(CGSignBottle);
 	UPDATE_OBJ_PROPERTIES(CGSignBottle);
-	UPDATE_OBJ_PROPERTIES(CGLighthouse);
+	UPDATE_OBJ_PROPERTIES(FlaggableMapObject);
 	UPDATE_OBJ_PROPERTIES(CRewardableObject);
 	UPDATE_OBJ_PROPERTIES(CRewardableObject);
 	UPDATE_OBJ_PROPERTIES(CGPandoraBox);
 	UPDATE_OBJ_PROPERTIES(CGPandoraBox);
 	UPDATE_OBJ_PROPERTIES(CGEvent);
 	UPDATE_OBJ_PROPERTIES(CGEvent);
@@ -540,7 +542,7 @@ void Inspector::setProperty(const QString & key, const QVariant & value)
 	SET_PROPERTIES(CGHeroInstance);
 	SET_PROPERTIES(CGHeroInstance);
 	SET_PROPERTIES(CGShipyard);
 	SET_PROPERTIES(CGShipyard);
 	SET_PROPERTIES(CGSignBottle);
 	SET_PROPERTIES(CGSignBottle);
-	SET_PROPERTIES(CGLighthouse);
+	SET_PROPERTIES(FlaggableMapObject);
 	SET_PROPERTIES(CRewardableObject);
 	SET_PROPERTIES(CRewardableObject);
 	SET_PROPERTIES(CGPandoraBox);
 	SET_PROPERTIES(CGPandoraBox);
 	SET_PROPERTIES(CGEvent);
 	SET_PROPERTIES(CGEvent);
@@ -553,7 +555,7 @@ void Inspector::setProperty(CArmedInstance * o, const QString & key, const QVari
 	if(!o) return;
 	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;
 	if(!o) return;
 }
 }

+ 3 - 2
mapeditor/inspector/inspector.h

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

+ 2 - 1
mapeditor/mainwindow.cpp

@@ -182,6 +182,7 @@ MainWindow::MainWindow(QWidget* parent) :
 	console = new CConsoleHandler();
 	console = new CConsoleHandler();
 	logConfig = new CBasicLogConfigurator(logPath, console);
 	logConfig = new CBasicLogConfigurator(logPath, console);
 	logConfig->configureDefault();
 	logConfig->configureDefault();
+	logGlobal->info("Starting map editor of '%s'", GameConstants::VCMI_VERSION);
 	logGlobal->info("The log file will be saved to %s", logPath);
 	logGlobal->info("The log file will be saved to %s", logPath);
 
 
 	//init
 	//init
@@ -317,7 +318,7 @@ void MainWindow::setStatusMessage(const QString & status)
 
 
 void MainWindow::setTitle()
 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);
 	setWindowTitle(title);
 }
 }
 
 

+ 4 - 3
mapeditor/mapcontroller.cpp

@@ -121,7 +121,7 @@ void MapController::repairMap(CMap * map) const
 			   dynamic_cast<CGTownInstance*>(obj.get()) ||
 			   dynamic_cast<CGTownInstance*>(obj.get()) ||
 			   dynamic_cast<CGGarrison*>(obj.get()) ||
 			   dynamic_cast<CGGarrison*>(obj.get()) ||
 			   dynamic_cast<CGShipyard*>(obj.get()) ||
 			   dynamic_cast<CGShipyard*>(obj.get()) ||
-			   dynamic_cast<CGLighthouse*>(obj.get()) ||
+			   dynamic_cast<FlaggableMapObject*>(obj.get()) ||
 			   dynamic_cast<CGHeroInstance*>(obj.get()))
 			   dynamic_cast<CGHeroInstance*>(obj.get()))
 				obj->tempOwner = PlayerColor::NEUTRAL;
 				obj->tempOwner = PlayerColor::NEUTRAL;
 		}
 		}
@@ -369,6 +369,7 @@ void MapController::pasteFromClipboard(int level)
 		if (!canPlaceObject(level, obj, errorMsg))
 		if (!canPlaceObject(level, obj, errorMsg))
 		{
 		{
 			errors.push_back(std::move(errorMsg));
 			errors.push_back(std::move(errorMsg));
+			continue;
 		}
 		}
 		auto newPos = objUniquePtr->pos + shift;
 		auto newPos = objUniquePtr->pos + shift;
 		if(_map->isInTheMap(newPos))
 		if(_map->isInTheMap(newPos))
@@ -380,8 +381,8 @@ void MapController::pasteFromClipboard(int level)
 		_scenes[level]->selectionObjectsView.selectObject(obj);
 		_scenes[level]->selectionObjectsView.selectObject(obj);
 		_mapHandler->invalidate(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]->objectsView.draw();
 	_scenes[level]->passabilityView.update();
 	_scenes[level]->passabilityView.update();

+ 15 - 5
server/battles/BattleProcessor.cpp

@@ -28,10 +28,12 @@
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/gameState/CGameState.h"
 #include "../../lib/mapping/CMap.h"
 #include "../../lib/mapping/CMap.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/modding/IdentifierStorage.h"
 #include "../../lib/modding/IdentifierStorage.h"
 #include "../../lib/networkPacks/PacksForClient.h"
 #include "../../lib/networkPacks/PacksForClient.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
 #include "../../lib/CPlayerState.h"
 #include "../../lib/CPlayerState.h"
+#include <vstd/RNG.h>
 
 
 BattleProcessor::BattleProcessor(CGameHandler * gameHandler)
 BattleProcessor::BattleProcessor(CGameHandler * gameHandler)
 	: gameHandler(gameHandler)
 	: gameHandler(gameHandler)
@@ -156,16 +158,24 @@ BattleID BattleProcessor::setupBattle(int3 tile, BattleSideArray<const CArmedIns
 {
 {
 	const auto & t = *gameHandler->getTile(tile);
 	const auto & t = *gameHandler->getTile(tile);
 	TerrainId terrain = t.terType->getId();
 	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;
 		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
 	//send info about battles
 	BattleStart bs;
 	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;
 	bs.battleID = gameHandler->gameState()->nextBattleID;
 
 
 	engageIntoBattle(bs.info->getSide(BattleSide::ATTACKER).color);
 	engageIntoBattle(bs.info->getSide(BattleSide::ATTACKER).color);