Selaa lähdekoodia

Merge remote-tracking branch 'origin/beta' into random_prison_distributor

Tomasz Zieliński 1 vuosi sitten
vanhempi
sitoutus
b0f0e9caa8
100 muutettua tiedostoa jossa 2813 lisäystä ja 746 poistoa
  1. 11 1
      .github/workflows/github.yml
  2. 20 17
      AI/BattleAI/BattleEvaluator.cpp
  3. 1 0
      CI/conan/base/cross-macro.j2
  4. 16 0
      CI/mingw-32/before_install.sh
  5. 1 1
      CI/mingw/before_install.sh
  6. 4 1
      CMakeLists.txt
  7. 25 0
      ChangeLog.md
  8. 101 9
      Mods/vcmi/config/vcmi/chinese.json
  9. 237 138
      Mods/vcmi/config/vcmi/czech.json
  10. 3 0
      Mods/vcmi/config/vcmi/english.json
  11. 2 0
      Mods/vcmi/config/vcmi/german.json
  12. 5 0
      Mods/vcmi/config/vcmi/ukrainian.json
  13. 2 2
      android/vcmi-app/build.gradle
  14. 0 8
      android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java
  15. 71 0
      android/vcmi-app/src/main/res/values-cs/strings.xml
  16. 31 11
      client/CMT.cpp
  17. 1 1
      client/CVideoHandler.cpp
  18. 5 1
      client/NetPacksLobbyClient.cpp
  19. 2 0
      client/PlayerLocalState.cpp
  20. 2 2
      client/PlayerLocalState.h
  21. 7 2
      client/adventureMap/AdventureMapInterface.cpp
  22. 3 0
      client/adventureMap/AdventureMapInterface.h
  23. 3 0
      client/adventureMap/CInGameConsole.cpp
  24. 8 14
      client/adventureMap/CList.cpp
  25. 0 2
      client/adventureMap/CList.h
  26. 140 111
      client/adventureMap/TurnTimerWidget.cpp
  27. 23 30
      client/adventureMap/TurnTimerWidget.h
  28. 2 1
      client/battle/BattleInterface.cpp
  29. 2 0
      client/battle/BattleStacksController.cpp
  30. 43 4
      client/battle/BattleWindow.cpp
  31. 5 0
      client/battle/BattleWindow.h
  32. 2 4
      client/lobby/CBonusSelection.cpp
  33. 2 0
      client/lobby/CLobbyScreen.cpp
  34. 6 3
      client/lobby/CSelectionBase.cpp
  35. 2 0
      client/lobby/CSelectionBase.h
  36. 1 1
      client/lobby/OptionsTab.cpp
  37. 5 1
      client/lobby/RandomMapTab.cpp
  38. 2 3
      client/mainmenu/CHighScoreScreen.cpp
  39. 20 3
      client/mainmenu/CMainMenu.cpp
  40. 30 6
      client/widgets/CArtifactHolder.cpp
  41. 14 9
      client/widgets/CArtifactHolder.h
  42. 3 3
      client/widgets/CArtifactsOfHeroAltar.cpp
  43. 1 1
      client/widgets/CArtifactsOfHeroAltar.h
  44. 148 40
      client/widgets/CArtifactsOfHeroBackpack.cpp
  45. 24 6
      client/widgets/CArtifactsOfHeroBackpack.h
  46. 26 11
      client/widgets/CArtifactsOfHeroBase.cpp
  47. 7 4
      client/widgets/CArtifactsOfHeroBase.h
  48. 6 5
      client/widgets/CArtifactsOfHeroKingdom.cpp
  49. 1 1
      client/widgets/CArtifactsOfHeroKingdom.h
  50. 4 3
      client/widgets/CArtifactsOfHeroMain.cpp
  51. 1 1
      client/widgets/CArtifactsOfHeroMain.h
  52. 2 2
      client/widgets/CArtifactsOfHeroMarket.cpp
  53. 1 1
      client/widgets/CArtifactsOfHeroMarket.h
  54. 46 12
      client/widgets/CWindowWithArtifacts.cpp
  55. 5 3
      client/widgets/CWindowWithArtifacts.h
  56. 14 2
      client/widgets/MiscWidgets.cpp
  57. 3 0
      client/widgets/MiscWidgets.h
  58. 3 3
      client/widgets/TextControls.cpp
  59. 51 14
      client/windows/CHeroBackpackWindow.cpp
  60. 17 2
      client/windows/CHeroBackpackWindow.h
  61. 0 1
      client/windows/CHeroWindow.h
  62. 3 3
      client/windows/CMapOverview.cpp
  63. 114 62
      client/windows/CSpellWindow.cpp
  64. 8 0
      client/windows/CSpellWindow.h
  65. 1 1
      client/windows/CTradeWindow.cpp
  66. 1 1
      client/windows/CTradeWindow.h
  67. 3 1
      client/windows/GUIClasses.cpp
  68. 2 1
      client/windows/GUIClasses.h
  69. 1 1
      cmake_modules/VersionDefinition.cmake
  70. 2 0
      config/artifacts.json
  71. 12 1
      config/objects/generic.json
  72. 1 1
      config/schemas/settings.json
  73. 1 1
      config/spells/timed.json
  74. 2 1
      config/terrainViewPatterns.json
  75. 2 2
      config/widgets/turnOptionsTab.json
  76. 0 34
      config/widgets/turnTimer.json
  77. 7 1
      debian/changelog
  78. 1 0
      docs/Readme.md
  79. 2 1
      docs/modders/Bonus/Bonus_Duration_Types.md
  80. 15 3
      docs/players/Cheat_Codes.md
  81. 1 1
      include/vstd/DateUtils.h
  82. 61 37
      launcher/eu.vcmi.VCMI.metainfo.xml
  83. 7 7
      launcher/mainwindow_moc.cpp
  84. 9 1
      launcher/modManager/cmodlistview_moc.cpp
  85. 1 0
      launcher/modManager/cmodlistview_moc.h
  86. 16 5
      launcher/modManager/cmodmanager.cpp
  87. 1207 0
      launcher/translation/czech.ts
  88. 20 6
      lib/BasicTypes.cpp
  89. 1 13
      lib/CArtHandler.cpp
  90. 1 5
      lib/CArtHandler.h
  91. 5 3
      lib/CArtifactInstance.cpp
  92. 1 0
      lib/CArtifactInstance.h
  93. 5 0
      lib/CGameInfoCallback.cpp
  94. 1 1
      lib/CGameInfoCallback.h
  95. 2 3
      lib/GameSettings.cpp
  96. 35 25
      lib/JsonNode.cpp
  97. 3 2
      lib/JsonRandom.cpp
  98. 24 21
      lib/Languages.h
  99. 9 0
      lib/TextOperations.cpp
  100. 3 0
      lib/TextOperations.h

+ 11 - 1
.github/workflows/github.yml

@@ -116,7 +116,7 @@ jobs:
             pack_type: RelWithDebInfo
             extension: exe
             preset: windows-msvc-release-ccache
-          - platform: mingw-ubuntu
+          - platform: mingw
             os: ubuntu-22.04
             test: 0
             pack: 1
@@ -126,6 +126,16 @@ jobs:
             cmake_args: -G Ninja
             preset: windows-mingw-conan-linux
             conan_profile: mingw64-linux.jinja
+          - platform: mingw-32
+            os: ubuntu-22.04
+            test: 0
+            pack: 1
+            pack_type: Release
+            extension: exe
+            cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis`
+            cmake_args: -G Ninja
+            preset: windows-mingw-conan-linux
+            conan_profile: mingw32-linux.jinja
           - platform: android-32
             os: ubuntu-22.04
             extension: apk

+ 20 - 17
AI/BattleAI/BattleEvaluator.cpp

@@ -427,33 +427,36 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 				state->nextTurn(unit->unitId());
 
-				PotentialTargets pt(unit, damageCache, state);
+				PotentialTargets potentialTargets(unit, damageCache, state);
 
-				if(!pt.possibleAttacks.empty())
+				if(!potentialTargets.possibleAttacks.empty())
 				{
-					AttackPossibility ap = pt.bestAction();
+					AttackPossibility attackPossibility = potentialTargets.bestAction();
 
-					auto swb = state->getForUpdate(unit->unitId());
-					*swb = *ap.attackerState;
+					auto stackWithBonuses = state->getForUpdate(unit->unitId());
+					*stackWithBonuses = *attackPossibility.attackerState;
 
-					if(ap.defenderDamageReduce > 0)
-						swb->removeUnitBonus(Bonus::UntilAttack);
-					if(ap.attackerDamageReduce > 0)
-						swb->removeUnitBonus(Bonus::UntilBeingAttacked);
+					if(attackPossibility.defenderDamageReduce > 0)
+					{
+						stackWithBonuses->removeUnitBonus(Bonus::UntilAttack);
+						stackWithBonuses->removeUnitBonus(Bonus::UntilOwnAttack);
+					}
+					if(attackPossibility.attackerDamageReduce > 0)
+						stackWithBonuses->removeUnitBonus(Bonus::UntilBeingAttacked);
 
-					for(auto affected : ap.affectedUnits)
+					for(auto affected : attackPossibility.affectedUnits)
 					{
-						swb = state->getForUpdate(affected->unitId());
-						*swb = *affected;
+						stackWithBonuses = state->getForUpdate(affected->unitId());
+						*stackWithBonuses = *affected;
 
-						if(ap.defenderDamageReduce > 0)
-							swb->removeUnitBonus(Bonus::UntilBeingAttacked);
-						if(ap.attackerDamageReduce > 0 && ap.attack.defender->unitId() == affected->unitId())
-							swb->removeUnitBonus(Bonus::UntilAttack);
+						if(attackPossibility.defenderDamageReduce > 0)
+							stackWithBonuses->removeUnitBonus(Bonus::UntilBeingAttacked);
+						if(attackPossibility.attackerDamageReduce > 0 && attackPossibility.attack.defender->unitId() == affected->unitId())
+							stackWithBonuses->removeUnitBonus(Bonus::UntilAttack);
 					}
 				}
 
-				auto bav = pt.bestActionValue();
+				auto bav = potentialTargets.bestActionValue();
 
 				//best action is from effective owner`s point if view, we need to convert to our point if view
 				if(state->battleGetOwner(unit) != playerID)

+ 1 - 0
CI/conan/base/cross-macro.j2

@@ -17,4 +17,5 @@ RC={{ target_host }}-windres
 {% macro generate_conf(target_host) -%}
 tools.build:compiler_executables = {"c": "{{ target_host }}-gcc", "cpp": "{{ target_host }}-g++"}
 tools.build:sysroot = /usr/{{ target_host }}
+tools.build:defines = ["WINVER=0x0601", "_WIN32_WINNT=0x0601"]
 {%- endmacro -%}

+ 16 - 0
CI/mingw-32/before_install.sh

@@ -0,0 +1,16 @@
+#!/usr/bin/env bash
+
+sudo apt-get update
+sudo apt-get install ninja-build mingw-w64 nsis
+sudo update-alternatives --set i686-w64-mingw32-g++ /usr/bin/i686-w64-mingw32-g++-posix
+
+# Workaround for getting new MinGW headers on Ubuntu 22.04.
+# Remove it once MinGW headers version in repository will be 10.0 at least
+curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-common_10.0.0-3_all.deb \
+  && sudo dpkg -i mingw-w64-common_10.0.0-3_all.deb;
+curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-i686-dev_10.0.0-3_all.deb \
+  && sudo dpkg -i mingw-w64-i686-dev_10.0.0-3_all.deb;
+
+mkdir ~/.conan ; cd ~/.conan
+curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.1/vcmi-deps-windows-conan-w32.tgz" \
+	| tar -xzf -

+ 1 - 1
CI/mingw-ubuntu/before_install.sh → CI/mingw/before_install.sh

@@ -12,5 +12,5 @@ curl -O -L http://mirrors.kernel.org/ubuntu/pool/universe/m/mingw-w64/mingw-w64-
   && sudo dpkg -i mingw-w64-x86-64-dev_10.0.0-3_all.deb;
 
 mkdir ~/.conan ; cd ~/.conan
-curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.0/vcmi-deps-windows-conan-w64.tgz" \
+curl -L "https://github.com/vcmi/vcmi-deps-windows-conan/releases/download/1.1/vcmi-deps-windows-conan-w64.tgz" \
 	| tar -xzf -

+ 4 - 1
CMakeLists.txt

@@ -255,7 +255,10 @@ endif()
 
 if(MINGW OR MSVC)
 	# Windows Vista or newer for FuzzyLite 6 to compile
-	add_definitions(-D_WIN32_WINNT=0x0600)
+	# Except for conan which already has this definition in its preset
+	if(NOT USING_CONAN)
+		add_definitions(-D_WIN32_WINNT=0x0600)
+	endif()
 
 	#delete lib prefix for dlls (libvcmi -> vcmi)
 	set(CMAKE_SHARED_LIBRARY_PREFIX "")

+ 25 - 0
ChangeLog.md

@@ -1,3 +1,28 @@
+# 1.4.0 -> 1.4.1
+
+### General
+* Fixed position for interaction with starting heroes
+* Fixed smooth map scrolling when running at high framerate
+* Fixed calculation of Fire Shield damage when caster has artifacts that increase its damage
+* Fixed untranslated message when visiting signs with random text
+* Fixed slider scrolling to maximum value when clicking on "scroll right" button
+* Fixed events and seer huts not activating in some cases
+* Fixed bug leading to Artifact Merchant selling Grails in loaded saved games
+* Fixed placement of objects in random maps near the top border of the map
+* Creatures under Slayer spell will no longer deal additional damage to creatures not affected by Slayer
+* Description of a mod in Launcher will no longer be converted to lower-case
+* Game will no longer fail to generate random map when AI-only players option is set to non-zero value
+* Added option to mute audio when VCMI window is not active
+* Added option to disable smooth map scrolling
+* Reverted ban on U-turns in pathfinder
+
+### Stability
+* Fixed crash on using mods made for VCMI 1.3
+* Fixed crash on generating random map with large number of monoliths
+* Fixed crash on losing mission-critical hero in battle
+* Fixed crash on generating growth detalization in some localizations
+* Fixed crash on loading of some user-made maps
+
 # 1.3.2 -> 1.4.0
 
 ### General

+ 101 - 9
Mods/vcmi/config/vcmi/chinese.json

@@ -36,14 +36,27 @@
 	"vcmi.heroOverview.spells" : "魔法",
 
 	"vcmi.radialWheel.mergeSameUnit" : "合并相同生物",
-	"vcmi.radialWheel.showUnitInformation" : "显示生物信息",
+	"vcmi.radialWheel.fillSingleUnit" : "单个生物填充空格",
 	"vcmi.radialWheel.splitSingleUnit" : "分割单个生物",
 	"vcmi.radialWheel.splitUnitEqually" : "平均分配生物",
 	"vcmi.radialWheel.moveUnit" : "将生物移动到部队",
 	"vcmi.radialWheel.splitUnit" : "分割生物到其他空位",
 
+	"vcmi.radialWheel.heroGetArmy" : "移动生物",
+	"vcmi.radialWheel.heroSwapArmy" : "交换生物",
+	"vcmi.radialWheel.heroExchange" : "开启英雄交换",
+	"vcmi.radialWheel.heroGetArtifacts" : "移动宝物",
+	"vcmi.radialWheel.heroSwapArtifacts" : "交换宝物",
+	"vcmi.radialWheel.heroDismiss" : "解雇英雄",
+
+	"vcmi.radialWheel.moveTop" : "移到顶端",
+	"vcmi.radialWheel.moveUp" : "上移",
+	"vcmi.radialWheel.moveDown" : "下移",
+	"vcmi.radialWheel.moveBottom" : "移到底端",
+	
 	"vcmi.mainMenu.serverConnecting" : "连接中...",
 	"vcmi.mainMenu.serverAddressEnter" : "使用地址:",
+	"vcmi.mainMenu.serverConnectionFailed" : "连接失败",
 	"vcmi.mainMenu.serverClosing" : "关闭中...",
 	"vcmi.mainMenu.hostTCP" : "创建TCP/IP游戏",
 	"vcmi.mainMenu.joinTCP" : "加入TCP/IP游戏",
@@ -51,11 +64,18 @@
 
 	"vcmi.lobby.filename" : "文件名",
 	"vcmi.lobby.creationDate" : "创建时间",
+	"vcmi.lobby.scenarioName" : "场景名称",
+	"vcmi.lobby.mapPreview" : "地图预览",
+	"vcmi.lobby.noPreview" : "无地上部分",
+	"vcmi.lobby.noUnderground" : "无地下部分",
 
 	"vcmi.server.errors.existingProcess"     : "一个VCMI进程已经在运行,启动新进程前请结束它。",
 	"vcmi.server.errors.modsToEnable"    : "{需要启用的mod列表}",
 	"vcmi.server.errors.modsToDisable"   : "{需要禁用的mod列表}",
 	"vcmi.server.confirmReconnect"           : "您想要重连上一个会话么?",
+	"vcmi.server.errors.modNoDependency" : "读取mod包 {'%s'}失败!\n 需要的mod {'%s'} 没有安装或无效!\n",
+	"vcmi.server.errors.modConflict" : "读取的mod包 {'%s'}无法运行!\n 与另一个mod {'%s'}冲突!\n",
+	"vcmi.server.errors.unknownEntity" : "加载保存失败! 在保存的游戏中发现未知实体'%s'! 保存可能与当前安装的mod版本不兼容!",
 
 	"vcmi.settingsMainWindow.generalTab.hover"   : "常规",
 	"vcmi.settingsMainWindow.generalTab.help"    : "切换到“常规”选项卡 - 设置游戏客户端呈现",
@@ -92,6 +112,10 @@
 	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{触觉反馈}\n\n切换触摸输入的触觉反馈。",
 	"vcmi.systemOptions.enableUiEnhancementsButton.hover"  : "界面增强",
 	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{界面增强}\n\n显示所有界面增强内容,如大背包和魔法书等。",
+	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "增大魔法书界面",
+	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{增大魔法书界面}\n\n可以在魔法书单页中显示更多的魔法,从而获得更好的视觉效果。",
+	"vcmi.systemOptions.audioMuteFocus.hover"  : "切换窗口时静音",
+	"vcmi.systemOptions.audioMuteFocus.help"   : "{切换窗口时静音}\n\n快速切换窗口时将静音,在工作时,切换游戏窗口不会有声音。",
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "在信息面板显示消息",
 	"vcmi.adventureOptions.infoBarPick.help" : "{在信息面板显示消息}\n\n来自访问地图物件的信息将显示在信息面板,而不是弹出窗口。",
@@ -107,6 +131,8 @@
 	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{信息面板生物管理}\n\n允许在信息面板中重新排列生物,而不是在默认组件之间循环。",
 	"vcmi.adventureOptions.leftButtonDrag.hover" : "左键拖动地图",
 	"vcmi.adventureOptions.leftButtonDrag.help" : "{左键拖动地图}\n\n启用后,按住左键移动鼠标将拖动冒险地图视图。",
+	"vcmi.adventureOptions.smoothDragging.hover" : "平滑地图拖动",
+	"vcmi.adventureOptions.smoothDragging.help" : "{平滑地图拖动}\n\n启用后,地图拖动会产生柔和的羽化效果。",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
@@ -139,6 +165,9 @@
 	"vcmi.battleOptions.enableAutocombatSpells.help": "{魔法}\n\n快速战斗时不会使用魔法。",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "跳过战斗开始音乐",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{跳过战斗开始音乐}\n\n战斗开始音乐播放期间,你也能够进行操作。",
+	
+	"vcmi.adventureMap.revisitObject.hover" : "重新访问",
+	"vcmi.adventureMap.revisitObject.help" : "{重新访问}\n\n让当前英雄重新访问地图建筑或城镇。",
 
 	"vcmi.battleWindow.pressKeyToSkipIntro" : "按下任意键立即开始战斗",
 	"vcmi.battleWindow.damageEstimation.melee" : "近战攻击 %CREATURE (%DAMAGE).",
@@ -153,6 +182,15 @@
 	"vcmi.battleWindow.damageEstimation.kills.1" : "%d 将被消灭",
 
 	"vcmi.battleResultsWindow.applyResultsLabel" : "接受战斗结果",
+	
+	"vcmi.tutorialWindow.title" : "触摸屏介绍",
+	"vcmi.tutorialWindow.decription.RightClick" : "触摸并按住要右键单击的元素。 触摸可用区域以关闭。",
+	"vcmi.tutorialWindow.decription.MapPanning" : "用一根手指触摸并拖动来移动地图。",
+	"vcmi.tutorialWindow.decription.MapZooming" : "用两根手指捏合可更改地图缩放比例。",
+	"vcmi.tutorialWindow.decription.RadialWheel" : "滑动可打开径向轮以执行各种操作,例如生物/英雄管理和城镇排序。",
+	"vcmi.tutorialWindow.decription.BattleDirection" : "要从特定方向攻击,请向要进行攻击的方向滑动。",
+	"vcmi.tutorialWindow.decription.BattleDirectionAbort" : "如果手指距离足够远,可以取消攻击方向手势。",
+	"vcmi.tutorialWindow.decription.AbortSpell" : "触摸并按住可取消魔法。",
 
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "显示可招募生物",
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{显示可招募生物}\n\n在城镇摘要(城镇屏幕的左下角)中显示可招募的生物数量,而不是增长。",
@@ -202,14 +240,68 @@
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "同盟关系",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "道路类型",
 
-	"vcmi.optionsTab.chessFieldBase.hover" : "额外计时器",
-	"vcmi.optionsTab.chessFieldTurn.hover" : "转动计时器",
+	"vcmi.optionsTab.turnOptions.hover" : "回合选项",
+	"vcmi.optionsTab.turnOptions.help" : "选择回合计时器并同步回合选项",
+	"vcmi.optionsTab.selectPreset" : "预设",
+
+	"vcmi.optionsTab.chessFieldBase.hover" : "基本计时器",
+	"vcmi.optionsTab.chessFieldTurn.hover" : "回合计时器",
 	"vcmi.optionsTab.chessFieldBattle.hover" : "战斗计时器",
-	"vcmi.optionsTab.chessFieldUnit.hover" : "堆栈计时器",
-	"vcmi.optionsTab.chessFieldBase.help" : "当{转动计时器}达到零时开始倒计时。 它仅在游戏开始时设置一次。 当计时器达到零时,玩家的回合结束。",
-	"vcmi.optionsTab.chessFieldTurn.help" : "当玩家在冒险地图上开始回合时开始倒计时。 它在每回合开始时重置为其初始值。 任何未使用的回合时间将被添加到{额外计时器}(如果正在使用)中。",
-	"vcmi.optionsTab.chessFieldBattle.help" : "战斗期间当 {堆栈计时器} 达到0时进行倒计时。 每次战斗开始时都会重置为初始值。 如果计时器达到零,当前活动的堆栈将进行防御。",
-	"vcmi.optionsTab.chessFieldUnit.help" : "当玩家在战斗中为当前堆栈选择一个动作时开始倒计时。 堆栈操作完成后,它会重置为其初始值。",
+	"vcmi.optionsTab.chessFieldUnit.hover" : "单位计时器",
+	"vcmi.optionsTab.chessFieldBase.help" : "当 {回合计时器} 达到 0 时使用。在游戏开始时设置一次。达到 0 时,结束当前回合。任何正在进行的战斗都会以失败告终。",
+	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "在战斗外或{战斗计时器}耗尽时使用。 每回合重置。 剩余部分在回合结束时添加至 {基本计时器}。",
+	"vcmi.optionsTab.chessFieldTurnDiscard.help" : "在战斗外或{战斗计时器}耗尽时使用。 每回合重置。 任何未花费的时间都会丢失。",
+	"vcmi.optionsTab.chessFieldBattle.help" : "在与 AI 的战斗中使用,或者在 {单位计时器} 耗尽时用于 pvp 战斗。在每次战斗开始时重置。",
+	"vcmi.optionsTab.chessFieldUnitAccumulate.help" : "在 PVP 战斗中选择单位动作时使用。 在单位回合结束时将剩余物添加到{战斗计时器}。",
+	"vcmi.optionsTab.chessFieldUnitDiscard.help" : "在 PVP 战斗中选择单位动作时使用。 在每个单位回合开始时重置。 任何未花费的时间都会丢失。",
+	
+	"vcmi.optionsTab.accumulate" : "累积",
+
+	"vcmi.optionsTab.simturnsTitle" : "同时进行回合",
+	"vcmi.optionsTab.simturnsMin.hover" : "最少回合",
+	"vcmi.optionsTab.simturnsMax.hover" : "最多回合",
+	"vcmi.optionsTab.simturnsAI.hover" : "(测试中) AI回合同时行动",
+	"vcmi.optionsTab.simturnsMin.help" : "同时游戏进行的最少指定天数。在此期间玩家之间的联系将被阻止",
+	"vcmi.optionsTab.simturnsMax.help" : "同时游戏指定的最多天数或直到与其他玩家联系",
+	"vcmi.optionsTab.simturnsAI.help" : "{AI回合同时行动}\n实验选项。启用同时回合后,允许 AI 玩家与人类玩家同时行动。",
+	
+	"vcmi.optionsTab.turnTime.select"     : "回合计时器预设",
+	"vcmi.optionsTab.turnTime.unlimited"  : "无限时",
+	"vcmi.optionsTab.turnTime.classic.1"  : "经典计时器: 1 分钟",
+	"vcmi.optionsTab.turnTime.classic.2"  : "经典计时器: 2 分钟",
+	"vcmi.optionsTab.turnTime.classic.5"  : "经典计时器: 5 分钟",
+	"vcmi.optionsTab.turnTime.classic.10" : "经典计时器: 10 分钟",
+	"vcmi.optionsTab.turnTime.classic.20" : "经典计时器: 20 分钟",
+	"vcmi.optionsTab.turnTime.classic.30" : "经典计时器: 30 分钟",
+	"vcmi.optionsTab.turnTime.chess.20"   : "国际象棋计时器: 20:00 + 10:00 + 02:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.16"   : "国际象棋计时器: 16:00 + 08:00 + 01:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.8"    : "国际象棋计时器: 08:00 + 04:00 + 01:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.4"    : "国际象棋计时器: 04:00 + 02:00 + 00:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.2"    : "国际象棋计时器: 02:00 + 01:00 + 00:15 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.1"    : "国际象棋计时器: 01:00 + 01:00 + 00:00 + 00:00",
+	
+	"vcmi.optionsTab.simturns.select"         : "同时进行回合预设",
+	"vcmi.optionsTab.simturns.none"           : "不同时进行回合",
+	"vcmi.optionsTab.simturns.tillContactMax" : "同时进行: 可进行联系",
+	"vcmi.optionsTab.simturns.tillContact1"   : "同时进行: 1 周, 联系时中断",
+	"vcmi.optionsTab.simturns.tillContact2"   : "同时进行: 2 周, 联系时中断",
+	"vcmi.optionsTab.simturns.tillContact4"   : "同时进行: 1 月, 联系时中断",
+	"vcmi.optionsTab.simturns.blocked1"       : "同时进行: 1 周, 屏蔽联系",
+	"vcmi.optionsTab.simturns.blocked2"       : "同时进行: 2 周, 屏蔽联系",
+	"vcmi.optionsTab.simturns.blocked4"       : "同时进行: 1 月, 屏蔽联系",
+	
+	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
+	// Using this information, VCMI will automatically select correct plural form for every possible amount
+	"vcmi.optionsTab.simturns.days.0" : " %d 天",
+	"vcmi.optionsTab.simturns.days.1" : " %d 天",
+	"vcmi.optionsTab.simturns.days.2" : " %d 天",
+	"vcmi.optionsTab.simturns.weeks.0" : " %d 周",
+	"vcmi.optionsTab.simturns.weeks.1" : " %d 周",
+	"vcmi.optionsTab.simturns.weeks.2" : " %d 周",
+	"vcmi.optionsTab.simturns.months.0" : " %d 月",
+	"vcmi.optionsTab.simturns.months.1" : " %d 月",
+	"vcmi.optionsTab.simturns.months.2" : " %d 月",
+
 
 	// Custom victory conditions for H3 campaigns and HotA maps
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "敌人依然存活至今,你失败了!",
@@ -275,7 +367,7 @@
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "忽略防御 (${val}%)",
 	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "当攻击时,目标生物${val}%的防御力将被无视。",
 	"core.bonus.FIRE_IMMUNITY.name": "火系免疫",
-	"core.bonus.FIRE_IMMUNITY.description": "免疫所有火系魔法",
+	"core.bonus.FIRE_IMMUNITY.description": "免疫所有火系魔法",
 	"core.bonus.FIRE_SHIELD.name": "烈火神盾 (${val}%)",
 	"core.bonus.FIRE_SHIELD.description": "反弹部分受到的近战伤害",
 	"core.bonus.FIRST_STRIKE.name": "抢先反击",

+ 237 - 138
Mods/vcmi/config/vcmi/czech.json

@@ -29,16 +29,34 @@
 	"vcmi.capitalColors.5" : "Fialový",
 	"vcmi.capitalColors.6" : "Tyrkysový",
 	"vcmi.capitalColors.7" : "Růžový",
+
+	"vcmi.heroOverview.startingArmy" : "Počáteční jednotky",
+	"vcmi.heroOverview.warMachine" : "Bojové stroje",
+	"vcmi.heroOverview.secondarySkills" : "Druhotné schopnosti",
+	"vcmi.heroOverview.spells" : "Kouzla",
 	
 	"vcmi.radialWheel.mergeSameUnit" : "Sloučit stejné jednotky",
 	"vcmi.radialWheel.fillSingleUnit" : "Vyplnit jednou jednotkou",
 	"vcmi.radialWheel.splitSingleUnit" : "Rozdělit jedinou jednotku",
-	"vcmi.radialWheel.splitUnitEqually" : "Rovnoměrně rozdělit jednotky",
+	"vcmi.radialWheel.splitUnitEqually" : "Rozdělit jednotky rovnoměrně",
 	"vcmi.radialWheel.moveUnit" : "Přesunout jednotky do jiného oddílu",
 	"vcmi.radialWheel.splitUnit" : "Rozdělit jednotku do jiné pozice",
 
+	"vcmi.radialWheel.heroGetArmy" : "Získat armádu jiného hrdiny",
+	"vcmi.radialWheel.heroSwapArmy" : "Vyměnit armádu s jiným hrdinou",
+	"vcmi.radialWheel.heroExchange" : "Otevřít výměnu hrdinů",
+	"vcmi.radialWheel.heroGetArtifacts" : "Získat artefakty od jiního hrdiny",
+	"vcmi.radialWheel.heroSwapArtifacts" : "Vyměnit artefakty s jiným hrdinou",
+	"vcmi.radialWheel.heroDismiss" : "Propustit hrdinu",
+
+	"vcmi.radialWheel.moveTop" : "Move to top",
+	"vcmi.radialWheel.moveUp" : "Move up",
+	"vcmi.radialWheel.moveDown" : "Move down",
+	"vcmi.radialWheel.moveBottom" : "Move to bottom",
+
 	"vcmi.mainMenu.serverConnecting" : "Připojování...",
 	"vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:",
+	"vcmi.mainMenu.serverConnectionFailed" : "Připojování selhalo",
 	"vcmi.mainMenu.serverClosing" : "Zavírání...",
 	"vcmi.mainMenu.hostTCP" : "Pořádat hru TCP/IP",
 	"vcmi.mainMenu.joinTCP" : "Připojit se do hry TCP/IP",
@@ -46,10 +64,18 @@
 	
 	"vcmi.lobby.filepath" : "Název souboru",
 	"vcmi.lobby.creationDate" : "Datum vytvoření",
+	"vcmi.lobby.scenarioName" : "Název scénáře",
+	"vcmi.lobby.mapPreview" : "Náhled mapy",
+	"vcmi.lobby.noPreview" : "bez náhledu",
+	"vcmi.lobby.noUnderground" : "bez podzemí",
 
 	"vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.",
 	"vcmi.server.errors.modsToEnable"    : "{Následující modifikace jsou nutné pro načtení hry}",
+	"vcmi.server.errors.modsToDisable"   : "{Následující modifikace musí být zakázány}",
 	"vcmi.server.confirmReconnect"       : "Chcete se připojit k poslední relaci?",
+	"vcmi.server.errors.modNoDependency" : "Nelze načíst modifikaci {'%s'}!\n Závisí na modifikaci {'%s'}, která není aktivní!\n",
+	"vcmi.server.errors.modConflict" : "Nelze načíst modifikaci {'%s'}!\n Je v kolizi s aktivní modifikací {'%s'}!\n",
+	"vcmi.server.errors.unknownEntity" : "Nelze načíst uloženou pozici! Neznámá entita '%s' nalezena v uložené pozici! Uložná pozice nemusí být kompatibilní s aktuálními verzemi modifikací!",
 
 	"vcmi.settingsMainWindow.generalTab.hover" : "Obecné",
 	"vcmi.settingsMainWindow.generalTab.help"     : "Přepne na kartu obecných nastavení, která obsahuje nastavení související s obecným chováním klienta hry",
@@ -84,6 +110,10 @@
 	"vcmi.systemOptions.framerateButton.help"   : "{Zobrazit FPS}\n\nPřepne viditelnost počitadla snímků za sekundu v rohu obrazovky hry",
 	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Vibrace",
 	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Vibrace}\n\nPřepnout stav vibrací při dotykovém ovládání",
+	"vcmi.systemOptions.enableUiEnhancementsButton.hover"  : "Vylepšení rozhraní",
+	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Vylepšení rozhraní}\n\nZapne různá vylepšení rozhraní, jako je tlačítko batohu atd. Zakažte pro zážitek klasické hry.",
+	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "Velká kniha kouzel",
+	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Velká kniha kouzel}\n\nPovolí větší knihu kouzel, do které se jich více vleze na jednu stranu. Animace změny stránek s tímto nastavením nefunguje.",
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "Zobrazit zprávy v panelu informací",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Zobrazit zprávy v panelu informací}\n\nKdyž bude možné, herní zprávy z návštěv míst na mapě budou zobrazeny v panelu informací místo ve zvláštním okně.",
@@ -97,8 +127,8 @@
 	"vcmi.adventureOptions.borderScroll.help" : "{Posouvání okraji}\n\nPosouvat mapu světa, když je kurzor na okraji obrazovky. Může být zakázáno držením klávesy CTRL.",
 	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info Panel Creature Management", //TODO
 	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info Panel Creature Management}\n\nAllows rearranging creatures in info panel instead of cycling between default components",
-	"vcmi.adventureOptions.leftButtonDrag.hover" : "Left Click Drag Map",
-	"vcmi.adventureOptions.leftButtonDrag.help" : "{Left Click Drag Map}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view",
+	"vcmi.adventureOptions.leftButtonDrag.hover" : "Posouvání mapy levým kliknutím",
+	"vcmi.adventureOptions.leftButtonDrag.help" : "{Posouvání mapy levým kliknutím}\n\nPosouvání mapy tažením myši se stisknutým levým tlačítkem",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
@@ -129,6 +159,8 @@
 	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Zobrazit okno statistik hrdinů}\n\nTrvale zapne okno statistiky hrdinů, které ukazuje hlavní schopnosti a magickou energii.",
 	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Přeskočit úvodní hudbu",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Přeskočit úvodní hudbu}\n\nPovolí akce při úvodní hudbě přehrávané při začátku každé bitvy.",
+	"vcmi.adventureMap.revisitObject.hover" : "Znovu navštívit místo",
+	"vcmi.adventureMap.revisitObject.help" : "{Znovu navštívit místo}\n\nPokud se hrdina nachází na nějakém místě mapy, může jej znovu navštívit.",
 
 	"vcmi.battleWindow.pressKeyToSkipIntro" : "Stiskněte jakoukoliv klávesu pro okamžité zahájení bitvy",
 	"vcmi.battleWindow.damageEstimation.melee" : "Zaútočit na %CREATURE (%DAMAGE).",
@@ -144,6 +176,15 @@
 
 	"vcmi.battleResultsWindow.applyResultsLabel" : "Použít výsledek bitvy",
 
+	"vcmi.tutorialWindow.title" : "Úvod ovládání dotykem",
+	"vcmi.tutorialWindow.decription.RightClick" : "Klepněte a držte prvek, na který byste chtěli použít pravé tlačítko myši. Klepněte na volnou oblast pro zavření.",
+	"vcmi.tutorialWindow.decription.MapPanning" : "Klepněte a držte jedním prstem pro posouvání mapy.",
+	"vcmi.tutorialWindow.decription.MapZooming" : "Přibližte dva prsty k sobě pro přiblížení mapy.",
+	"vcmi.tutorialWindow.decription.RadialWheel" : "Přejetí otevře kruhovou nabídku pro různé akce, třeba správa hrdiny/bojovnínků a příkazy měst.",
+	"vcmi.tutorialWindow.decription.BattleDirection" : "Pro útok ze speifického úhlu, přejeďte směrem, ze kterého má být útok vykonán.",
+	"vcmi.tutorialWindow.decription.BattleDirectionAbort" : "Gesto útoku pod úhlem může být zrušeno, pokud, pokud je prst dostatečně daleko.",
+	"vcmi.tutorialWindow.decription.AbortSpell" : "Klepněte a držte pro zrušení kouzla.",
+
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Zobrazit dostupné jednotky",
 	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Zobrazit dostupné jednotky}\n\nZobrazit počet jednotek dostupných ke koupení místo jejich týdenního přírůstku v přehledu města. (levý spodní okraj obrazovky města).",
 	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Zobrazit týdenní přírůstek jednotek",
@@ -152,18 +193,18 @@
 	"vcmi.otherOptions.compactTownCreatureInfo.help": "{Kompaktní informace o jednotkách}\n\nZobrazit menší informace o jednotkách města v jeho přehledu (levý spodní okraj obrazovky města).",
 
 	"vcmi.townHall.missingBase"             : "Základní budova %s musí být postavena jako první",
-	"vcmi.townHall.noCreaturesToRecruit"    : "Žádné jednotky k vycvičení!", //TODO
-	"vcmi.townHall.greetingManaVortex"      : "As you near the %s your body is filled with new energy. You have doubled your normal spell points.",
-	"vcmi.townHall.greetingKnowledge"       : "You study the glyphs on the %s and gain insight into the workings of various magics (+1 Knowledge).",
+	"vcmi.townHall.noCreaturesToRecruit"    : "Žádné jednotky k vycvičení!",
+	"vcmi.townHall.greetingManaVortex"      : "Při pobytu u místa %s se vaše tělo naplnilo novou energií. Máte dvojnásobné množství maximální magické energie.",
+	"vcmi.townHall.greetingKnowledge"       : "Studujete glyfy na the %s a porozumíte fungování různých magií (+1 Znalosti).",
 	"vcmi.townHall.greetingSpellPower"      : "%s vás učí nové cesty zaměření vaší magické síly (+1 Síla kouzel).",
 	"vcmi.townHall.greetingExperience"      : "Návštěva %s vás naučila spoustu nových dovedností (+1000 zkušeností).",
-	"vcmi.townHall.greetingAttack"          : "Čas strávený poblíž místa zvaného %s allows you to learn more effective combat skills (+1 Attack Skill).",
-	"vcmi.townHall.greetingDefence"         : "Spending time in the %s, the experienced warriors therein teach you additional defensive skills (+1 Defense).",
-	"vcmi.townHall.hasNotProduced"          : "%s zatím nic nevyrobil.",
-	"vcmi.townHall.hasProduced"             : "%s vyrobil %d %s tento týden.",
+	"vcmi.townHall.greetingAttack"          : "Čas strávený poblíž místa zvaného %s vám dovolil se naučit efektivnější bojové dovednosti (+1 Útočná síla).",
+	"vcmi.townHall.greetingDefence"         : "Trávíte čas na místě zvaném %s, zkušení bojovníci vás u toho naučili nové metody obrany (+1 Obranná síla).",
+	"vcmi.townHall.hasNotProduced"          : "%s - zatím nic nevyrobeno.",
+	"vcmi.townHall.hasProduced"             : "%s - vyrobeno %d %s tento týden.",
 	"vcmi.townHall.greetingCustomBonus"     : "%s vám dává +%d %s%s",
 	"vcmi.townHall.greetingCustomUntil"     : " do další bitvy.",
-	"vcmi.townHall.greetingInTownMagicWell" : "%s obnovil na maximum vaši magickou energii.",
+	"vcmi.townHall.greetingInTownMagicWell" : "%s - obnoveno na maximum vaši magickou energii.",
 
 	"vcmi.logicalExpressions.anyOf"  : "Něco z následujících:",
 	"vcmi.logicalExpressions.allOf"  : "Všechny následující:",
@@ -174,17 +215,17 @@
 	"vcmi.heroWindow.openBackpack.hover" : "Open artifact backpack window",
 	"vcmi.heroWindow.openBackpack.help"  : "Opens window that allows easier artifact backpack management",
 
-	"vcmi.commanderWindow.artifactMessage" : "Do you want to return this artifact to the hero?",
+	"vcmi.commanderWindow.artifactMessage" : "Chcete navrátit tento artefakt hrdinovi?",
 
-	"vcmi.creatureWindow.showBonuses.hover"    : "Switch to bonuses view",
+	"vcmi.creatureWindow.showBonuses.hover"    : "Přepnout na zobrazení bonusů",
 	"vcmi.creatureWindow.showBonuses.help"     : "Display all active bonuses of the commander",
-	"vcmi.creatureWindow.showSkills.hover"     : "Switch to skills view",
+	"vcmi.creatureWindow.showSkills.hover"     : "Přepnout na zobrazení schoostí",
 	"vcmi.creatureWindow.showSkills.help"      : "Display all learned skills of the commander",
 	"vcmi.creatureWindow.returnArtifact.hover" : "Vrátit artefakt",
 	"vcmi.creatureWindow.returnArtifact.help"  : "Klikněte na toto tlačítko pro navrácení artefaktů do hrdinova batohu",
 
-	"vcmi.questLog.hideComplete.hover" : "Hide complete quests",
-	"vcmi.questLog.hideComplete.help"  : "Hide all completed quests",
+	"vcmi.questLog.hideComplete.hover" : "Skrýt dokončené úkoly",
+	"vcmi.questLog.hideComplete.help"  : "Skrýt všechny dokončené úkoly",
 
 	"vcmi.randomMapTab.widgets.randomTemplate"      : "(Random)",
 	"vcmi.randomMapTab.widgets.templateLabel"        : "Šablona",
@@ -192,9 +233,67 @@
 	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Team Alignments",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Druhy cest",
 
-	"vcmi.optionsTab.widgets.labelTimer" : "Časovač",
-	"vcmi.optionsTab.widgets.timerModeSwitch.classic" : "Klasický časovač",
-	"vcmi.optionsTab.widgets.timerModeSwitch.chess" : "Šachový časovač",
+	"vcmi.optionsTab.turnOptions.hover" : "Možnosti tahu",
+	"vcmi.optionsTab.turnOptions.help" : "Vyberte odpočítávadlo tahů a nastavení souběžných tahů",
+	"vcmi.optionsTab.selectPreset" : "Preset",
+
+	"vcmi.optionsTab.chessFieldBase.hover" : "Base timer",
+	"vcmi.optionsTab.chessFieldTurn.hover" : "Turn timer",
+	"vcmi.optionsTab.chessFieldBattle.hover" : "Battle timer",
+	"vcmi.optionsTab.chessFieldUnit.hover" : "Unit timer",
+	"vcmi.optionsTab.chessFieldBase.help" : "Used when {Turn Timer} reaches 0. Set once at game start. On reaching zero, ends current turn. Any ongoing combat with end with a loss.",
+	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Leftover added to {Base Timer} at turn's end.",
+	"vcmi.optionsTab.chessFieldTurnDiscard.help" : "Used out of combat or when {Battle Timer} runs out. Reset each turn. Any unspent time is lost",
+	"vcmi.optionsTab.chessFieldBattle.help" : "Used in battles with AI or in pvp combat when {Unit Timer} runs out. Reset at start of each combat.",
+	"vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Used when selecting unit action in pvp combat. Leftover added to {Battle Timer} at end of unit turn.",
+	"vcmi.optionsTab.chessFieldUnitDiscard.help" : "Used when selecting unit action in pvp combat. Reset at start of each unit's turn. Any unspent time is lost",
+
+	"vcmi.optionsTab.accumulate" : "Accumulate",
+
+	"vcmi.optionsTab.simturnsTitle" : "Souběžné tahy",
+	"vcmi.optionsTab.simturnsMin.hover" : "At least for",
+	"vcmi.optionsTab.simturnsMax.hover" : "At most for",
+	"vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Simultaneous AI Turns",
+	"vcmi.optionsTab.simturnsMin.help" : "Play simultaneously for specified number of days. Contacts between players during this period are blocked",
+	"vcmi.optionsTab.simturnsMax.help" : "Play simultaneously for specified number of days or until contact with another player",
+	"vcmi.optionsTab.simturnsAI.help" : "{Simultaneous AI Turns}\nExperimental option. Allows AI players to act at the same time as human player when simultaneous turns are enabled.",
+
+	"vcmi.optionsTab.turnTime.select"     : "Select turn timer preset",
+	"vcmi.optionsTab.turnTime.unlimited"  : "Unlimited turn time",
+	"vcmi.optionsTab.turnTime.classic.1"  : "Classic timer: 1 minute",
+	"vcmi.optionsTab.turnTime.classic.2"  : "Classic timer: 2 minutes",
+	"vcmi.optionsTab.turnTime.classic.5"  : "Classic timer: 5 minutes",
+	"vcmi.optionsTab.turnTime.classic.10" : "Classic timer: 10 minutes",
+	"vcmi.optionsTab.turnTime.classic.20" : "Classic timer: 20 minutes",
+	"vcmi.optionsTab.turnTime.classic.30" : "Classic timer: 30 minutes",
+	"vcmi.optionsTab.turnTime.chess.20"   : "Chess: 20:00 + 10:00 + 02:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.16"   : "Chess: 16:00 + 08:00 + 01:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.8"    : "Chess: 08:00 + 04:00 + 01:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.4"    : "Chess: 04:00 + 02:00 + 00:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.2"    : "Chess: 02:00 + 01:00 + 00:15 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.1"    : "Chess: 01:00 + 01:00 + 00:00 + 00:00",
+
+	"vcmi.optionsTab.simturns.select"         : "Select simultaneous turns preset",
+	"vcmi.optionsTab.simturns.none"           : "No simultaneous turns",
+	"vcmi.optionsTab.simturns.tillContactMax" : "Simturns: Until contact",
+	"vcmi.optionsTab.simturns.tillContact1"   : "Simturns: 1 week, break on contact",
+	"vcmi.optionsTab.simturns.tillContact2"   : "Simturns: 2 weeks, break on contact",
+	"vcmi.optionsTab.simturns.tillContact4"   : "Simturns: 1 month, break on contact",
+	"vcmi.optionsTab.simturns.blocked1"       : "Simturns: 1 week, contacts blocked",
+	"vcmi.optionsTab.simturns.blocked2"       : "Simturns: 2 weeks, contacts blocked",
+	"vcmi.optionsTab.simturns.blocked4"       : "Simturns: 1 month, contacts blocked",
+	
+	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
+	// Using this information, VCMI will automatically select correct plural form for every possible amount
+	"vcmi.optionsTab.simturns.days.0" : " %d dní",
+	"vcmi.optionsTab.simturns.days.1" : " %d den",
+	"vcmi.optionsTab.simturns.days.2" : " %d dny",
+	"vcmi.optionsTab.simturns.weeks.0" : " %d týdnů",
+	"vcmi.optionsTab.simturns.weeks.1" : " %d týden",
+	"vcmi.optionsTab.simturns.weeks.2" : " %d týdny",
+	"vcmi.optionsTab.simturns.months.0" : " %d měsíců",
+	"vcmi.optionsTab.simturns.months.1" : " %d měsíc",
+	"vcmi.optionsTab.simturns.months.2" : " %d měsíce",
 
 	// Custom victory conditions for H3 campaigns and HotA maps
 	"vcmi.map.victoryCondition.daysPassed.toOthers" : "Nepřítel zvládl přežít do této chvíle. Vítězství je jeho!",
@@ -203,162 +302,162 @@
 	"vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Gratulace! Porazili jste všechny nepřátele zamořující tuto zemi a můžete si nárokovat vítězství!",
 	"vcmi.map.victoryCondition.collectArtifacts.message" : "Získejte tři artefakty",
 	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Gratulace! Všichni vaši nepřítelé byli poraženi a máte Andělskou alianci! Vítězství je vaše!",
-	"vcmi.map.victoryCondition.angelicAlliance.message" : "Porazte všechny nepřátele a vyrobte Andělskou alianci",
+	"vcmi.map.victoryCondition.angelicAlliance.message" : "Porazte všechny nepřátele a utužte Andělskou alianci",
 
 	// few strings from WoG used by vcmi
-	"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" : "Basic",
-	"vcmi.stackExperience.rank.1" : "Novice",
-	"vcmi.stackExperience.rank.2" : "Trained",
-	"vcmi.stackExperience.rank.3" : "Skilled",
-	"vcmi.stackExperience.rank.4" : "Proven",
-	"vcmi.stackExperience.rank.5" : "Veteran",
+	"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", //TODO
+	"vcmi.stackExperience.rank.0" : "Začátečník",
+	"vcmi.stackExperience.rank.1" : "Učeň",
+	"vcmi.stackExperience.rank.2" : "Trénovaný",
+	"vcmi.stackExperience.rank.3" : "Zručný",
+	"vcmi.stackExperience.rank.4" : "Prověřený",
+	"vcmi.stackExperience.rank.5" : "Veterán",
 	"vcmi.stackExperience.rank.6" : "Adept",
 	"vcmi.stackExperience.rank.7" : "Expert",
-	"vcmi.stackExperience.rank.8" : "Elite",
+	"vcmi.stackExperience.rank.8" : "Elit",
 	"vcmi.stackExperience.rank.9" : "Master",
-	"vcmi.stackExperience.rank.10" : "Ace",
+	"vcmi.stackExperience.rank.10" : "Eso",
 	
 	"core.bonus.ADDITIONAL_ATTACK.name": "Dvojitý úder",
 	"core.bonus.ADDITIONAL_ATTACK.description": "Útočí dvakrát",
-	"core.bonus.ADDITIONAL_RETALIATION.name": "Additional retaliations",
-	"core.bonus.ADDITIONAL_RETALIATION.description": "May retaliate ${val} extra times",
+	"core.bonus.ADDITIONAL_RETALIATION.name": "Další odveta",
+	"core.bonus.ADDITIONAL_RETALIATION.description": "Může zaútočit zpět navíc ${val}x",
 	"core.bonus.AIR_IMMUNITY.name": "Vzdušná odolnost",
-	"core.bonus.AIR_IMMUNITY.description": "Immune to all spells from the school of Air magic",
+	"core.bonus.AIR_IMMUNITY.description": "Imunní všem kouzlům školy vzdušné magie",
 	"core.bonus.ATTACKS_ALL_ADJACENT.name": "Útok okolo",
 	"core.bonus.ATTACKS_ALL_ADJACENT.description": "Útočí na všechny sousední jednotky",
-	"core.bonus.BLOCKS_RETALIATION.name": "No retaliation",
-	"core.bonus.BLOCKS_RETALIATION.description": "Enemy cannot retaliate",
-	"core.bonus.BLOCKS_RANGED_RETALIATION.name": "No ranged retaliation",
-	"core.bonus.BLOCKS_RANGED_RETALIATION.description": "Enemy cannot retaliate by using a ranged attack",
+	"core.bonus.BLOCKS_RETALIATION.name": "Žádná odplata",
+	"core.bonus.BLOCKS_RETALIATION.description": "Nepřítel nemůže zaútočit zpět",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.name": "Žádná odplata na dálku",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.description": "Nepřítel nemůže zaútočit zpět útokem na dálku",
 	"core.bonus.CATAPULT.name": "Katapult",
-	"core.bonus.CATAPULT.description": "Attacks siege walls",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Reduce Casting Cost (${val})",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Reduces the spellcasting cost for the hero by ${val}",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Magic Damper (${val})",
-	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Increases spellcasting cost of enemy spells by ${val}",
-	"core.bonus.CHARGE_IMMUNITY.name": "Immune to Charge",
+	"core.bonus.CATAPULT.description": "Útočí na ochranné hradby",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name": "Snížit cenu kouzel (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description": "Snižuje cenu energie hrdiny o ${val}",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name": "Tlumič magie (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description": "Zvyšuje cenu energie kouzlení nepřítele o ${val}",
+	"core.bonus.CHARGE_IMMUNITY.name": "Immune to Charge", // TODO
 	"core.bonus.CHARGE_IMMUNITY.description": "Immune to Cavalier's and Champion's Charge",
-	"core.bonus.DARKNESS.name": "Darkness cover",
-	"core.bonus.DARKNESS.description": "Creates a shroud of darkness with a ${val} radius",
-	"core.bonus.DEATH_STARE.name": "Death Stare (${val}%)",
-	"core.bonus.DEATH_STARE.description": "Has a ${val}% chance to kill a single creature",
-	"core.bonus.DEFENSIVE_STANCE.name": "Defense Bonus",
-	"core.bonus.DEFENSIVE_STANCE.description": "+${val} Defense when defending",
-	"core.bonus.DESTRUCTION.name": "Destruction",
-	"core.bonus.DESTRUCTION.description": "Has ${val}% chance to kill extra units after attack",
-	"core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Death Blow",
-	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Has a ${val}% chance of dealing double base damage when attacking",
+	"core.bonus.DARKNESS.name": "Závoj temnoty",
+	"core.bonus.DARKNESS.description": "Vytvoří clonu temnoty v oblasti ${val} polí",
+	"core.bonus.DEATH_STARE.name": "Smrtící pohled (${val}%)",
+	"core.bonus.DEATH_STARE.description": "Má ${val}% šanci zabít jednu creature",
+	"core.bonus.DEFENSIVE_STANCE.name": "Obranný bonus",
+	"core.bonus.DEFENSIVE_STANCE.description": "+${val} obranné síly při obraně",
+	"core.bonus.DESTRUCTION.name": "Zničení",
+	"core.bonus.DESTRUCTION.description": "Má ${val}% šanci zabít další jednotky po útoku",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.name": "Smrtící rána",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.description": "Má ${val}% šanci na udělení dvojnásobného základního poškození při útoku",
 	"core.bonus.DRAGON_NATURE.name": "Drak",
-	"core.bonus.DRAGON_NATURE.description": "Creature has a Dragon Nature",
+	"core.bonus.DRAGON_NATURE.description": "Jednotka má povahu draka",
 	"core.bonus.EARTH_IMMUNITY.name": "Zemní odolnost",
-	"core.bonus.EARTH_IMMUNITY.description": "Immune to all spells from the school of Earth magic",
-	"core.bonus.ENCHANTER.name": "Enchanter",
-	"core.bonus.ENCHANTER.description": "Can cast mass ${subtype.spell} every turn",
-	"core.bonus.ENCHANTED.name": "Enchanted",
-	"core.bonus.ENCHANTED.description": "Affected by permanent ${subtype.spell}",
-	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Ignore Defense (${val}%)",
-	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "When attacking, ${val}% of the defender's defense is ignored",
+	"core.bonus.EARTH_IMMUNITY.description": "Imunní všem kouzlům školy zemské magie",
+	"core.bonus.ENCHANTER.name": "Zaklínač",
+	"core.bonus.ENCHANTER.description": "Může masově seslat ${subtype.spell} každý tah",
+	"core.bonus.ENCHANTED.name": "Očarovaný",
+	"core.bonus.ENCHANTED.description": "Trvale ovlivněm kouzlem ${subtype.spell}",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.name": "Nevšímá si ${val} % bodů obrany",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.description": "Pří útoku nebude brát v potaz ${val}% bodů obrany obránce",
 	"core.bonus.FIRE_IMMUNITY.name": "Ohnivá odolnost",
-	"core.bonus.FIRE_IMMUNITY.description": "Immune to all spells from the school of Fire magic",
+	"core.bonus.FIRE_IMMUNITY.description": "Imunní všem kouzlům školy ohnivé magie",
 	"core.bonus.FIRE_SHIELD.name": "Ohnivý štít (${val}%)",
-	"core.bonus.FIRE_SHIELD.description": "Reflects part of melee damage",
-	"core.bonus.FIRST_STRIKE.name": "First Strike",
-	"core.bonus.FIRST_STRIKE.description": "This creature retaliates before being attacked",
-	"core.bonus.FEAR.name": "Fear",
-	"core.bonus.FEAR.description": "Causes Fear on an enemy stack",
+	"core.bonus.FIRE_SHIELD.description": "Odrazí část zranení útoku zblízka",
+	"core.bonus.FIRST_STRIKE.name": "První úder",
+	"core.bonus.FIRST_STRIKE.description": "Tato jednotka útočí zpět ještě než je na ni zaútočeno",
+	"core.bonus.FEAR.name": "Strach",
+	"core.bonus.FEAR.description": "Způsobí strach nepřátelskému oddílu",
 	"core.bonus.FEARLESS.name": "Nebojácnost",
 	"core.bonus.FEARLESS.description": "Odolnost proti strachu",
 	"core.bonus.FLYING.name": "Letec",
 	"core.bonus.FLYING.description": "Při pohybu létá (přes překážky)",
 	"core.bonus.FREE_SHOOTING.name": "Blízké výstřely",
 	"core.bonus.FREE_SHOOTING.description": "Může použít výstřely při útoku zblízka",
-	"core.bonus.GARGOYLE.name": "Gargoyle",
-	"core.bonus.GARGOYLE.description": "Cannot be raised or healed",
-	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Reduce Damage (${val}%)",
-	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Reduces physical damage from ranged or melee attacks",
+	"core.bonus.GARGOYLE.name": "Chrlič",
+	"core.bonus.GARGOYLE.description": "Cannot be raised or healed", // TODO
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.name": "Snižuje poškození (${val}%)",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.description": "Snižuje poškození od útoků z dálky a blízka",
 	"core.bonus.HATE.name": "Nesnáší ${subtype.creature}",
-	"core.bonus.HATE.description": "Does ${val}% more damage to ${subtype.creature}",
+	"core.bonus.HATE.description": "Dává o ${val} % větší zranění jednotce ${subtype.creature}",
 	"core.bonus.HEALER.name": "Léčitel",
 	"core.bonus.HEALER.description": "Léčí spojenecké jednotky",
 	"core.bonus.HP_REGENERATION.name": "Regenerace",
 	"core.bonus.HP_REGENERATION.description": "Každé kolo léčí ${val} životů",
-	"core.bonus.JOUSTING.name": "Champion charge",
-	"core.bonus.JOUSTING.description": "+${val}% damage for each hex travelled",
+	"core.bonus.JOUSTING.name": "Nabití šampiona",
+	"core.bonus.JOUSTING.description": "+${val}% poškození za každé projité pole",
 	"core.bonus.KING.name": "Král",
-	"core.bonus.KING.description": "Vulnerable to SLAYER level ${val} or higher",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Spell immunity 1-${val}",
-	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Immune to spells of levels 1-${val}",
-	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Limited shooting range",
-	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Unable to target units farther than ${val} hexes",
+	"core.bonus.KING.description": "Zranitelný zabijákovi úrovně ${val} a vyšší",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.name": "Odolnost kouzel 1-${val}",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.description": "Odolnost vůči kouzlům úrovní 1-${val}",
+	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Omezený dosah střelby",
+	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Nevystřelí na jednotky dále než ${val} polí",
 	"core.bonus.LIFE_DRAIN.name": "Vysátí životů (${val}%)",
 	"core.bonus.LIFE_DRAIN.description": "Vysaje ${val}% uděleného poškození",
-	"core.bonus.MANA_CHANNELING.name": "Magic Channel ${val}%",
-	"core.bonus.MANA_CHANNELING.description": "Gives your hero ${val}% of the mana spent by the enemy",
+	"core.bonus.MANA_CHANNELING.name": "Magic Channel ${val}%", // TODO
+	"core.bonus.MANA_CHANNELING.description": "Dá vašemu hrdinovi ${val} % many využité nepřítelem",
 	"core.bonus.MANA_DRAIN.name": "Vysátí many",
 	"core.bonus.MANA_DRAIN.description": "Každé kolo vysaje ${val} many",
 	"core.bonus.MAGIC_MIRROR.name": "Kouzelné zrcadlo (${val}%)",
-	"core.bonus.MAGIC_MIRROR.description": "Has a ${val}% chance to redirect an offensive spell to an enemy unit",
-	"core.bonus.MAGIC_RESISTANCE.name": "Magic Resistance (${val}%)",
-	"core.bonus.MAGIC_RESISTANCE.description": "Has a ${val}% chance to resist an enemy spell",
-	"core.bonus.MIND_IMMUNITY.name": "Mind Spell Immunity",
-	"core.bonus.MIND_IMMUNITY.description": "Immune to Mind-type spells",
-	"core.bonus.NO_DISTANCE_PENALTY.name": "No distance penalty",
-	"core.bonus.NO_DISTANCE_PENALTY.description": "Does full damage at any distance",
-	"core.bonus.NO_MELEE_PENALTY.name": "No melee penalty",
-	"core.bonus.NO_MELEE_PENALTY.description": "Creature has no Melee Penalty",
-	"core.bonus.NO_MORALE.name": "Neutral Morale",
-	"core.bonus.NO_MORALE.description": "Creature is immune to morale effects",
-	"core.bonus.NO_WALL_PENALTY.name": "No wall penalty",
-	"core.bonus.NO_WALL_PENALTY.description": "Full damage during siege",
-	"core.bonus.NON_LIVING.name": "Non living",
-	"core.bonus.NON_LIVING.description": "Immunity to many effects",
-	"core.bonus.RANDOM_SPELLCASTER.name": "Random spellcaster",
-	"core.bonus.RANDOM_SPELLCASTER.description": "Can cast random spell",
-	"core.bonus.RANGED_RETALIATION.name": "Ranged retaliation",
-	"core.bonus.RANGED_RETALIATION.description": "Can perform ranged counterattack",
-	"core.bonus.RECEPTIVE.name": "Receptive",
-	"core.bonus.RECEPTIVE.description": "No Immunity to Friendly Spells",
-	"core.bonus.REBIRTH.name": "Rebirth (${val}%)",
-	"core.bonus.REBIRTH.description": "${val}% of stack will rise after death",
-	"core.bonus.RETURN_AFTER_STRIKE.name": "Attack and Return",
-	"core.bonus.RETURN_AFTER_STRIKE.description": "Returns after melee attack",
-	"core.bonus.SHOOTER.name": "Ranged",
-	"core.bonus.SHOOTER.description": "Creature can shoot",
-	"core.bonus.SHOOTS_ALL_ADJACENT.name": "Shoot all around",
-	"core.bonus.SHOOTS_ALL_ADJACENT.description": "This creature's ranged attacks strike all targets in a small area",
-	"core.bonus.SOUL_STEAL.name": "Soul Steal",
-	"core.bonus.SOUL_STEAL.description": "Gains ${val} new creatures for each enemy killed",
-	"core.bonus.SPELLCASTER.name": "Spellcaster",
-	"core.bonus.SPELLCASTER.description": "Can cast ${subtype.spell}",
-	"core.bonus.SPELL_AFTER_ATTACK.name": "Cast After Attack",
-	"core.bonus.SPELL_AFTER_ATTACK.description": "Has a ${val}% chance to cast ${subtype.spell} after it attacks",
-	"core.bonus.SPELL_BEFORE_ATTACK.name": "Cast Before Attack",
-	"core.bonus.SPELL_BEFORE_ATTACK.description": "Has a ${val}% chance to cast ${subtype.spell} before it attacks",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Spell Resistance",
-	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Damage from spells reduced by ${val}%.",
+	"core.bonus.MAGIC_MIRROR.description": "Má ${val}% šanci odrazit útočné kouzlo na nepřátelskou jednotku",
+	"core.bonus.MAGIC_RESISTANCE.name": "Magická odolnost (${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description": "Má ${val}% šanci ustát nepřátelské kouzlo",
+	"core.bonus.MIND_IMMUNITY.name": "Imunita kouzel mysli",
+	"core.bonus.MIND_IMMUNITY.description": "Imunní vůči kouzlům cílícím na mysl",
+	"core.bonus.NO_DISTANCE_PENALTY.name": "Bez penalizace vzdálenosti",
+	"core.bonus.NO_DISTANCE_PENALTY.description": "Plné poškození na jakoukoliv vzdálenost",
+	"core.bonus.NO_MELEE_PENALTY.name": "Bez penalizace útoku zblízka",
+	"core.bonus.NO_MELEE_PENALTY.description": "Jednotka není penalizována za útok zblízka",
+	"core.bonus.NO_MORALE.name": "Neutrální morálka",
+	"core.bonus.NO_MORALE.description": "Jednotka je imunní vůči efektu morálky",
+	"core.bonus.NO_WALL_PENALTY.name": "Bez penalizace hradbami",
+	"core.bonus.NO_WALL_PENALTY.description": "Plné poškození při obléhání",
+	"core.bonus.NON_LIVING.name": "Neživoucí",
+	"core.bonus.NON_LIVING.description": "Imunní vůči mnohým efektům",
+	"core.bonus.RANDOM_SPELLCASTER.name": "Náhodný kouzelník",
+	"core.bonus.RANDOM_SPELLCASTER.description": "Může seslat náhodné kouzlo",
+	"core.bonus.RANGED_RETALIATION.name": "Vzdálená msta",
+	"core.bonus.RANGED_RETALIATION.description": "Může provést protiútok na dálku",
+	"core.bonus.RECEPTIVE.name": "Přijímavý",
+	"core.bonus.RECEPTIVE.description": "Není imunní vůči přátelským kouzlům",
+	"core.bonus.REBIRTH.name": "Znovuzrození (${val}%)",
+	"core.bonus.REBIRTH.description": "${val}% oddílu se po smrti znovu narodí",
+	"core.bonus.RETURN_AFTER_STRIKE.name": "Útok a návrat",
+	"core.bonus.RETURN_AFTER_STRIKE.description": "Navrátí se po útoku na blízko",
+	"core.bonus.SHOOTER.name": "Střelec",
+	"core.bonus.SHOOTER.description": "Jednotka může střílet",
+	"core.bonus.SHOOTS_ALL_ADJACENT.name": "Střílí okolo",
+	"core.bonus.SHOOTS_ALL_ADJACENT.description": "Vzdálené útoky této jednotky zasáhnou všechny cíle v malé oblasti",
+	"core.bonus.SOUL_STEAL.name": "Zloděj duší",
+	"core.bonus.SOUL_STEAL.description": "Získá ${val} nových jednotek za každého zabitého nepřítele",
+	"core.bonus.SPELLCASTER.name": "Kouzelník",
+	"core.bonus.SPELLCASTER.description": "Může seslat ${subtype.spell}",
+	"core.bonus.SPELL_AFTER_ATTACK.name": "Kouzlení po útoku",
+	"core.bonus.SPELL_AFTER_ATTACK.description": "Má ${val}% šanci seslat ${subtype.spell} po zaútočení",
+	"core.bonus.SPELL_BEFORE_ATTACK.name": "Kouzlení před útokem",
+	"core.bonus.SPELL_BEFORE_ATTACK.description": "Má ${val}% šanci seslat ${subtype.spell} před zaútočením",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name": "Magická odolnost",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description": "Zranění od kouzel sníženo o ${val}%.",
 	"core.bonus.SPELL_IMMUNITY.name": "Odolnost vůči kouzlům",
 	"core.bonus.SPELL_IMMUNITY.description": "Odolnost proti ${subtype.spell}",
-	"core.bonus.SPELL_LIKE_ATTACK.name": "Spell-like attack",
-	"core.bonus.SPELL_LIKE_ATTACK.description": "Attacks with ${subtype.spell}",
-	"core.bonus.SPELL_RESISTANCE_AURA.name": "Aura of Resistance",
-	"core.bonus.SPELL_RESISTANCE_AURA.description": "Nearby stacks get ${val}% magic resistance",
-	"core.bonus.SUMMON_GUARDIANS.name": "Summon guardians",
-	"core.bonus.SUMMON_GUARDIANS.description": "At the start of battle summons ${subtype.creature} (${val}%)",
-	"core.bonus.SYNERGY_TARGET.name": "Synergizable",
+	"core.bonus.SPELL_LIKE_ATTACK.name": "Útok kouzlem",
+	"core.bonus.SPELL_LIKE_ATTACK.description": "Útočí kouzlem ${subtype.spell}",
+	"core.bonus.SPELL_RESISTANCE_AURA.name": "Aura odolnosti",
+	"core.bonus.SPELL_RESISTANCE_AURA.description": "Oddíly poblíž získají ${val}% magickou odolnost",
+	"core.bonus.SUMMON_GUARDIANS.name": "Povolat strážce",
+	"core.bonus.SUMMON_GUARDIANS.description": "Na začátku bitvy povolá ${subtype.creature} (${val}%)",
+	"core.bonus.SYNERGY_TARGET.name": "Synergizable", // TODO
 	"core.bonus.SYNERGY_TARGET.description": "This creature is vulnerable to synergy effect",
-	"core.bonus.TWO_HEX_ATTACK_BREATH.name": "Breath",
-	"core.bonus.TWO_HEX_ATTACK_BREATH.description": "Breath Attack (2-hex range)",
-	"core.bonus.THREE_HEADED_ATTACK.name": "Three-headed attack",
-	"core.bonus.THREE_HEADED_ATTACK.description": "Attacks three adjacent units",
-	"core.bonus.TRANSMUTATION.name": "Transmutation",
-	"core.bonus.TRANSMUTATION.description": "${val}% chance to transform attacked unit to a different type",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.name": "Dech",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.description": "Dechový útok (dosah do dvou polí)",
+	"core.bonus.THREE_HEADED_ATTACK.name": "Tříhlavý útok",
+	"core.bonus.THREE_HEADED_ATTACK.description": "Útočí na tři sousední jednotky",
+	"core.bonus.TRANSMUTATION.name": "Transmutace",
+	"core.bonus.TRANSMUTATION.description": "${val}% šance na přeměnu útočené jednotky na jiný druh",
 	"core.bonus.UNDEAD.name": "Nemrtvý",
 	"core.bonus.UNDEAD.description": "Jednotka je nemrtvá",
-	"core.bonus.UNLIMITED_RETALIATIONS.name": "Unlimited retaliations",
-	"core.bonus.UNLIMITED_RETALIATIONS.description": "Can retaliate against an unlimited number of attacks",
-	"core.bonus.WATER_IMMUNITY.name": "Water immunity",
-	"core.bonus.WATER_IMMUNITY.description": "Immune to all spells from the school of Water magic",
-	"core.bonus.WIDE_BREATH.name": "Wide breath",
-	"core.bonus.WIDE_BREATH.description": "Wide breath attack (multiple hexes)"
+	"core.bonus.UNLIMITED_RETALIATIONS.name": "Neomezené odvety",
+	"core.bonus.UNLIMITED_RETALIATIONS.description": "Může se mstít za neomezený počet útoků",
+	"core.bonus.WATER_IMMUNITY.name": "Vodní odolnost",
+	"core.bonus.WATER_IMMUNITY.description": "Imunní všem kouzlům školy vodní magie",
+	"core.bonus.WIDE_BREATH.name": "Široký dech",
+	"core.bonus.WIDE_BREATH.description": "Útočí širokým dechem (více polí)"
 }

+ 3 - 0
Mods/vcmi/config/vcmi/english.json

@@ -54,6 +54,8 @@
 	"vcmi.radialWheel.moveDown" : "Move down",
 	"vcmi.radialWheel.moveBottom" : "Move to bottom",
 
+	"vcmi.spellBook.search" : "search...",
+
 	"vcmi.mainMenu.serverConnecting" : "Connecting...",
 	"vcmi.mainMenu.serverAddressEnter" : "Enter address:",
 	"vcmi.mainMenu.serverConnectionFailed" : "Failed to connect",
@@ -69,6 +71,7 @@
 	"vcmi.lobby.noPreview" : "no preview",
 	"vcmi.lobby.noUnderground" : "no underground",
 
+	"vcmi.client.errors.missingCampaigns" : "{Missing data files}\n\nCampaigns data files were not found! You may be using incomplete or corrupted Heroes 3 data files. Please reinstall game data.",
 	"vcmi.server.errors.existingProcess" : "Another VCMI server process is running. Please terminate it before starting a new game.",
 	"vcmi.server.errors.modsToEnable"    : "{Following mods are required}",
 	"vcmi.server.errors.modsToDisable"   : "{Following mods must be disabled}",

+ 2 - 0
Mods/vcmi/config/vcmi/german.json

@@ -54,6 +54,8 @@
 	"vcmi.radialWheel.moveDown" : "Nach unten bewegen",
 	"vcmi.radialWheel.moveBottom" : "Ganz nach unten bewegen",
 
+	"vcmi.spellBook.search" : "suchen...",
+
 	"vcmi.mainMenu.serverConnecting" : "Verbinde...",
 	"vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:",
 	"vcmi.mainMenu.serverConnectionFailed" : "Verbindung fehlgeschlagen",

+ 5 - 0
Mods/vcmi/config/vcmi/ukrainian.json

@@ -69,6 +69,7 @@
 	"vcmi.lobby.noPreview" : "огляд недоступний",
 	"vcmi.lobby.noUnderground" : "немає підземелля",
 
+	"vcmi.client.errors.missingCampaigns" : "{Не вистачає файлів даних}\n\nФайли даних кампаній не знайдено! Можливо, ви використовуєте неповні або пошкоджені файли даних Heroes 3. Будь ласка, перевстановіть дані гри.",
 	"vcmi.server.errors.existingProcess" : "Працює інший процес vcmiserver, будь ласка, спочатку завершіть його",
 	"vcmi.server.errors.modsToEnable"    : "{Потрібні модифікації для завантаження гри}",
 	"vcmi.server.errors.modsToDisable"   : "{Модифікації що мають бути вимкнені}",
@@ -114,6 +115,8 @@
 	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Розширення інтерфейсу}\n\nУвімкніть різні розширення інтерфейсу для покращення якості життя. Наприклад, більша книга заклинань, рюкзак тощо. Вимкнути, щоб отримати більш класичний досвід.",
 	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "Велика книга заклять",
 	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Велика книга заклять}\n\nВмикає більшу книгу заклять, яка вміщує більше заклять на сторінці. Якщо цей параметр увімкнено, анімація зміни сторінок книги заклять не буде відображатися.",
+	"vcmi.systemOptions.audioMuteFocus.hover"  : "Тиша при втраті фокусу",
+	"vcmi.systemOptions.audioMuteFocus.help"   : "{Тиша при втраті фокусу}\n\nВимкнути звук коли вікно не у фокусі. Виняток становлять ігрові сповіщення та звук нового ходу.",
 
 	"vcmi.adventureOptions.infoBarPick.help" : "{Повідомлення у панелі статусу}\n\nЗа можливості, повідомлення про відвідування об'єктів карти пригод будуть відображені у панелі статусу замість окремого вікна",
 	"vcmi.adventureOptions.infoBarPick.hover" : "Повідомлення у панелі статусу",
@@ -129,6 +132,8 @@
 	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Керування істотами у вікні статусу}\n\nДозволяє впорядковувати істот у вікні статусу замість циклічного перемикання між типовими компонентами",
 	"vcmi.adventureOptions.leftButtonDrag.hover" : "Переміщення мапи лівою кнопкою",
 	"vcmi.adventureOptions.leftButtonDrag.help" : "{Переміщення мапи лівою кнопкою}\n\nЯкщо увімкнено, переміщення миші з натиснутою лівою кнопкою буде перетягувати мапу пригод",
+	"vcmi.adventureOptions.smoothDragging.hover" : "Плавне перетягування мапи",
+	"vcmi.adventureOptions.smoothDragging.help" : "{Плавне перетягування мапи}\n\nЯкщо увімкнено, перетягування мапи має сучасний ефект завершення.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",

+ 2 - 2
android/vcmi-app/build.gradle

@@ -10,8 +10,8 @@ android {
 		applicationId "is.xyz.vcmi"
 		minSdk 19
 		targetSdk 33
-		versionCode 1410
-		versionName "1.4.1"
+		versionCode 1420
+		versionName "1.4.2"
 		setProperty("archivesBaseName", "vcmi")
 	}
 

+ 0 - 8
android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java

@@ -156,14 +156,6 @@ public class NativeMethods
         }
     }
 
-    @SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
-    public static String getFormattedDateTime()
-    {
-        String currentDate = new SimpleDateFormat((new SimpleDateFormat()).toLocalizedPattern(), Locale.getDefault()).format(new Date());
-
-        return currentDate;
-    }
-
     private static void internalProgressDisplay(final boolean show)
     {
         final Context ctx = SDL.getContext();

+ 71 - 0
android/vcmi-app/src/main/res/values-cs/strings.xml

@@ -0,0 +1,71 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <string name="url_project_page" translatable="false">https://vcmi.eu</string>
+    <string name="url_project_repo" translatable="false">https://github.com/vcmi/vcmi</string>
+    <string name="url_launcher_repo" translatable="false">https://github.com/vcmi/vcmi-android</string>
+    <string name="url_launcher_privacy" translatable="false">https://github.com/vcmi/vcmi/blob/master/docs/players/Privacy_Policy.md</string>
+
+    <string name="app_name">VCMI</string>
+    <string name="server_name">Server VCMI</string>
+    <string name="launcher_title">Spouštěč VCMI</string>
+    <string name="launcher_btn_scale_title">Škálování herního rozlišení</string>
+    <string name="launcher_btn_scale_subtitle_unknown">Současné: neznámé</string>
+    <string name="launcher_btn_scale_subtitle">Současné: %1$d%%</string>
+    <string name="launcher_btn_start_title">Spustit VCMI</string>
+    <string name="launcher_btn_start_subtitle">Současná verze VCMI: %1$s</string>
+    <string name="launcher_btn_mods_title">Modifikace</string>
+    <string name="launcher_btn_mods_subtitle">Nainstalovat nové frakce, přeměty a bonusy</string>
+    <string name="launcher_btn_language_title">Jazyk</string>
+    <string name="launcher_btn_language_subtitle_unknown">Současný: neznámý</string>
+    <string name="launcher_btn_language_subtitle">Současný: %1$s</string>
+    <string name="launcher_btn_pointermode_title">Změnit režim ukazatele</string>
+    <string name="launcher_btn_pointermode_subtitle">Současný: %1$s</string>
+    <string name="launcher_btn_pointermulti_title">Násobitel rychlosti relativního ukazatele</string>
+    <string name="launcher_btn_pointermulti_subtitle">Současný: %1$s</string>
+    <string name="launcher_btn_sound_title">Hlasitost zvuků</string>
+    <string name="launcher_btn_music_title">Hlasitost hudby</string>
+    <string name="launcher_btn_adventure_ai">AI světa</string>
+    <string name="launcher_btn_adventure_ai_title">Změnit AI světa</string>
+    <string name="launcher_btn_import_title">Importovat data VCMI</string>
+    <string name="launcher_btn_import_description">Zkopírovat soubury VCMI do vestavěného úložiště. Můžete importovat starou složku dat vcmi z vydání 0.99 nebo soubory HOMM3</string>
+    <string name="launcher_btn_export_title">Exportovat data VCMI</string>
+    <string name="launcher_btn_export_description">Udělat kopii dat VCMI před odinstalací nebo pro synchronizaci s desktopovou verzí. Též můžete přímo přistoupit k interním datům.</string>
+    <string name="launcher_progress_copy">Kopírování %1$s</string>
+    <string name="launcher_version">Současná verze spouštěče: %1$s</string>
+    <string name="launcher_error_vcmi_data_root_failed">Nelze vytvořit datovou složku VCMI v %1$s.</string>
+    <string name="launcher_error_h3_data_missing">Nelze najít datovou složku v \'%1$s\'. Vložte do ní své datové soubory HoMM3 nebo použijte tlačítko níže. Možná budete muset také restartovat aplikaci.</string>
+    <string name="launcher_error_vcmi_data_internal_missing">Nelze najít nebo rozbalit data vcmi ze zdrojů aplikace. Zkuste přeinstalovat aplikaci.</string>
+    <string name="launcher_error_vcmi_data_internal_update">Nelze aktualizovat data vcmi ze zdrojů aplikace. Zkuste přeinstalovat aplikaci.</string>
+    <string name="launcher_error_permissions">Tato aplikace potřebuje oprávnění k zápisu pro použití obsahu na externím úložišti</string>
+    <string name="launcher_error_permission_broken">Nelze správně vyřešit oprávnění</string>
+    <string name="mods_item_author_template">od %1$s</string>
+    <string name="misc_try_again">Zkusit znovu</string>
+    <string name="launcher_section_init">Inicializae hry</string>
+    <string name="launcher_section_settings">Nastavení</string>
+    <string name="menu_mods_download_repo">Stáhnout data repozitáře</string>
+
+    <string name="misc_pointermode_normal">Normální</string>
+    <string name="misc_pointermode_relative">Relativní</string>
+    <string name="menu_launcher_about">O spouštěči</string>
+
+    <string name="mods_title">Nalezené modifikace</string>
+    <string name="mods_failed_mod_loading">Nelze načíst modifikaci ve složce \'%1$s\'</string>
+    <string name="mods_removal_title">Odebírání %1$s</string>
+    <string name="mods_removal_confirmation">Jste si jisti odebráním %1$s?</string>
+
+    <string name="about_title">O aplikaci</string>
+    <string name="about_version_app">Verze aplikace: %1$s</string>
+    <string name="about_version_launcher">Verze spouštěče: %1$s</string>
+    <string name="about_section_project">Projekt</string>
+    <string name="about_section_legal">Právní záležitosti</string>
+    <string name="about_links_main">Hlavní stránka: %1$s</string>
+    <string name="about_links_repo">Repozitář projektu: %1$s</string>
+    <string name="about_links_repo_launcher">Repozitář spouštěče: %1$s</string>
+    <string name="about_btn_authors">Autoři</string>
+    <string name="about_btn_privacy">Zásady ochrany osobních údajů: %1$s</string>
+    <string name="about_error_opening_url">Nebylo možné otevřít webovou stránku (nenalezena patřičná aplikace)</string>
+
+    <string name="dialog_authors_vcmi">Autoři VCMI</string>
+    <string name="dialog_authors_launcher">Autoři spouštěče</string>
+    <string name="launcher_error_config_saving_failed">Nelze uložit konfigurační soubor VCMI; důvod: %1$s</string>
+</resources>

+ 31 - 11
client/CMT.cpp

@@ -55,12 +55,14 @@
 namespace po = boost::program_options;
 namespace po_style = boost::program_options::command_line_style;
 
+static std::atomic<bool> quitRequestedDuringOpeningPlayback = false;
 static po::variables_map vm;
 
 #ifndef VCMI_IOS
 void processCommand(const std::string &message);
 #endif
 void playIntro();
+[[noreturn]] static void quitApplication();
 static void mainLoop();
 
 static CBasicLogConfigurator *logConfig;
@@ -257,10 +259,10 @@ int main(int argc, char * argv[])
 	};
 
 	testFile("DATA/HELP.TXT", "VCMI requires Heroes III: Shadow of Death or Heroes III: Complete data files to run!");
-	testFile("MODS/VCMI/MOD.JSON", "VCMI installation is corrupted! Built-in mod was not found!");
-	testFile("DATA/PLAYERS.PAL", "Heroes III data files are missing or corruped! Please reinstall them.");
-	testFile("SPRITES/DEFAULT.DEF", "Heroes III data files are missing or corruped! Please reinstall them.");
 	testFile("DATA/TENTCOLR.TXT", "Heroes III: Restoration of Erathia (including HD Edition) data files are not supported!");
+	testFile("MODS/VCMI/MOD.JSON", "VCMI installation is corrupted! Built-in mod was not found!");
+	testFile("DATA/PLAYERS.PAL", "Heroes III data files (Data/H3Bitmap.lod) are incomplete or corruped! Please reinstall them.");
+	testFile("SPRITES/DEFAULT.DEF", "Heroes III data files (Data/H3Sprite.lod) are incomplete or corruped! Please reinstall them.");
 
 	srand ( (unsigned int)time(nullptr) );
 
@@ -313,7 +315,6 @@ int main(int argc, char * argv[])
 		GH.screenHandler().clearScreen();
 	}
 
-
 #ifndef VCMI_NO_THREADED_LOAD
 	#ifdef VCMI_ANDROID // android loads the data quite slowly so we display native progressbar to prevent having only black screen for few seconds
 	{
@@ -327,6 +328,9 @@ int main(int argc, char * argv[])
 	#endif // ANDROID
 #endif // THREADED
 
+	if (quitRequestedDuringOpeningPlayback)
+		quitApplication();
+
 	if(!settings["session"]["headless"].Bool())
 	{
 		pomtime.getDiff();
@@ -414,7 +418,7 @@ int main(int argc, char * argv[])
 	else
 	{
 		while(true)
-			boost::this_thread::sleep_for(boost::chrono::milliseconds(1000));
+			boost::this_thread::sleep_for(boost::chrono::milliseconds(200));
 	}
 
 	return 0;
@@ -451,7 +455,7 @@ static void mainLoop()
 	}
 }
 
-static void quitApplication()
+[[noreturn]] static void quitApplication()
 {
 	if(!settings["session"]["headless"].Bool())
 	{
@@ -487,7 +491,8 @@ static void quitApplication()
 	vstd::clear_pointer(CSH);
 	vstd::clear_pointer(VLC);
 
-	vstd::clear_pointer(console);// should be removed after everything else since used by logging
+	// sometimes leads to a hang. TODO: investigate
+	//vstd::clear_pointer(console);// should be removed after everything else since used by logging
 
 	if(!settings["session"]["headless"].Bool())
 		GH.screenHandler().close();
@@ -501,14 +506,29 @@ static void quitApplication()
 
 	std::cout << "Ending...\n";
 
-	// this method is always called from event/network threads, which keep interface mutex locked
-	// unlock it here to avoid assertion failure on GH destruction in exit()
-	GH.interfaceMutex.unlock();
-	exit(0);
+	// Perform quick exit without executing static destructors and let OS cleanup anything that we did not
+	// We generally don't care about them and this leads to numerous issues, e.g.
+	// destruction of locked mutexes (fails an assertion), even in third-party libraries (as well as native libs on Android)
+	// Android - std::quick_exit is available only starting from API level 21
+	// Mingw, macOS and iOS - std::quick_exit is unavailable (at least in current version of CI)
+#if (defined(__ANDROID_API__) && __ANDROID_API__ < 21) || (defined(__MINGW32__)) || defined(VCMI_APPLE)
+	::exit(0);
+#else
+	std::quick_exit(0);
+#endif
 }
 
 void handleQuit(bool ask)
 {
+	// FIXME: avoids crash if player attempts to close game while opening is still playing
+	// use cursor handler as indicator that loading is not done yet
+	// proper solution would be to abort init thread (or wait for it to finish)
+	if (!CCS->curh)
+	{
+		quitRequestedDuringOpeningPlayback = true;
+		return;
+	}
+
 	if(ask)
 	{
 		CCS->curh->set(Cursor::Map::POINTER);

+ 1 - 1
client/CVideoHandler.cpp

@@ -637,7 +637,7 @@ bool CVideoPlayer::playVideo(int x, int y, bool stopOnKey)
 
 		SDL_Rect rect = CSDL_Ext::toSDL(pos);
 
-		SDL_RenderClear(mainRenderer);
+		SDL_RenderFillRect(mainRenderer, &rect);
 		SDL_RenderCopy(mainRenderer, texture, nullptr, &rect);
 		SDL_RenderPresent(mainRenderer);
 

+ 5 - 1
client/NetPacksLobbyClient.cpp

@@ -15,6 +15,7 @@
 
 #include "lobby/OptionsTab.h"
 #include "lobby/RandomMapTab.h"
+#include "lobby/TurnOptionsTab.h"
 #include "lobby/SelectionTab.h"
 #include "lobby/CBonusSelection.h"
 
@@ -47,7 +48,7 @@ void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientConnected(LobbyClientCon
 
 void ApplyOnLobbyHandlerNetPackVisitor::visitLobbyClientDisconnected(LobbyClientDisconnected & pack)
 {
-	if(pack.clientId != pack.c->connectionID)
+	if(pack.clientId != handler.c->connectionID)
 	{
 		result = false;
 		return;
@@ -95,6 +96,9 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction & pack
 	case LobbyGuiAction::OPEN_RANDOM_MAP_OPTIONS:
 		lobby->toggleTab(lobby->tabRand);
 		break;
+	case LobbyGuiAction::OPEN_TURN_OPTIONS:
+		lobby->toggleTab(lobby->tabTurnOptions);
+		break;
 	}
 }
 

+ 2 - 0
client/PlayerLocalState.cpp

@@ -239,6 +239,8 @@ void PlayerLocalState::swapWanderingHero(int pos1, int pos2)
 {
 	assert(wanderingHeroes[pos1] && wanderingHeroes[pos2]);
 	std::swap(wanderingHeroes[pos1], wanderingHeroes[pos2]);
+
+	adventureInt->onHeroOrderChanged();
 }
 
 const std::vector<const CGTownInstance *> & PlayerLocalState::getOwnedTowns()

+ 2 - 2
client/PlayerLocalState.h

@@ -42,7 +42,7 @@ public:
 	{
 		//on which page we left spellbook
 		int spellbookLastPageBattle = 0;
-		int spellbokLastPageAdvmap = 0;
+		int spellbookLastPageAdvmap = 0;
 		int spellbookLastTabBattle = 4;
 		int spellbookLastTabAdvmap = 4;
 
@@ -50,7 +50,7 @@ public:
 		void serialize(Handler & h, const int version)
 		{
 			h & spellbookLastPageBattle;
-			h & spellbokLastPageAdvmap;
+			h & spellbookLastPageAdvmap;
 			h & spellbookLastTabBattle;
 			h & spellbookLastTabAdvmap;
 		}

+ 7 - 2
client/adventureMap/AdventureMapInterface.cpp

@@ -65,8 +65,8 @@ AdventureMapInterface::AdventureMapInterface():
 	shortcuts->setState(EAdventureState::MAKING_TURN);
 	widget->getMapView()->onViewMapActivated();
 
-	if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.isEnabled() || LOCPLINT->cb->getStartInfo()->turnTimerInfo.isBattleEnabled())
-		watches = std::make_shared<TurnTimerWidget>();
+	if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.turnTimer != 0)
+		watches = std::make_shared<TurnTimerWidget>(Point(24, 24));
 	
 	addUsedEvents(KEYBOARD | TIME);
 }
@@ -331,6 +331,11 @@ void AdventureMapInterface::onTownOrderChanged()
 	widget->getTownList()->updateWidget();
 }
 
+void AdventureMapInterface::onHeroOrderChanged()
+{
+	widget->getHeroList()->updateWidget();
+}
+
 void AdventureMapInterface::onMapTilesChanged(boost::optional<std::unordered_set<int3>> positions)
 {
 	if (positions)

+ 3 - 0
client/adventureMap/AdventureMapInterface.h

@@ -149,6 +149,9 @@ public:
 	/// Called when town order changes
 	void onTownOrderChanged();
 
+	/// Called when hero order changes
+	void onHeroOrderChanged();
+
 	/// Called when map audio should be paused, e.g. on combat or town screen access
 	void onAudioPaused();
 

+ 3 - 0
client/adventureMap/CInGameConsole.cpp

@@ -151,6 +151,9 @@ void CInGameConsole::keyPressed (EShortcut key)
 		break;
 
 	case EShortcut::GAME_ACTIVATE_CONSOLE:
+		if(GH.isKeyboardAltDown())
+			return; //QoL for alt-tab operating system shortcut
+
 		if(!enteredText.empty())
 			endEnteringText(false);
 		else

+ 8 - 14
client/adventureMap/CList.cpp

@@ -218,8 +218,7 @@ CHeroList::CEmptyHeroItem::CEmptyHeroItem()
 
 CHeroList::CHeroItem::CHeroItem(CHeroList *parent, const CGHeroInstance * Hero)
 	: CListItem(parent),
-	hero(Hero),
-	parentList(parent)
+	hero(Hero)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 	movement = std::make_shared<CAnimImage>(AnimationPath::builtin("IMOBIL"), 0, 0, 0, 1);
@@ -285,19 +284,17 @@ void CHeroList::CHeroItem::gesture(bool on, const Point & initialPosition, const
 	const CGHeroInstance * heroLower = (heroPos > heroes.size() - 2) ? nullptr : heroes[heroPos + 1];
 
 	std::vector<RadialMenuConfig> menuElements = {
-		{ RadialMenuConfig::ITEM_ALT_NN, heroUpper != nullptr, "altUpTop", "vcmi.radialWheel.moveTop", [this, heroPos]()
+		{ RadialMenuConfig::ITEM_ALT_NN, heroUpper != nullptr, "altUpTop", "vcmi.radialWheel.moveTop", [heroPos]()
 		{
 			for (int i = heroPos; i > 0; i--)
 				LOCPLINT->localState->swapWanderingHero(i, i - 1);
-			parentList->updateWidget();
 		} },
-		{ RadialMenuConfig::ITEM_ALT_NW, heroUpper != nullptr, "altUp", "vcmi.radialWheel.moveUp", [this, heroPos](){LOCPLINT->localState->swapWanderingHero(heroPos, heroPos - 1); parentList->updateWidget(); } },
-		{ RadialMenuConfig::ITEM_ALT_SW, heroLower != nullptr, "altDown", "vcmi.radialWheel.moveDown", [this, heroPos](){ LOCPLINT->localState->swapWanderingHero(heroPos, heroPos + 1); parentList->updateWidget(); } },
-		{ RadialMenuConfig::ITEM_ALT_SS, heroLower != nullptr, "altDownBottom", "vcmi.radialWheel.moveBottom", [this, heroPos, heroes]()
+		{ RadialMenuConfig::ITEM_ALT_NW, heroUpper != nullptr, "altUp", "vcmi.radialWheel.moveUp", [heroPos](){LOCPLINT->localState->swapWanderingHero(heroPos, heroPos - 1); } },
+		{ RadialMenuConfig::ITEM_ALT_SW, heroLower != nullptr, "altDown", "vcmi.radialWheel.moveDown", [heroPos](){ LOCPLINT->localState->swapWanderingHero(heroPos, heroPos + 1); } },
+		{ RadialMenuConfig::ITEM_ALT_SS, heroLower != nullptr, "altDownBottom", "vcmi.radialWheel.moveBottom", [heroPos, heroes]()
 		{
 			for (int i = heroPos; i < heroes.size() - 1; i++)
 				LOCPLINT->localState->swapWanderingHero(i, i + 1);
-			parentList->updateWidget();
 		} },
 	};
 
@@ -365,8 +362,7 @@ std::shared_ptr<CIntObject> CTownList::createItem(size_t index)
 }
 
 CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town):
-	CListItem(parent),
-	parentList(parent)
+	CListItem(parent)
 {
 	const std::vector<const CGTownInstance *> towns = LOCPLINT->localState->getOwnedTowns();
 	townIndex = std::distance(towns.begin(), std::find(towns.begin(), towns.end(), Town));
@@ -430,15 +426,13 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const
 		{
 			for (int i = townIndex; i > 0; i--)
 				LOCPLINT->localState->swapOwnedTowns(i, i - 1);
-			parentList->updateWidget();
 		} },
-		{ RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [this, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); parentList->updateWidget(); } },
-		{ RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [this, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); parentList->updateWidget(); } },
+		{ RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [this, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); } },
+		{ RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [this, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); } },
 		{ RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [this, towns]()
 		{
 			for (int i = townIndex; i < towns.size() - 1; i++)
 				LOCPLINT->localState->swapOwnedTowns(i, i + 1);
-			parentList->updateWidget();
 		} },
 	};
 

+ 0 - 2
client/adventureMap/CList.h

@@ -117,7 +117,6 @@ class CHeroList	: public CList
 		std::shared_ptr<CAnimImage> movement;
 		std::shared_ptr<CAnimImage> mana;
 		std::shared_ptr<CAnimImage> portrait;
-		CHeroList *parentList;
 	public:
 		const CGHeroInstance * const hero;
 
@@ -152,7 +151,6 @@ class CTownList	: public CList
 	class CTownItem : public CListItem
 	{
 		std::shared_ptr<CAnimImage> picture;
-		CTownList *parentList;
 	public:
 		int townIndex;
 

+ 140 - 111
client/adventureMap/TurnTimerWidget.cpp

@@ -15,54 +15,86 @@
 #include "../CPlayerInterface.h"
 #include "../battle/BattleInterface.h"
 #include "../battle/BattleStacksController.h"
-
-#include "../render/EFont.h"
-#include "../render/Graphics.h"
 #include "../gui/CGuiHandler.h"
-#include "../gui/TextAlignment.h"
+#include "../render/Graphics.h"
 #include "../widgets/Images.h"
+#include "../widgets/MiscWidgets.h"
 #include "../widgets/TextControls.h"
+
 #include "../../CCallback.h"
-#include "../../lib/CStack.h"
 #include "../../lib/CPlayerState.h"
-#include "../../lib/filesystem/ResourcePath.h"
+#include "../../lib/CStack.h"
+#include "../../lib/StartInfo.h"
 
-TurnTimerWidget::DrawRect::DrawRect(const Rect & r, const ColorRGBA & c):
-	CIntObject(), rect(r), color(c)
-{
-}
+TurnTimerWidget::TurnTimerWidget(const Point & position)
+	: TurnTimerWidget(position, PlayerColor::NEUTRAL)
+{}
 
-void TurnTimerWidget::DrawRect::showAll(Canvas & to)
+TurnTimerWidget::TurnTimerWidget(const Point & position, PlayerColor player)
+	: CIntObject(TIME)
+	, lastSoundCheckSeconds(0)
+	, isBattleMode(player.isValidPlayer())
 {
-	to.drawColor(rect, color);
-	
-	CIntObject::showAll(to);
-}
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 
-TurnTimerWidget::TurnTimerWidget():
-	InterfaceObjectConfigurable(TIME),
-	turnTime(0), lastTurnTime(0), cachedTurnTime(0), lastPlayer(PlayerColor::CANNOT_DETERMINE)
-{
-	REGISTER_BUILDER("drawRect", &TurnTimerWidget::buildDrawRect);
-	
+	pos += position;
+	pos.w = 0;
+	pos.h = 0;
 	recActions &= ~DEACTIVATE;
-	
-	const JsonNode config(JsonPath::builtin("config/widgets/turnTimer.json"));
-	
-	build(config);
-	
-	std::transform(variables["notificationTime"].Vector().begin(),
-				   variables["notificationTime"].Vector().end(),
-				   std::inserter(notifications, notifications.begin()),
-				   [](const JsonNode & node){ return node.Integer(); });
-}
+	const auto & timers = LOCPLINT->cb->getStartInfo()->turnTimerInfo;
 
-std::shared_ptr<TurnTimerWidget::DrawRect> TurnTimerWidget::buildDrawRect(const JsonNode & config) const
-{
-	logGlobal->debug("Building widget TurnTimerWidget::DrawRect");
-	auto rect = readRect(config["rect"]);
-	auto color = readColor(config["color"]);
-	return std::make_shared<TurnTimerWidget::DrawRect>(rect, color);
+	backgroundTexture = std::make_shared<CFilledTexture>(ImagePath::builtin("DiBoxBck"), pos); // 1 px smaller on all sides
+
+	if (isBattleMode)
+		backgroundBorder = std::make_shared<TransparentFilledRectangle>(pos, ColorRGBA(0, 0, 0, 128), Colors::BRIGHT_YELLOW);
+	else
+		backgroundBorder = std::make_shared<TransparentFilledRectangle>(pos, ColorRGBA(0, 0, 0, 128), Colors::BLACK);
+
+	if (isBattleMode)
+	{
+		pos.w = 76;
+
+		pos.h += 20;
+		playerLabelsMain[player] = std::make_shared<CLabel>(pos.w / 2, pos.h - 10, FONT_BIG, ETextAlignment::CENTER, graphics->playerColors[player], "");
+
+		if (timers.battleTimer != 0)
+		{
+			pos.h += 20;
+			playerLabelsBattle[player] = std::make_shared<CLabel>(pos.w / 2, pos.h - 10, FONT_BIG, ETextAlignment::CENTER, graphics->playerColors[player], "");
+		}
+
+		if (!timers.accumulatingUnitTimer && timers.unitTimer != 0)
+		{
+			pos.h += 20;
+			playerLabelsUnit[player] = std::make_shared<CLabel>(pos.w / 2, pos.h - 10, FONT_BIG, ETextAlignment::CENTER, graphics->playerColors[player], "");
+		}
+
+		updateTextLabel(player, LOCPLINT->cb->getPlayerTurnTime(player));
+	}
+	else
+	{
+		if (!timers.accumulatingTurnTimer && timers.baseTimer != 0)
+			pos.w = 120;
+		else
+			pos.w = 60;
+
+		for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player)
+		{
+			if (LOCPLINT->cb->getStartInfo()->playerInfos.count(player) == 0)
+				continue;
+
+			if (!LOCPLINT->cb->getStartInfo()->playerInfos.at(player).isControlledByHuman())
+				continue;
+
+			pos.h += 20;
+			playerLabelsMain[player] = std::make_shared<CLabel>(pos.w / 2, pos.h - 10, FONT_BIG, ETextAlignment::CENTER, graphics->playerColors[player], "");
+
+			updateTextLabel(player, LOCPLINT->cb->getPlayerTurnTime(player));
+		}
+	}
+
+	backgroundTexture->pos = Rect::createAround(pos, -1);
+	backgroundBorder->pos = pos;
 }
 
 void TurnTimerWidget::show(Canvas & to)
@@ -70,98 +102,95 @@ void TurnTimerWidget::show(Canvas & to)
 	showAll(to);
 }
 
-void TurnTimerWidget::setTime(PlayerColor player, int time)
+void TurnTimerWidget::updateNotifications(PlayerColor player, int timeMs)
 {
-	int newTime = time / 1000;
-	if(player == LOCPLINT->playerID
-	   && newTime != turnTime
-	   && notifications.count(newTime))
-	{
-		CCS->soundh->playSound(AudioPath::fromJson(variables["notificationSound"]));
-	}
+	if(player != LOCPLINT->playerID)
+		return;
 
-	turnTime = newTime;
+	int newTimeSeconds = timeMs / 1000;
 
-	if(auto w = widget<CLabel>("timer"))
-	{
-		std::ostringstream oss;
-		oss << turnTime / 60 << ":" << std::setw(2) << std::setfill('0') << turnTime % 60;
-		w->setText(oss.str());
-		
-		if(graphics && LOCPLINT && LOCPLINT->cb
-		   && variables["textColorFromPlayerColor"].Bool()
-		   && player.isValidPlayer())
-		{
-			w->setColor(graphics->playerColors[player]);
-		}
-	}
+	if (newTimeSeconds != lastSoundCheckSeconds && notificationThresholds.count(newTimeSeconds))
+		CCS->soundh->playSound(AudioPath::builtin("WE5"));
+
+	lastSoundCheckSeconds = newTimeSeconds;
 }
 
-void TurnTimerWidget::updateTimer(PlayerColor player, uint32_t msPassed)
+static std::string msToString(int timeMs)
 {
-	const auto & time = LOCPLINT->cb->getPlayerTurnTime(player);
-	if(time.isActive)
-		cachedTurnTime -= msPassed;
-	
-	if(cachedTurnTime < 0)
-		cachedTurnTime = 0; //do not go below zero
-	
-	if(lastPlayer != player)
-	{
-		lastPlayer = player;
-		lastTurnTime = 0;
-	}
-	
-	auto timeCheckAndUpdate = [&](int time)
+	int timeSeconds = timeMs / 1000;
+	std::ostringstream oss;
+	oss << timeSeconds / 60 << ":" << std::setw(2) << std::setfill('0') << timeSeconds % 60;
+	return oss.str();
+}
+
+void TurnTimerWidget::updateTextLabel(PlayerColor player, const TurnTimerInfo & timer)
+{
+	const auto & timerSettings = LOCPLINT->cb->getStartInfo()->turnTimerInfo;
+	auto mainLabel = playerLabelsMain[player];
+
+	if (isBattleMode)
 	{
-		if(time / 1000 != lastTurnTime / 1000)
+		mainLabel->setText(msToString(timer.baseTimer + timer.turnTimer));
+
+		if (timerSettings.battleTimer != 0)
 		{
-			//do not update timer on this tick
-			lastTurnTime = time;
-			cachedTurnTime = time;
+			auto battleLabel = playerLabelsBattle[player];
+			if (timer.battleTimer != 0)
+			{
+				if (timerSettings.accumulatingUnitTimer)
+					battleLabel->setText("+" + msToString(timer.battleTimer + timer.unitTimer));
+				else
+					battleLabel->setText("+" + msToString(timer.battleTimer));
+			}
+			else
+				battleLabel->setText("");
 		}
-		else
-			setTime(player, cachedTurnTime);
-	};
-	
-	auto * playerInfo = LOCPLINT->cb->getPlayer(player);
-	if(player.isValidPlayer() || (playerInfo && playerInfo->isHuman()))
+
+		if (!timerSettings.accumulatingUnitTimer && timerSettings.unitTimer != 0)
+		{
+			auto unitLabel = playerLabelsUnit[player];
+			if (timer.unitTimer != 0)
+				unitLabel->setText("+" + msToString(timer.unitTimer));
+			else
+				unitLabel->setText("");
+		}
+	}
+	else
 	{
-		if(time.isBattle)
-			timeCheckAndUpdate(time.baseTimer + time.turnTimer + time.battleTimer + time.unitTimer);
+		if (!timerSettings.accumulatingTurnTimer && timerSettings.baseTimer != 0)
+			mainLabel->setText(msToString(timer.baseTimer) + "+" + msToString(timer.turnTimer));
 		else
-			timeCheckAndUpdate(time.baseTimer + time.turnTimer);
+			mainLabel->setText(msToString(timer.baseTimer + timer.turnTimer));
 	}
-	else
-		timeCheckAndUpdate(0);
 }
 
-void TurnTimerWidget::tick(uint32_t msPassed)
+void TurnTimerWidget::updateTimer(PlayerColor player, uint32_t msPassed)
 {
-	if(!LOCPLINT || !LOCPLINT->cb)
-		return;
+	const auto & gamestateTimer = LOCPLINT->cb->getPlayerTurnTime(player);
+	updateNotifications(player, gamestateTimer.valueMs());
+	updateTextLabel(player, gamestateTimer);
+}
 
-	if(LOCPLINT->battleInt)
-	{
-		if(auto * stack = LOCPLINT->battleInt->stacksController->getActiveStack())
-			updateTimer(stack->getOwner(), msPassed);
-		else
-			updateTimer(PlayerColor::NEUTRAL, msPassed);
-	}
-	else
+void TurnTimerWidget::tick(uint32_t msPassed)
+{
+	for(const auto & player : playerLabelsMain)
 	{
-		if(LOCPLINT->makingTurn)
-			updateTimer(LOCPLINT->playerID, msPassed);
-		else
+		if (LOCPLINT->battleInt)
 		{
-			for(PlayerColor p(0); p < PlayerColor::PLAYER_LIMIT; ++p)
-			{
-				if(LOCPLINT->cb->isPlayerMakingTurn(p))
-				{
-					updateTimer(p, msPassed);
-					break;
-				}
-			}
+			const auto & battle = LOCPLINT->battleInt->getBattle();
+
+			bool isDefender = battle->sideToPlayer(BattleSide::DEFENDER) == player.first;
+			bool isAttacker = battle->sideToPlayer(BattleSide::ATTACKER) == player.first;
+			bool isMakingUnitTurn = battle->battleActiveUnit() && battle->battleActiveUnit()->unitOwner() == player.first;
+			bool isEngagedInBattle = isDefender || isAttacker;
+
+			// Due to way our network message queue works during battle animation
+			// client actually does not receives updates from server as to which timer is active when game has battle animations playing
+			// so during battle skip updating timer unless game is waiting for player to select action
+			if (isEngagedInBattle && !isMakingUnitTurn)
+				continue;
 		}
+
+		updateTimer(player.first, msPassed);
 	}
 }

+ 23 - 30
client/adventureMap/TurnTimerWidget.h

@@ -11,50 +11,43 @@
 #pragma once
 
 #include "../gui/CIntObject.h"
-#include "../gui/InterfaceObjectConfigurable.h"
-#include "../render/Canvas.h"
 #include "../render/Colors.h"
+#include "../../lib/TurnTimerInfo.h"
 
 class CAnimImage;
 class CLabel;
+class CFilledTexture;
+class TransparentFilledRectangle;
 
 VCMI_LIB_NAMESPACE_BEGIN
-
 class PlayerColor;
-
 VCMI_LIB_NAMESPACE_END
 
-class TurnTimerWidget : public InterfaceObjectConfigurable
+class TurnTimerWidget : public CIntObject
 {
-private:
-	
-	class DrawRect : public CIntObject
-	{
-		const Rect rect;
-		const ColorRGBA color;
-		
-	public:
-		DrawRect(const Rect &, const ColorRGBA &);
-		void showAll(Canvas & to) override;
-	};
-
-	int turnTime;
-	int lastTurnTime;
-	int cachedTurnTime;
-	PlayerColor lastPlayer;
-	
-	std::set<int> notifications;
-	
-	std::shared_ptr<DrawRect> buildDrawRect(const JsonNode & config) const;
-	
-	void updateTimer(PlayerColor player, uint32_t msPassed);
+	int lastSoundCheckSeconds;
+	bool isBattleMode;
 
-public:
+	const std::set<int> notificationThresholds = {1, 2, 3, 4, 5, 10, 20, 30};
+
+	std::map<PlayerColor, std::shared_ptr<CLabel>> playerLabelsMain;
+	std::map<PlayerColor, std::shared_ptr<CLabel>> playerLabelsBattle;
+	std::map<PlayerColor, std::shared_ptr<CLabel>> playerLabelsUnit;
+	std::shared_ptr<CFilledTexture> backgroundTexture;
+	std::shared_ptr<TransparentFilledRectangle> backgroundBorder;
+
+	void updateTimer(PlayerColor player, uint32_t msPassed);
 
 	void show(Canvas & to) override;
 	void tick(uint32_t msPassed) override;
 	
-	void setTime(PlayerColor player, int time);
+	void updateNotifications(PlayerColor player, int timeMs);
+	void updateTextLabel(PlayerColor player, const TurnTimerInfo & timer);
+
+public:
+	/// Activates adventure map mode in which widget will display timer for all players
+	TurnTimerWidget(const Point & position);
 
-	TurnTimerWidget();
+	/// Activates battle mode in which timer displays only timer of specific player
+	TurnTimerWidget(const Point & position, PlayerColor player);
 };

+ 2 - 1
client/battle/BattleInterface.cpp

@@ -107,7 +107,8 @@ void BattleInterface::playIntroSoundAndUnlockInterface()
 {
 	auto onIntroPlayed = [this]()
 	{
-		if(LOCPLINT->battleInt)
+		// Make sure that battle have not ended while intro was playing AND that a different one has not started
+		if(LOCPLINT->battleInt.get() == this)
 			onIntroSoundPlayed();
 	};
 

+ 2 - 0
client/battle/BattleStacksController.cpp

@@ -691,6 +691,8 @@ void BattleStacksController::endAction(const BattleAction & action)
 
 void BattleStacksController::startAction(const BattleAction & action)
 {
+	// if timer run out and we did not act in time - deactivate current stack
+	setActiveStack(nullptr);
 	removeExpiredColorFilters();
 }
 

+ 43 - 4
client/battle/BattleWindow.cpp

@@ -31,6 +31,7 @@
 #include "../render/Canvas.h"
 #include "../render/IRenderHandler.h"
 #include "../adventureMap/CInGameConsole.h"
+#include "../adventureMap/TurnTimerWidget.h"
 
 #include "../../CCallback.h"
 #include "../../lib/CGeneralTextHandler.h"
@@ -39,6 +40,7 @@
 #include "../../lib/CStack.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/filesystem/ResourcePath.h"
+#include "../../lib/StartInfo.h"
 #include "../windows/settings/SettingsMainWindow.h"
 
 BattleWindow::BattleWindow(BattleInterface & owner):
@@ -83,6 +85,7 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 
 	createQueue();
 	createStickyHeroInfoWindows();
+	createTimerInfoWindows();
 
 	if ( owner.tacticsMode )
 		tacticPhaseStarted();
@@ -128,8 +131,8 @@ void BattleWindow::createStickyHeroInfoWindows()
 		InfoAboutHero info;
 		info.initFromHero(owner.defendingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
 		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x + pos.w + 15, pos.y)
-				: Point(pos.x + pos.w -79, pos.y + 135);
+				? Point(pos.x + pos.w + 15, pos.y + 60)
+				: Point(pos.x + pos.w -79, pos.y + 195);
 		defenderHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, &position);
 	}
 	if(owner.attackingHeroInstance)
@@ -137,8 +140,8 @@ void BattleWindow::createStickyHeroInfoWindows()
 		InfoAboutHero info;
 		info.initFromHero(owner.attackingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
 		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x - 93, pos.y)
-				: Point(pos.x + 1, pos.y + 135);
+				? Point(pos.x - 93, pos.y + 60)
+				: Point(pos.x + 1, pos.y + 195);
 		attackerHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, &position);
 	}
 
@@ -154,6 +157,33 @@ void BattleWindow::createStickyHeroInfoWindows()
 	}
 }
 
+void BattleWindow::createTimerInfoWindows()
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.battleTimer != 0 || LOCPLINT->cb->getStartInfo()->turnTimerInfo.unitTimer != 0)
+	{
+		PlayerColor attacker = owner.getBattle()->sideToPlayer(BattleSide::ATTACKER);
+		PlayerColor defender = owner.getBattle()->sideToPlayer(BattleSide::DEFENDER);
+
+		if (attacker.isValidPlayer())
+		{
+			if (GH.screenDimensions().x >= 1000)
+				attackerTimerWidget = std::make_shared<TurnTimerWidget>(Point(-92, 1), attacker);
+			else
+				attackerTimerWidget = std::make_shared<TurnTimerWidget>(Point(1, 135), attacker);
+		}
+
+		if (defender.isValidPlayer())
+		{
+			if (GH.screenDimensions().x >= 1000)
+				defenderTimerWidget = std::make_shared<TurnTimerWidget>(Point(pos.w + 16, 1), defender);
+			else
+				defenderTimerWidget = std::make_shared<TurnTimerWidget>(Point(pos.w - 78, 135), defender);
+		}
+	}
+}
+
 BattleWindow::~BattleWindow()
 {
 	CPlayerInterface::battleInt = nullptr;
@@ -557,6 +587,15 @@ void BattleWindow::bSpellf()
 			LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[683])
 										% heroName % CGI->artifacts()->getByIndex(artID)->getNameTranslated()));
 		}
+		else if(blockingBonus->source == BonusSource::OBJECT_TYPE)
+		{
+			if(blockingBonus->sid.as<MapObjectID>() == Obj::GARRISON || blockingBonus->sid.as<MapObjectID>() == Obj::GARRISON2)
+				LOCPLINT->showInfoDialog(CGI->generaltexth->allTexts[684]);
+		}
+	}
+	else
+	{
+		logGlobal->warn("Unexpected problem with readiness to cast spell");
 	}
 }
 

+ 5 - 0
client/battle/BattleWindow.h

@@ -24,6 +24,7 @@ class BattleInterface;
 class BattleConsole;
 class BattleRenderer;
 class StackQueue;
+class TurnTimerWidget;
 class HeroInfoBasicPanel;
 
 /// GUI object that handles functionality of panel at the bottom of combat screen
@@ -36,6 +37,9 @@ class BattleWindow : public InterfaceObjectConfigurable
 	std::shared_ptr<HeroInfoBasicPanel> attackerHeroWindow;
 	std::shared_ptr<HeroInfoBasicPanel> defenderHeroWindow;
 
+	std::shared_ptr<TurnTimerWidget> attackerTimerWidget;
+	std::shared_ptr<TurnTimerWidget> defenderTimerWidget;
+
 	/// button press handling functions
 	void bOptionsf();
 	void bSurrenderf();
@@ -65,6 +69,7 @@ class BattleWindow : public InterfaceObjectConfigurable
 
 	void toggleStickyHeroWindowsVisibility();
 	void createStickyHeroInfoWindows();
+	void createTimerInfoWindows();
 
 	std::shared_ptr<BattleConsole> buildBattleConsole(const JsonNode &) const;
 

+ 2 - 4
client/lobby/CBonusSelection.cpp

@@ -273,17 +273,15 @@ void CBonusSelection::createBonusesIcons()
 		}
 
 		case CampaignBonusType::HERO:
-
-			desc.appendLocalString(EMetaText::GENERAL_TXT, 718);
-			desc.replaceTextID(TextIdentifier("core", "genrltxt", "capColors", bonDescs[i].info1).get());
 			if(bonDescs[i].info2 == 0xFFFF)
 			{
-				desc.replaceLocalString(EMetaText::GENERAL_TXT, 101);
+				desc.appendLocalString(EMetaText::GENERAL_TXT, 720); // Start with random hero
 				picNumber = -1;
 				picName = "CBONN1A3.BMP";
 			}
 			else
 			{
+				desc.appendLocalString(EMetaText::GENERAL_TXT, 715); // Start with %s
 				desc.replaceTextID(CGI->heroh->objects[bonDescs[i].info2]->getNameTextID());
 			}
 			break;

+ 2 - 0
client/lobby/CLobbyScreen.cpp

@@ -120,6 +120,8 @@ void CLobbyScreen::toggleTab(std::shared_ptr<CIntObject> tab)
 		CSH->sendGuiAction(LobbyGuiAction::OPEN_SCENARIO_LIST);
 	else if(tab == tabRand)
 		CSH->sendGuiAction(LobbyGuiAction::OPEN_RANDOM_MAP_OPTIONS);
+	else if(tab == tabTurnOptions)
+		CSH->sendGuiAction(LobbyGuiAction::OPEN_TURN_OPTIONS);
 	CSelectionBase::toggleTab(tab);
 }
 

+ 6 - 3
client/lobby/CSelectionBase.cpp

@@ -135,7 +135,7 @@ InfoCard::InfoCard()
 	Rect descriptionRect(26, 149, 320, 115);
 	mapDescription = std::make_shared<CTextBox>("", descriptionRect, 1);
 	playerListBg = std::make_shared<CPicture>(ImagePath::builtin("CHATPLUG.bmp"), 16, 276);
-	chat = std::make_shared<CChatBox>(Rect(26, 132, 340, 132));
+	chat = std::make_shared<CChatBox>(Rect(18, 126, 335, 143));
 
 	if(SEL->screenType == ESelectionScreen::campaignList)
 	{
@@ -332,9 +332,12 @@ CChatBox::CChatBox(const Rect & rect)
 	setRedrawParent(true);
 
 	const int height = static_cast<int>(graphics->fonts[FONT_SMALL]->getLineHeight());
-	inputBox = std::make_shared<CTextInput>(Rect(0, rect.h - height, rect.w, height), EFonts::FONT_SMALL, 0);
+	Rect textInputArea(1, rect.h - height, rect.w - 1, height);
+	Rect chatHistoryArea(3, 1, rect.w - 3, rect.h - height - 1);
+	inputBackground = std::make_shared<TransparentFilledRectangle>(textInputArea, ColorRGBA(0,0,0,192));
+	inputBox = std::make_shared<CTextInput>(textInputArea, EFonts::FONT_SMALL, 0);
 	inputBox->removeUsedEvents(KEYBOARD);
-	chatHistory = std::make_shared<CTextBox>("", Rect(0, 0, rect.w, rect.h - height), 1);
+	chatHistory = std::make_shared<CTextBox>("", chatHistoryArea, 1);
 
 	chatHistory->label->color = Colors::GREEN;
 }

+ 2 - 0
client/lobby/CSelectionBase.h

@@ -33,6 +33,7 @@ class CChatBox;
 class CLabel;
 class CFlagBox;
 class CLabelGroup;
+class TransparentFilledRectangle;
 
 class ISelectionScreenInfo
 {
@@ -122,6 +123,7 @@ class CChatBox : public CIntObject
 public:
 	std::shared_ptr<CTextBox> chatHistory;
 	std::shared_ptr<CTextInput> inputBox;
+	std::shared_ptr<TransparentFilledRectangle> inputBackground;
 
 	CChatBox(const Rect & rect);
 

+ 1 - 1
client/lobby/OptionsTab.cpp

@@ -503,7 +503,7 @@ void OptionsTab::SelectionWindow::recreate()
 			int count = 0;
 			for(auto & elem : allowedHeroes)
 			{
-				CHero * type = VLC->heroh->objects[elem];
+				const CHero * type = elem.toHeroType();
 				if(type->heroClass->faction == selectedFaction)
 				{
 					count++;

+ 5 - 1
client/lobby/RandomMapTab.cpp

@@ -358,7 +358,11 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr<CMapGenOptions> opts)
 	}
 	for(auto r : VLC->roadTypeHandler->objects)
 	{
-		if(auto w = widget<CToggleButton>(r->getJsonKey()))
+		// Workaround for vcmi-extras bug
+		std::string jsonKey = r->getJsonKey();
+		std::string identifier = jsonKey.substr(jsonKey.find(':')+1);
+
+		if(auto w = widget<CToggleButton>(identifier))
 		{
 			w->setSelected(opts->isRoadEnabled(r->getId()));
 		}

+ 2 - 3
client/mainmenu/CHighScoreScreen.cpp

@@ -29,8 +29,7 @@
 #include "../../lib/CCreatureHandler.h"
 #include "../../lib/constants/EntityIdentifiers.h"
 #include "../../lib/TextOperations.h"
-
-#include "vstd/DateUtils.h"
+#include "../../lib/Languages.h"
 
 auto HighScoreCalculation::calculate()
 {
@@ -264,7 +263,7 @@ int CHighScoreInputScreen::addEntry(std::string text) {
 		newNode["scenarioName"].String() = calc.calculate().cheater ? CGI->generaltexth->translate("core.genrltxt.260") : calc.parameters[0].scenarioName;
 	newNode["days"].Integer() = calc.calculate().sumDays;
 	newNode["points"].Integer() = calc.calculate().cheater ? 0 : calc.calculate().total;
-	newNode["datetime"].String() = vstd::getFormattedDateTime(std::time(nullptr));
+	newNode["datetime"].String() = TextOperations::getFormattedDateTimeLocal(std::time(nullptr));
 	newNode["posFlag"].Bool() = true;
 
 	baseNode.push_back(newNode);

+ 20 - 3
client/mainmenu/CMainMenu.cpp

@@ -382,12 +382,29 @@ void CMainMenu::openCampaignLobby(std::shared_ptr<CampaignState> campaign)
 
 void CMainMenu::openCampaignScreen(std::string name)
 {
-	if(vstd::contains(CMainMenuConfig::get().getCampaigns().Struct(), name))
+	auto const & config = CMainMenuConfig::get().getCampaigns();
+
+	if(!vstd::contains(config.Struct(), name))
 	{
-		GH.windows().createAndPushWindow<CCampaignScreen>(CMainMenuConfig::get().getCampaigns(), name);
+		logGlobal->error("Unknown campaign set: %s", name);
 		return;
 	}
-	logGlobal->error("Unknown campaign set: %s", name);
+
+	bool campaignsFound = true;
+	for (auto const & entry : config[name]["items"].Vector())
+	{
+		ResourcePath resourceID(entry["file"].String(), EResType::CAMPAIGN);
+		if (!CResourceHandler::get()->existsResource(resourceID))
+			campaignsFound = false;
+	}
+
+	if (!campaignsFound)
+	{
+		CInfoWindow::showInfoDialog(CGI->generaltexth->translate("vcmi.client.errors.missingCampaigns"), std::vector<std::shared_ptr<CComponent>>(), PlayerColor(1));
+		return;
+	}
+
+	GH.windows().createAndPushWindow<CCampaignScreen>(config, name);
 }
 
 void CMainMenu::startTutorial()

+ 30 - 6
client/widgets/CArtifactHolder.cpp

@@ -139,7 +139,7 @@ void CCommanderArtPlace::clickPressed(const Point & cursorPosition)
 		LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.commanderWindow.artifactMessage"), [this]() { returnArtToHeroCallback(); }, []() {});
 }
 
-void CCommanderArtPlace::showPopupWindow(const Point & cursorPosition)
+void CCommanderArtPlace::showPopupWindow(const Point& cursorPosition)
 {
 	if(ourArt && text.size())
 		CArtPlace::showPopupWindow(cursorPosition);
@@ -180,16 +180,25 @@ bool CArtPlace::isSelected() const
 	return selection->visible;
 }
 
-void CHeroArtPlace::clickPressed(const Point & cursorPosition)
+void CArtPlace::clickPressed(const Point & cursorPosition)
 {
-	if(leftClickCallback)
-		leftClickCallback(*this);
+	if(clickPressedCallback)
+		clickPressedCallback(*this, cursorPosition);
 }
 
-void CHeroArtPlace::showPopupWindow(const Point & cursorPosition)
+void CArtPlace::showPopupWindow(const Point & cursorPosition)
 {
 	if(showPopupCallback)
-		showPopupCallback(*this);
+		showPopupCallback(*this, cursorPosition);
+}
+
+void CArtPlace::gesture(bool on, const Point & initialPosition, const Point & finalPosition)
+{
+	if(!on)
+		return;
+
+	if(gestureCallback)
+		gestureCallback(*this, initialPosition);
 }
 
 void CArtPlace::showAll(Canvas & to)
@@ -216,6 +225,21 @@ void CArtPlace::setArtifact(const CArtifactInstance * art)
 	}
 }
 
+void CArtPlace::setClickPressedCallback(ClickFunctor callback)
+{
+	clickPressedCallback = callback;
+}
+
+void CArtPlace::setShowPopupCallback(ClickFunctor callback)
+{
+	showPopupCallback = callback;
+}
+
+void CArtPlace::setGestureCallback(ClickFunctor callback)
+{
+	gestureCallback = callback;
+}
+
 void CHeroArtPlace::addCombinedArtInfo(std::map<const CArtifact*, int> & arts)
 {
 	for(const auto & combinedArt : arts)

+ 14 - 9
client/widgets/CArtifactHolder.h

@@ -32,14 +32,24 @@ public:
 class CArtPlace : public LRClickableAreaWTextComp
 {
 public:
+	using ClickFunctor = std::function<void(CArtPlace&, const Point&)>;
+
+	ArtifactPosition slot;
+
 	CArtPlace(Point position, const CArtifactInstance * art = nullptr);
-	const CArtifactInstance* getArt();
+	const CArtifactInstance * getArt();
 	void lockSlot(bool on);
 	bool isLocked() const;
 	void selectSlot(bool on);
 	bool isSelected() const;
 	void showAll(Canvas & to) override;
 	void setArtifact(const CArtifactInstance * art);
+	void setClickPressedCallback(ClickFunctor callback);
+	void setShowPopupCallback(ClickFunctor callback);
+	void setGestureCallback(ClickFunctor callback);
+	void clickPressed(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
+	void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override;
 
 protected:
 	std::shared_ptr<CAnimImage> image;
@@ -47,6 +57,9 @@ protected:
 	int imageIndex;
 	std::shared_ptr<CAnimImage> selection;
 	bool locked;
+	ClickFunctor clickPressedCallback;
+	ClickFunctor showPopupCallback;
+	ClickFunctor gestureCallback;
 
 	void setInternals(const CArtifactInstance * artInst);
 };
@@ -68,15 +81,7 @@ public:
 class CHeroArtPlace: public CArtPlace
 {
 public:
-	using ClickFunctor = std::function<void(CHeroArtPlace&)>;
-
-	ArtifactPosition slot;
-	ClickFunctor leftClickCallback;
-	ClickFunctor showPopupCallback;
-
 	CHeroArtPlace(Point position, const CArtifactInstance * art = nullptr);
-	void clickPressed(const Point & cursorPosition) override;
-	void showPopupWindow(const Point & cursorPosition) override;
 	void addCombinedArtInfo(std::map<const CArtifact*, int> & arts);
 };
 

+ 3 - 3
client/widgets/CArtifactsOfHeroAltar.cpp

@@ -23,8 +23,8 @@ CArtifactsOfHeroAltar::CArtifactsOfHeroAltar(const Point & position)
 	: visibleArtSet(ArtBearer::ArtBearer::HERO)
 {
 	init(
-		std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1), 
-		std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1), 
+		std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2),
+		std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2),
 		position,
 		std::bind(&CArtifactsOfHeroAltar::scrollBackpack, this, _1));
 	pickedArtFromSlot = ArtifactPosition::PRE_FIRST;
@@ -69,7 +69,7 @@ void CArtifactsOfHeroAltar::scrollBackpack(int offset)
 	redraw();
 }
 
-void CArtifactsOfHeroAltar::pickUpArtifact(CHeroArtPlace & artPlace)
+void CArtifactsOfHeroAltar::pickUpArtifact(CArtPlace & artPlace)
 {
 	if(const auto art = artPlace.getArt())
 	{

+ 1 - 1
client/widgets/CArtifactsOfHeroAltar.h

@@ -26,7 +26,7 @@ public:
 	void updateWornSlots() override;
 	void updateBackpackSlots() override;
 	void scrollBackpack(int offset) override;
-	void pickUpArtifact(CHeroArtPlace & artPlace);
+	void pickUpArtifact(CArtPlace & artPlace);
 	void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc);
 	void pickedArtMoveToAltar(const ArtifactPosition & slot);
 	void deleteFromVisible(const CArtifactInstance * artInst);

+ 148 - 40
client/widgets/CArtifactsOfHeroBackpack.cpp

@@ -17,37 +17,91 @@
 #include "ObjectLists.h"
 
 #include "../CPlayerInterface.h"
+#include "../../lib/ArtifactUtils.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/networkPacks/ArtifactLocation.h"
 
 #include "../../CCallback.h"
 
-CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack(const Point & position)
+CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack(size_t slotsColumnsMax, size_t slotsRowsMax)
+	: slotsColumnsMax(slotsColumnsMax)
+	, slotsRowsMax(slotsRowsMax)
 {
-	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
-	pos += position;
 	setRedrawParent(true);
+}
 
+CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack()
+	: CArtifactsOfHeroBackpack(8, 8)
+{
 	const auto backpackCap = VLC->settings()->getInteger(EGameSettings::HEROES_BACKPACK_CAP);
-	auto visibleCapacityMax = HERO_BACKPACK_WINDOW_SLOT_ROWS * HERO_BACKPACK_WINDOW_SLOT_COLUMNS;
+	auto visibleCapacityMax = slotsRowsMax * slotsColumnsMax;
 	if(backpackCap >= 0)
 		visibleCapacityMax = visibleCapacityMax > backpackCap ? backpackCap : visibleCapacityMax;
 
-	backpack.resize(visibleCapacityMax);
+	initAOHbackpack(visibleCapacityMax, backpackCap < 0 || visibleCapacityMax < backpackCap);
+}
+
+void CArtifactsOfHeroBackpack::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc)
+{
+	LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc);
+}
+
+void CArtifactsOfHeroBackpack::pickUpArtifact(CArtPlace & artPlace)
+{
+	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot),
+		ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS));
+}
+
+void CArtifactsOfHeroBackpack::scrollBackpack(int offset)
+{
+	if(backpackListBox)
+		backpackListBox->resize(getActiveSlotRowsNum());
+	backpackPos += offset;
+	auto slot = ArtifactPosition::BACKPACK_START + backpackPos;
+	for(auto artPlace : backpack)
+	{
+		setSlotData(artPlace, slot, *curHero);
+		slot = slot + 1;
+	}
+	redraw();
+}
+
+void CArtifactsOfHeroBackpack::updateBackpackSlots()
+{
+	if(backpackListBox)
+		backpackListBox->resize(getActiveSlotRowsNum());
+	CArtifactsOfHeroBase::updateBackpackSlots();
+}
+
+size_t CArtifactsOfHeroBackpack::getActiveSlotRowsNum()
+{
+	return (curHero->artifactsInBackpack.size() + slotsColumnsMax - 1) / slotsColumnsMax;
+}
+
+size_t CArtifactsOfHeroBackpack::getSlotsNum()
+{
+	return backpack.size();
+}
+
+void CArtifactsOfHeroBackpack::initAOHbackpack(size_t slots, bool slider)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	backpack.resize(slots);
 	size_t artPlaceIdx = 0;
 	for(auto & artPlace : backpack)
 	{
-		const auto pos = Point(slotSizeWithMargin * (artPlaceIdx % HERO_BACKPACK_WINDOW_SLOT_COLUMNS),
-			slotSizeWithMargin * (artPlaceIdx / HERO_BACKPACK_WINDOW_SLOT_COLUMNS));
+		const auto pos = Point(slotSizeWithMargin * (artPlaceIdx % slotsColumnsMax),
+			slotSizeWithMargin * (artPlaceIdx / slotsColumnsMax));
 		backpackSlotsBackgrounds.emplace_back(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/artifactSlotEmpty"), pos));
 		artPlace = std::make_shared<CHeroArtPlace>(pos);
 		artPlace->setArtifact(nullptr);
-		artPlace->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1);
-		artPlace->showPopupCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1);
+		artPlace->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2));
+		artPlace->setShowPopupCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2));
 		artPlaceIdx++;
 	}
 
-	if(backpackCap < 0 || visibleCapacityMax < backpackCap)
+	if(slider)
 	{
 		auto onCreate = [](size_t index) -> std::shared_ptr<CIntObject>
 		{
@@ -55,57 +109,111 @@ CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack(const Point & position)
 		};
 		CListBoxWithCallback::MovedPosCallback posMoved = [this](size_t pos) -> void
 		{
-			scrollBackpack(static_cast<int>(pos) * HERO_BACKPACK_WINDOW_SLOT_COLUMNS - backpackPos);
+			scrollBackpack(static_cast<int>(pos) * slotsColumnsMax - backpackPos);
 		};
 		backpackListBox = std::make_shared<CListBoxWithCallback>(
-				posMoved, onCreate, Point(0, 0), Point(0, 0), HERO_BACKPACK_WINDOW_SLOT_ROWS, 0, 0, 1,
-				Rect(HERO_BACKPACK_WINDOW_SLOT_COLUMNS * slotSizeWithMargin + sliderPosOffsetX, 0, HERO_BACKPACK_WINDOW_SLOT_ROWS * slotSizeWithMargin - 2, 0));
+			posMoved, onCreate, Point(0, 0), Point(0, 0), slotsRowsMax, 0, 0, 1,
+			Rect(slotsColumnsMax * slotSizeWithMargin + sliderPosOffsetX, 0, slotsRowsMax * slotSizeWithMargin - 2, 0));
 	}
 
-	pos.w = visibleCapacityMax > HERO_BACKPACK_WINDOW_SLOT_COLUMNS ? HERO_BACKPACK_WINDOW_SLOT_COLUMNS : visibleCapacityMax;
+	pos.w = slots > slotsColumnsMax ? slotsColumnsMax : slots;
 	pos.w *= slotSizeWithMargin;
-	if(backpackListBox)
+	if(slider)
 		pos.w += sliderPosOffsetX + 16; // 16 is slider width. TODO: get it from CListBox directly;
-
-	pos.h = (visibleCapacityMax / HERO_BACKPACK_WINDOW_SLOT_COLUMNS);
-	if(visibleCapacityMax % HERO_BACKPACK_WINDOW_SLOT_COLUMNS != 0)
-		pos.h += 1;
-	pos.h *= slotSizeWithMargin;
+	pos.h = calcRows(slots) * slotSizeWithMargin;
 }
 
-void CArtifactsOfHeroBackpack::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc)
+size_t CArtifactsOfHeroBackpack::calcRows(size_t slots)
 {
-	LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc);
+	size_t rows = 0;
+	if(slotsColumnsMax != 0)
+	{
+		rows = slots / slotsColumnsMax;
+		if(slots % slotsColumnsMax != 0)
+			rows += 1;
+	}
+	return rows;
 }
 
-void CArtifactsOfHeroBackpack::pickUpArtifact(CHeroArtPlace & artPlace)
+CArtifactsOfHeroQuickBackpack::CArtifactsOfHeroQuickBackpack(const ArtifactPosition filterBySlot)
+	: CArtifactsOfHeroBackpack(0, 0)
 {
-	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot),
-		ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS));
+	assert(filterBySlot != ArtifactPosition::FIRST_AVAILABLE);
+
+	if(!ArtifactUtils::isSlotEquipment(filterBySlot))
+		return;
+
+	this->filterBySlot = filterBySlot;
 }
 
-void CArtifactsOfHeroBackpack::scrollBackpack(int offset)
+void CArtifactsOfHeroQuickBackpack::setHero(const CGHeroInstance * hero)
 {
-	if(backpackListBox)
-		backpackListBox->resize(getActiveSlotRowsNum());
-	backpackPos += offset;
-	auto slot = ArtifactPosition::BACKPACK_START + backpackPos;
-	for(auto artPlace : backpack)
+	if(curHero == hero)
+		return;
+	
+	curHero = hero;
+	if(curHero)
 	{
-		setSlotData(artPlace, slot, *curHero);
-		slot = slot + 1;
+		ArtifactID artInSlotId = ArtifactID::NONE;
+		SpellID scrollInSlotSpellId = SpellID::NONE;
+		if(auto artInSlot = curHero->getArt(filterBySlot))
+		{
+			artInSlotId = artInSlot->getTypeId();
+			scrollInSlotSpellId = artInSlot->getScrollSpellID();
+		}
+
+		std::map<const ArtifactID, const CArtifactInstance*> filteredArts;
+		for(auto & slotInfo : curHero->artifactsInBackpack)
+			if(slotInfo.artifact->getTypeId() != artInSlotId &&	!slotInfo.artifact->isScroll() &&
+				slotInfo.artifact->artType->canBePutAt(curHero, filterBySlot, true))
+			{
+				filteredArts.insert(std::pair(slotInfo.artifact->getTypeId(), slotInfo.artifact));
+			}
+
+		std::map<const SpellID, const CArtifactInstance*> filteredScrolls;
+		if(filterBySlot == ArtifactPosition::MISC1 || filterBySlot == ArtifactPosition::MISC2 || filterBySlot == ArtifactPosition::MISC3 ||
+			filterBySlot == ArtifactPosition::MISC4 || filterBySlot == ArtifactPosition::MISC5)
+		{
+			for(auto & slotInfo : curHero->artifactsInBackpack)
+			{
+				if(slotInfo.artifact->isScroll() && slotInfo.artifact->getScrollSpellID() != scrollInSlotSpellId)
+					filteredScrolls.insert(std::pair(slotInfo.artifact->getScrollSpellID(), slotInfo.artifact));
+			}
+		}
+
+		backpack.clear();
+		auto requiredSlots = filteredArts.size() + filteredScrolls.size();
+		slotsColumnsMax = ceilf(sqrtf(requiredSlots));
+		slotsRowsMax = calcRows(requiredSlots);
+		initAOHbackpack(requiredSlots, false);
+		auto artPlace = backpack.begin();
+		for(auto & art : filteredArts)
+			setSlotData(*artPlace++, curHero->getSlotByInstance(art.second), *curHero);
+		for(auto & art : filteredScrolls)
+			setSlotData(*artPlace++, curHero->getSlotByInstance(art.second), *curHero);
 	}
-	redraw();
 }
 
-void CArtifactsOfHeroBackpack::updateBackpackSlots()
+ArtifactPosition CArtifactsOfHeroQuickBackpack::getFilterSlot()
 {
-	if(backpackListBox)
-		backpackListBox->resize(getActiveSlotRowsNum());
-	CArtifactsOfHeroBase::updateBackpackSlots();
+	return filterBySlot;
 }
 
-size_t CArtifactsOfHeroBackpack::getActiveSlotRowsNum()
+void CArtifactsOfHeroQuickBackpack::selectSlotAt(const Point & position)
 {
-	return (curHero->artifactsInBackpack.size() + HERO_BACKPACK_WINDOW_SLOT_COLUMNS - 1) / HERO_BACKPACK_WINDOW_SLOT_COLUMNS;
+	for(auto & artPlace : backpack)
+		artPlace->selectSlot(artPlace->pos.isInside(position));
 }
+
+void CArtifactsOfHeroQuickBackpack::swapSelected()
+{
+	ArtifactLocation backpackLoc(curHero->id, ArtifactPosition::PRE_FIRST);
+	for(auto & artPlace : backpack)
+		if(artPlace->isSelected())
+		{
+			backpackLoc.slot = artPlace->slot;
+			break;
+		}
+	if(backpackLoc.slot != ArtifactPosition::PRE_FIRST && filterBySlot != ArtifactPosition::PRE_FIRST && curHero)
+		swapArtifacts(backpackLoc, ArtifactLocation(curHero->id, filterBySlot));
+}

+ 24 - 6
client/widgets/CArtifactsOfHeroBackpack.h

@@ -22,18 +22,36 @@ class CListBoxWithCallback;
 class CArtifactsOfHeroBackpack : public CArtifactsOfHeroBase
 {
 public:
-	CArtifactsOfHeroBackpack(const Point & position);
+	CArtifactsOfHeroBackpack(size_t slotsColumnsMax, size_t slotsRowsMax);
+	CArtifactsOfHeroBackpack();
 	void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc);
-	void pickUpArtifact(CHeroArtPlace & artPlace);
+	void pickUpArtifact(CArtPlace & artPlace);
 	void scrollBackpack(int offset) override;
 	void updateBackpackSlots() override;
 	size_t getActiveSlotRowsNum();
+	size_t getSlotsNum();
 
-private:
+protected:
 	std::shared_ptr<CListBoxWithCallback> backpackListBox;
 	std::vector<std::shared_ptr<CPicture>> backpackSlotsBackgrounds;
-	const size_t HERO_BACKPACK_WINDOW_SLOT_COLUMNS = 8;
-	const size_t HERO_BACKPACK_WINDOW_SLOT_ROWS = 8;
+	size_t slotsColumnsMax;
+	size_t slotsRowsMax;
 	const int slotSizeWithMargin = 46;
-	const int sliderPosOffsetX = 10;
+	const int sliderPosOffsetX = 5;
+
+	void initAOHbackpack(size_t slots, bool slider);
+	size_t calcRows(size_t slots);
+};
+
+class CArtifactsOfHeroQuickBackpack : public CArtifactsOfHeroBackpack
+{
+public:
+	CArtifactsOfHeroQuickBackpack(const ArtifactPosition filterBySlot);
+	void setHero(const CGHeroInstance * hero);
+	ArtifactPosition getFilterSlot();
+	void selectSlotAt(const Point & position);
+	void swapSelected();
+
+private:
+	ArtifactPosition filterBySlot;
 };

+ 26 - 11
client/widgets/CArtifactsOfHeroBase.cpp

@@ -56,8 +56,8 @@ void CArtifactsOfHeroBase::setPutBackPickedArtifactCallback(PutBackPickedArtCall
 }
 
 void CArtifactsOfHeroBase::init(
-	CHeroArtPlace::ClickFunctor lClickCallback,
-	CHeroArtPlace::ClickFunctor showPopupCallback,
+	CArtPlace::ClickFunctor lClickCallback,
+	CArtPlace::ClickFunctor showPopupCallback,
 	const Point & position,
 	BpackScrollFunctor scrollCallback)
 {
@@ -78,14 +78,14 @@ void CArtifactsOfHeroBase::init(
 	{
 		artPlace.second->slot = artPlace.first;
 		artPlace.second->setArtifact(nullptr);
-		artPlace.second->leftClickCallback = lClickCallback;
-		artPlace.second->showPopupCallback = showPopupCallback;
+		artPlace.second->setClickPressedCallback(lClickCallback);
+		artPlace.second->setShowPopupCallback(showPopupCallback);
 	}
 	for(auto artPlace : backpack)
 	{
 		artPlace->setArtifact(nullptr);
-		artPlace->leftClickCallback = lClickCallback;
-		artPlace->showPopupCallback = showPopupCallback;
+		artPlace->setClickPressedCallback(lClickCallback);
+		artPlace->setShowPopupCallback(showPopupCallback);
 	}
 	leftBackpackRoll = std::make_shared<CButton>(Point(379, 364), AnimationPath::builtin("hsbtns3.def"), CButton::tooltip(), [scrollCallback]() {scrollCallback(-1);}, EShortcut::MOVE_LEFT);
 	rightBackpackRoll = std::make_shared<CButton>(Point(632, 364), AnimationPath::builtin("hsbtns5.def"), CButton::tooltip(), [scrollCallback]() {scrollCallback(+1);}, EShortcut::MOVE_RIGHT);
@@ -95,16 +95,22 @@ void CArtifactsOfHeroBase::init(
 	setRedrawParent(true);
 }
 
-void CArtifactsOfHeroBase::leftClickArtPlace(CHeroArtPlace & artPlace)
+void CArtifactsOfHeroBase::clickPrassedArtPlace(CArtPlace & artPlace, const Point & cursorPosition)
 {
-	if(leftClickCallback)
-		leftClickCallback(*this, artPlace);
+	if(clickPressedCallback)
+		clickPressedCallback(*this, artPlace, cursorPosition);
 }
 
-void CArtifactsOfHeroBase::rightClickArtPlace(CHeroArtPlace & artPlace)
+void CArtifactsOfHeroBase::showPopupArtPlace(CArtPlace & artPlace, const Point & cursorPosition)
 {
 	if(showPopupCallback)
-		showPopupCallback(*this, artPlace);
+		showPopupCallback(*this, artPlace, cursorPosition);
+}
+
+void CArtifactsOfHeroBase::gestureArtPlace(CArtPlace & artPlace, const Point & cursorPosition)
+{
+	if(gestureCallback)
+		gestureCallback(*this, artPlace, cursorPosition);
 }
 
 void CArtifactsOfHeroBase::setHero(const CGHeroInstance * hero)
@@ -241,6 +247,15 @@ const CArtifactInstance * CArtifactsOfHeroBase::getPickedArtifact()
 		return curHero->getArt(ArtifactPosition::TRANSITION_POS);
 }
 
+void CArtifactsOfHeroBase::addGestureCallback(CArtPlace::ClickFunctor callback)
+{
+	for(auto & artPlace : artWorn)
+	{
+		artPlace.second->setGestureCallback(callback);
+		artPlace.second->addUsedEvents(GESTURE);
+	}
+}
+
 void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosition & slot, const CArtifactSet & artSet)
 {
 	// Spurious call from artifactMoved in attempt to update hidden backpack slot

+ 7 - 4
client/widgets/CArtifactsOfHeroBase.h

@@ -21,17 +21,19 @@ protected:
 
 public:
 	using ArtPlaceMap = std::map<ArtifactPosition, ArtPlacePtr>;
-	using ClickFunctor = std::function<void(CArtifactsOfHeroBase&, CHeroArtPlace&)>;
+	using ClickFunctor = std::function<void(CArtifactsOfHeroBase&, CArtPlace&, const Point&)>;
 	using PutBackPickedArtCallback = std::function<void()>;
 
-	ClickFunctor leftClickCallback;
+	ClickFunctor clickPressedCallback;
 	ClickFunctor showPopupCallback;
+	ClickFunctor gestureCallback;
 	
 	CArtifactsOfHeroBase();
 	virtual void putBackPickedArtifact();
 	virtual void setPutBackPickedArtifactCallback(PutBackPickedArtCallback callback);
-	virtual void leftClickArtPlace(CHeroArtPlace & artPlace);
-	virtual void rightClickArtPlace(CHeroArtPlace & artPlace);
+	virtual void clickPrassedArtPlace(CArtPlace & artPlace, const Point & cursorPosition);
+	virtual void showPopupArtPlace(CArtPlace & artPlace, const Point & cursorPosition);
+	virtual void gestureArtPlace(CArtPlace & artPlace, const Point & cursorPosition);
 	virtual void setHero(const CGHeroInstance * hero);
 	virtual const CGHeroInstance * getHero() const;
 	virtual void scrollBackpack(int offset);
@@ -42,6 +44,7 @@ public:
 	virtual void updateBackpackSlots();
 	virtual void updateSlot(const ArtifactPosition & slot);
 	virtual const CArtifactInstance * getPickedArtifact();
+	void addGestureCallback(CArtPlace::ClickFunctor callback);
 
 protected:
 	const CGHeroInstance * curHero;

+ 6 - 5
client/widgets/CArtifactsOfHeroKingdom.cpp

@@ -30,14 +30,15 @@ CArtifactsOfHeroKingdom::CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vecto
 	{
 		artPlace.second->slot = artPlace.first;
 		artPlace.second->setArtifact(nullptr);
-		artPlace.second->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1);
-		artPlace.second->showPopupCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1);
+		artPlace.second->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2));
+		artPlace.second->setShowPopupCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2));
 	}
+	addGestureCallback(std::bind(&CArtifactsOfHeroBase::gestureArtPlace, this, _1, _2));
 	for(auto artPlace : backpack)
 	{
 		artPlace->setArtifact(nullptr);
-		artPlace->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1);
-		artPlace->showPopupCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1);
+		artPlace->setClickPressedCallback(std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2));
+		artPlace->setShowPopupCallback(std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2));
 	}
 	leftBackpackRoll->addCallback(std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, -1));
 	rightBackpackRoll->addCallback(std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, +1));
@@ -55,7 +56,7 @@ void CArtifactsOfHeroKingdom::swapArtifacts(const ArtifactLocation & srcLoc, con
 	LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc);
 }
 
-void CArtifactsOfHeroKingdom::pickUpArtifact(CHeroArtPlace & artPlace)
+void CArtifactsOfHeroKingdom::pickUpArtifact(CArtPlace & artPlace)
 {
 	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot),
 		ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS));

+ 1 - 1
client/widgets/CArtifactsOfHeroKingdom.h

@@ -24,5 +24,5 @@ public:
 		std::shared_ptr<CButton> leftScroll, std::shared_ptr<CButton> rightScroll);
 	~CArtifactsOfHeroKingdom();
 	void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc);
-	void pickUpArtifact(CHeroArtPlace & artPlace);
+	void pickUpArtifact(CArtPlace & artPlace);
 };

+ 4 - 3
client/widgets/CArtifactsOfHeroMain.cpp

@@ -19,10 +19,11 @@
 CArtifactsOfHeroMain::CArtifactsOfHeroMain(const Point & position)
 {
 	init(
-		std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1),
-		std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1),
+		std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2),
+		std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2),
 		position,
 		std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, _1));
+	addGestureCallback(std::bind(&CArtifactsOfHeroBase::gestureArtPlace, this, _1, _2));
 }
 
 CArtifactsOfHeroMain::~CArtifactsOfHeroMain()
@@ -35,7 +36,7 @@ void CArtifactsOfHeroMain::swapArtifacts(const ArtifactLocation & srcLoc, const
 	LOCPLINT->cb->swapArtifacts(srcLoc, dstLoc);
 }
 
-void CArtifactsOfHeroMain::pickUpArtifact(CHeroArtPlace & artPlace)
+void CArtifactsOfHeroMain::pickUpArtifact(CArtPlace & artPlace)
 {
 	LOCPLINT->cb->swapArtifacts(ArtifactLocation(curHero->id, artPlace.slot),
 		ArtifactLocation(curHero->id, ArtifactPosition::TRANSITION_POS));

+ 1 - 1
client/widgets/CArtifactsOfHeroMain.h

@@ -23,5 +23,5 @@ public:
 	CArtifactsOfHeroMain(const Point & position);
 	~CArtifactsOfHeroMain();
 	void swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc);
-	void pickUpArtifact(CHeroArtPlace & artPlace);
+	void pickUpArtifact(CArtPlace & artPlace);
 };

+ 2 - 2
client/widgets/CArtifactsOfHeroMarket.cpp

@@ -15,8 +15,8 @@
 CArtifactsOfHeroMarket::CArtifactsOfHeroMarket(const Point & position)
 {
 	init(
-		std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1), 
-		std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1), 
+		std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2),
+		std::bind(&CArtifactsOfHeroBase::showPopupArtPlace, this, _1, _2),
 		position,
 		std::bind(&CArtifactsOfHeroMarket::scrollBackpack, this, _1));
 };

+ 1 - 1
client/widgets/CArtifactsOfHeroMarket.h

@@ -14,7 +14,7 @@
 class CArtifactsOfHeroMarket : public CArtifactsOfHeroBase
 {
 public:
-	std::function<void(CHeroArtPlace*)> selectArtCallback;
+	std::function<void(CArtPlace*)> selectArtCallback;
 
 	CArtifactsOfHeroMarket(const Point & position);
 	void scrollBackpack(int offset) override;

+ 46 - 12
client/widgets/CWindowWithArtifacts.cpp

@@ -23,6 +23,7 @@
 #include "../windows/CHeroWindow.h"
 #include "../windows/CSpellWindow.h"
 #include "../windows/GUIClasses.h"
+#include "../windows/CHeroBackpackWindow.h"
 #include "../CPlayerInterface.h"
 #include "../CGameInfo.h"
 
@@ -39,18 +40,19 @@ void CWindowWithArtifacts::addSet(CArtifactsOfHeroPtr artSet)
 
 void CWindowWithArtifacts::addSetAndCallbacks(CArtifactsOfHeroPtr artSet)
 {
-	CArtifactsOfHeroBase::PutBackPickedArtCallback artPutBackHandler = []() -> void
+	CArtifactsOfHeroBase::PutBackPickedArtCallback artPutBackFunctor = []() -> void
 	{
 		CCS->curh->dragAndDropCursor(nullptr);
 	};
 
 	addSet(artSet);
-	std::visit([this, artPutBackHandler](auto artSetWeak)
+	std::visit([this, artPutBackFunctor](auto artSetWeak)
 		{
 			auto artSet = artSetWeak.lock();
-			artSet->leftClickCallback = std::bind(&CWindowWithArtifacts::leftClickArtPlaceHero, this, _1, _2);
-			artSet->showPopupCallback = std::bind(&CWindowWithArtifacts::rightClickArtPlaceHero, this, _1, _2);
-			artSet->setPutBackPickedArtifactCallback(artPutBackHandler);
+			artSet->clickPressedCallback = std::bind(&CWindowWithArtifacts::clickPressedArtPlaceHero, this, _1, _2, _3);
+			artSet->showPopupCallback = std::bind(&CWindowWithArtifacts::showPopupArtPlaceHero, this, _1, _2, _3);
+			artSet->gestureCallback = std::bind(&CWindowWithArtifacts::gestureArtPlaceHero, this, _1, _2, _3);
+			artSet->setPutBackPickedArtifactCallback(artPutBackFunctor);
 		}, artSet);
 }
 
@@ -77,7 +79,7 @@ const CArtifactInstance * CWindowWithArtifacts::getPickedArtifact()
 		return nullptr;
 }
 
-void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst, CHeroArtPlace & artPlace)
+void CWindowWithArtifacts::clickPressedArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition)
 {
 	const auto artSetWeak = findAOHbyRef(artsInst);
 	assert(artSetWeak.has_value());
@@ -85,7 +87,7 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst
 	if(artPlace.isLocked())
 		return;
 
-	const auto checkSpecialArts = [](const CGHeroInstance * hero, CHeroArtPlace & artPlace) -> bool
+	const auto checkSpecialArts = [](const CGHeroInstance * hero, CArtPlace & artPlace) -> bool
 	{
 		if(artPlace.getArt()->getTypeId() == ArtifactID::SPELLBOOK)
 		{
@@ -206,10 +208,18 @@ void CWindowWithArtifacts::leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst
 					}
 				}
 			}
+			else if constexpr(std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroQuickBackpack>>)
+			{
+				const auto hero = artSetPtr->getHero();
+				artSetPtr->swapArtifacts(ArtifactLocation(hero->id, artPlace.slot),
+					ArtifactLocation(hero->id, artSetPtr->getFilterSlot()));
+				if(closeCallback)
+					closeCallback();
+			}
 		}, artSetWeak.value());
 }
 
-void CWindowWithArtifacts::rightClickArtPlaceHero(CArtifactsOfHeroBase & artsInst, CHeroArtPlace & artPlace)
+void CWindowWithArtifacts::showPopupArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition)
 {
 	const auto artSetWeak = findAOHbyRef(artsInst);
 	assert(artSetWeak.has_value());
@@ -218,7 +228,7 @@ void CWindowWithArtifacts::rightClickArtPlaceHero(CArtifactsOfHeroBase & artsIns
 		return;
 
 	std::visit(
-		[&artPlace](auto artSetWeak) -> void
+		[&artPlace, &cursorPosition](auto artSetWeak) -> void
 		{
 			const auto artSetPtr = artSetWeak.lock();
 
@@ -239,16 +249,40 @@ void CWindowWithArtifacts::rightClickArtPlaceHero(CArtifactsOfHeroBase & artsIns
 						return;
 					}
 					if(artPlace.text.size())
-						artPlace.LRClickableAreaWTextComp::showPopupWindow(GH.getCursorPosition());
+						artPlace.LRClickableAreaWTextComp::showPopupWindow(cursorPosition);
 				}
 			}
 			// Altar window, Market window right click handler
 			else if constexpr(
 				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroAltar>> ||
-				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroMarket>>)
+				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroMarket>> ||
+				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroQuickBackpack>>)
 			{
 				if(artPlace.getArt() && artPlace.text.size())
-					artPlace.LRClickableAreaWTextComp::showPopupWindow(GH.getCursorPosition());
+					artPlace.LRClickableAreaWTextComp::showPopupWindow(cursorPosition);
+			}
+		}, artSetWeak.value());
+}
+
+void CWindowWithArtifacts::gestureArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition)
+{
+	const auto artSetWeak = findAOHbyRef(artsInst);
+	assert(artSetWeak.has_value());
+	if(artPlace.isLocked())
+		return;
+
+	std::visit(
+		[&artPlace, cursorPosition](auto artSetWeak) -> void
+		{
+			const auto artSetPtr = artSetWeak.lock();
+			if constexpr(
+				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroMain>> ||
+				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroKingdom>>)
+			{
+				GH.windows().createAndPushWindow<CHeroQuickBackpackWindow>(artSetPtr->getHero(), artPlace.slot);
+				auto backpackWindow = GH.windows().topWindow<CHeroQuickBackpackWindow>();
+				backpackWindow->moveTo(cursorPosition - Point(1, 1));
+				backpackWindow->fitToScreen(15);
 			}
 		}, artSetWeak.value());
 }

+ 5 - 3
client/widgets/CWindowWithArtifacts.h

@@ -24,7 +24,8 @@ public:
 		std::weak_ptr<CArtifactsOfHeroAltar>,
 		std::weak_ptr<CArtifactsOfHeroKingdom>,
 		std::weak_ptr<CArtifactsOfHeroMain>,
-		std::weak_ptr<CArtifactsOfHeroBackpack>>;
+		std::weak_ptr<CArtifactsOfHeroBackpack>,
+		std::weak_ptr<CArtifactsOfHeroQuickBackpack>>;
 	using CloseCallback = std::function<void()>;
 
 	void addSet(CArtifactsOfHeroPtr artSet);
@@ -32,8 +33,9 @@ public:
 	void addCloseCallback(CloseCallback callback);
 	const CGHeroInstance * getHeroPickedArtifact();
 	const CArtifactInstance * getPickedArtifact();
-	void leftClickArtPlaceHero(CArtifactsOfHeroBase & artsInst, CHeroArtPlace & artPlace);
-	void rightClickArtPlaceHero(CArtifactsOfHeroBase & artsInst, CHeroArtPlace & artPlace);
+	void clickPressedArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition);
+	void showPopupArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition);
+	void gestureArtPlaceHero(CArtifactsOfHeroBase & artsInst, CArtPlace & artPlace, const Point & cursorPosition);
 
 	void artifactRemoved(const ArtifactLocation & artLoc) override;
 	void artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) override;

+ 14 - 2
client/widgets/MiscWidgets.cpp

@@ -121,9 +121,10 @@ void LRClickableAreaWTextComp::showPopupWindow(const Point & cursorPosition)
 }
 
 CHeroArea::CHeroArea(int x, int y, const CGHeroInstance * hero)
-	: CIntObject(LCLICK | HOVER),
+	: CIntObject(LCLICK | SHOW_POPUP | HOVER),
 	hero(hero),
-	clickFunctor(nullptr)
+	clickFunctor(nullptr),
+	clickRFunctor(nullptr)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
@@ -147,12 +148,23 @@ void CHeroArea::addClickCallback(ClickFunctor callback)
 	clickFunctor = callback;
 }
 
+void CHeroArea::addRClickCallback(ClickFunctor callback)
+{
+	clickRFunctor = callback;
+}
+
 void CHeroArea::clickPressed(const Point & cursorPosition)
 {
 	if(clickFunctor)
 		clickFunctor();
 }
 
+void CHeroArea::showPopupWindow(const Point & cursorPosition)
+{
+	if(clickRFunctor)
+		clickRFunctor();
+}
+
 void CHeroArea::hover(bool on)
 {
 	if (on && hero)

+ 3 - 0
client/widgets/MiscWidgets.h

@@ -190,12 +190,15 @@ public:
 
 	CHeroArea(int x, int y, const CGHeroInstance * hero);
 	void addClickCallback(ClickFunctor callback);
+	void addRClickCallback(ClickFunctor callback);
 	void clickPressed(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
 	void hover(bool on) override;
 private:
 	const CGHeroInstance * hero;
 	std::shared_ptr<CAnimImage> portrait;
 	ClickFunctor clickFunctor;
+	ClickFunctor clickRFunctor;
 	ClickFunctor showPopupHandler;
 };
 

+ 3 - 3
client/widgets/TextControls.cpp

@@ -375,7 +375,7 @@ void CTextBox::setText(const std::string & text)
 	else if(slider)
 	{
 		// decrease width again if slider still used
-		label->pos.w = pos.w - 32;
+		label->pos.w = pos.w - 16;
 		assert(label->pos.w > 0);
 		label->setText(text);
 		slider->setAmount(label->textSize.y);
@@ -383,12 +383,12 @@ void CTextBox::setText(const std::string & text)
 	else if(label->textSize.y > label->pos.h)
 	{
 		// create slider and update widget
-		label->pos.w = pos.w - 32;
+		label->pos.w = pos.w - 16;
 		assert(label->pos.w > 0);
 		label->setText(text);
 
 		OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
-		slider = std::make_shared<CSlider>(Point(pos.w - 32, 0), pos.h, std::bind(&CTextBox::sliderMoved, this, _1),
+		slider = std::make_shared<CSlider>(Point(pos.w - 16, 0), pos.h, std::bind(&CTextBox::sliderMoved, this, _1),
 			label->pos.h, label->textSize.y, 0, Orientation::VERTICAL, CSlider::EStyle(sliderStyle));
 		slider->setScrollStep((int)graphics->fonts[label->font]->getLineHeight());
 		slider->setPanningStep(1);

+ 51 - 14
client/windows/CHeroBackpackWindow.cpp

@@ -22,25 +22,19 @@
 CHeroBackpackWindow::CHeroBackpackWindow(const CGHeroInstance * hero)
 	: CWindowObject((EOptions)0)
 {
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
 
-	stretchedBackground = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, 410, 425));
-
-	arts = std::make_shared<CArtifactsOfHeroBackpack>(Point(windowMargin, windowMargin));
-	arts->setHero(hero);
+	stretchedBackground = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, 0, 0));
+	arts = std::make_shared<CArtifactsOfHeroBackpack>();
+	arts->moveBy(Point(windowMargin, windowMargin));
 	addSetAndCallbacks(arts);
-
+	arts->setHero(hero);
 	addCloseCallback(std::bind(&CHeroBackpackWindow::close, this));
-
 	quitButton = std::make_shared<CButton>(Point(), AnimationPath::builtin("IOKAY32.def"), CButton::tooltip(""), [this]() { close(); }, EShortcut::GLOBAL_RETURN);
-
-	stretchedBackground->pos.w = arts->pos.w + 2 * windowMargin;
-	stretchedBackground->pos.h = arts->pos.h + quitButton->pos.h + 3 * windowMargin;
-	pos.w = stretchedBackground->pos.w;
-	pos.h = stretchedBackground->pos.h;
+	pos.w = stretchedBackground->pos.w = arts->pos.w + 2 * windowMargin;
+	pos.h = stretchedBackground->pos.h = arts->pos.h + quitButton->pos.h + 3 * windowMargin;
+	quitButton->moveTo(Point(pos.x + pos.w / 2 - quitButton->pos.w / 2, pos.y + arts->pos.h + 2 * windowMargin));
 	center();
-
-	quitButton->moveBy(Point(GH.screenDimensions().x / 2 - quitButton->pos.w / 2 - quitButton->pos.x, arts->pos.h + 2 * windowMargin));
 }
 
 void CHeroBackpackWindow::showAll(Canvas & to)
@@ -48,3 +42,46 @@ void CHeroBackpackWindow::showAll(Canvas & to)
 	CIntObject::showAll(to);
 	CMessage::drawBorder(PlayerColor(LOCPLINT->playerID), to.getInternalSurface(), pos.w+28, pos.h+29, pos.x-14, pos.y-15);
 }
+
+CHeroQuickBackpackWindow::CHeroQuickBackpackWindow(const CGHeroInstance * hero, ArtifactPosition targetSlot)
+	: CWindowObject((EOptions)0)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	stretchedBackground = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, 0, 0));
+	arts = std::make_shared<CArtifactsOfHeroQuickBackpack>(targetSlot);
+	arts->moveBy(Point(windowMargin, windowMargin));
+	addSetAndCallbacks(static_cast<std::weak_ptr<CArtifactsOfHeroQuickBackpack>>(arts));
+	arts->setHero(hero);
+	addCloseCallback(std::bind(&CHeroQuickBackpackWindow::close, this));
+	addUsedEvents(GESTURE);
+	pos.w = stretchedBackground->pos.w = arts->pos.w + 2 * windowMargin;
+	pos.h = stretchedBackground->pos.h = arts->pos.h + windowMargin;
+}
+
+void CHeroQuickBackpackWindow::gesture(bool on, const Point & initialPosition, const Point & finalPosition)
+{
+	if(on)
+		return;
+
+	arts->swapSelected();
+	close();
+}
+
+void CHeroQuickBackpackWindow::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance)
+{
+	arts->selectSlotAt(currentPosition);
+	redraw();
+}
+
+void CHeroQuickBackpackWindow::showAll(Canvas & to)
+{
+	if(arts->getSlotsNum() == 0)
+	{
+		// Dirty solution for closing that window
+		close();
+		return;
+	}
+	CMessage::drawBorder(PlayerColor(LOCPLINT->playerID), to.getInternalSurface(), pos.w + 28, pos.h + 29, pos.x - 14, pos.y - 15);
+	CIntObject::showAll(to);
+}

+ 17 - 2
client/windows/CHeroBackpackWindow.h

@@ -19,11 +19,26 @@ class CHeroBackpackWindow : public CWindowObject, public CWindowWithArtifacts
 public:
 	CHeroBackpackWindow(const CGHeroInstance * hero);
 	
-private:
+protected:
 	std::shared_ptr<CArtifactsOfHeroBackpack> arts;
 	std::shared_ptr<CButton> quitButton;
 	std::shared_ptr<CFilledTexture> stretchedBackground;
-	const int windowMargin = 10;
+	const int windowMargin = 5;
+
+	void showAll(Canvas & to) override;
+};
+
+class CHeroQuickBackpackWindow : public CWindowObject, public CWindowWithArtifacts
+{
+public:
+	CHeroQuickBackpackWindow(const CGHeroInstance * hero, ArtifactPosition targetSlot);
+	void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override;
+	void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override;
+
+private:
+	std::shared_ptr<CArtifactsOfHeroQuickBackpack> arts;
+	std::shared_ptr<CFilledTexture> stretchedBackground;
+	const int windowMargin = 5;
 
 	void showAll(Canvas & to) override;
 };

+ 0 - 1
client/windows/CHeroWindow.h

@@ -111,6 +111,5 @@ public:
 	void createBackpackWindow();
 
 	//friends
-	friend void CHeroArtPlace::clickPressed(const Point & cursorPosition);
 	friend class CPlayerInterface;
 };

+ 3 - 3
client/windows/CMapOverview.cpp

@@ -13,8 +13,6 @@
 
 #include "../lobby/SelectionTab.h"
 
-#include <vstd/DateUtils.h>
-
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../widgets/CComponent.h"
@@ -29,6 +27,7 @@
 #include "../render/Graphics.h"
 
 #include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/TextOperations.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/campaign/CampaignState.h"
 #include "../../lib/mapping/CMap.h"
@@ -42,6 +41,7 @@
 #include "../../lib/serializer/CLoadFile.h"
 #include "../../lib/StartInfo.h"
 #include "../../lib/rmg/CMapGenOptions.h"
+#include "../../lib/Languages.h"
 
 CMapOverview::CMapOverview(std::string mapName, std::string fileName, std::string date, ResourcePath resource, ESelectionScreen tabType)
 	: CWindowObject(BORDERED | RCLICK_POPUP), resource(resource), mapName(mapName), fileName(fileName), date(date), tabType(tabType)
@@ -199,7 +199,7 @@ CMapOverviewWidget::CMapOverviewWidget(CMapOverview& parent):
 		if(p.date.empty())
 		{
 			std::time_t time = boost::filesystem::last_write_time(*CResourceHandler::get()->getResourceName(ResourcePath(p.resource.getName(), p.tabType == ESelectionScreen::campaignList ? EResType::CAMPAIGN : EResType::MAP)));
-			w->setText(vstd::getFormattedDateTime(time));
+			w->setText(TextOperations::getFormattedDateTimeLocal(time));
 		}
 		else
 			w->setText(p.date);

+ 114 - 62
client/windows/CSpellWindow.cpp

@@ -124,70 +124,22 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
 		offL = offR = offT = offB = offRM = 0;
 		spellsPerPage = 12;
 	}
-	pos = background->center(Point(pos.w/2 + pos.x, pos.h/2 + pos.y));
-
-	//initializing castable spells
-	mySpells.reserve(CGI->spellh->objects.size());
-	for(const CSpell * spell : CGI->spellh->objects)
-	{
-		if(!spell->isCreatureAbility() && myHero->canCastThisSpell(spell))
-			mySpells.push_back(spell);
-	}
-	std::sort(mySpells.begin(), mySpells.end(), spellsorter);
-
-	//initializing sizes of spellbook's parts
-	for(auto & elem : sitesPerTabAdv)
-		elem = 0;
-	for(auto & elem : sitesPerTabBattle)
-		elem = 0;
-
-	for(const auto spell : mySpells)
-	{
-		int * sitesPerOurTab = spell->isCombat() ? sitesPerTabBattle : sitesPerTabAdv;
-
-		++sitesPerOurTab[4];
-
-		spell->forEachSchool([&sitesPerOurTab](const SpellSchool & school, bool & stop)
-		{
-			++sitesPerOurTab[school];
-		});
-	}
-	if(sitesPerTabAdv[4] % spellsPerPage == 0)
-		sitesPerTabAdv[4]/=spellsPerPage;
-	else
-		sitesPerTabAdv[4] = sitesPerTabAdv[4]/spellsPerPage + 1;
-
-	for(int v=0; v<4; ++v)
-	{
-		if(sitesPerTabAdv[v] <= spellsPerPage - 2)
-			sitesPerTabAdv[v] = 1;
-		else
-		{
-			if((sitesPerTabAdv[v] - spellsPerPage - 2) % spellsPerPage == 0)
-				sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 1;
-			else
-				sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 2;
-		}
-	}
 
-	if(sitesPerTabBattle[4] % spellsPerPage == 0)
-		sitesPerTabBattle[4]/=spellsPerPage;
-	else
-		sitesPerTabBattle[4] = sitesPerTabBattle[4]/spellsPerPage + 1;
+	pos = background->center(Point(pos.w/2 + pos.x, pos.h/2 + pos.y));
 
-	for(int v=0; v<4; ++v)
+	if(settings["general"]["enableUiEnhancements"].Bool())
 	{
-		if(sitesPerTabBattle[v] <= spellsPerPage - 2)
-			sitesPerTabBattle[v] = 1;
-		else
-		{
-			if((sitesPerTabBattle[v] - spellsPerPage - 2) % spellsPerPage == 0)
-				sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 1;
-			else
-				sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 2;
-		}
+		Rect r(90, isBigSpellbook ? 480 : 420, isBigSpellbook ? 160 : 110, 16);
+		const ColorRGBA rectangleColor = ColorRGBA(0, 0, 0, 75);
+		const ColorRGBA borderColor = ColorRGBA(128, 100, 75);
+		const ColorRGBA grayedColor = ColorRGBA(158, 130, 105);
+		searchBoxRectangle = std::make_shared<TransparentFilledRectangle>(r.resize(1), rectangleColor, borderColor);
+		searchBoxDescription = std::make_shared<CLabel>(r.center().x, r.center().y, FONT_SMALL, ETextAlignment::CENTER, grayedColor, CGI->generaltexth->translate("vcmi.spellBook.search"));
+
+		searchBox = std::make_shared<CTextInput>(r, FONT_SMALL, std::bind(&CSpellWindow::searchInput, this));
 	}
 
+	processSpells();
 
 	//numbers of spell pages computed
 
@@ -253,7 +205,7 @@ CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _m
 
 	selectedTab = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap;
 	schoolTab->setFrame(selectedTab, 0);
-	int cp = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbokLastPageAdvmap;
+	int cp = battleSpellsOnly ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbookLastPageAdvmap;
 	// spellbook last page battle index is not reset after battle, so this needs to stay here
 	vstd::abetween(cp, 0, std::max(0, pagesWithinCurrentTab() - 1));
 	setCurrentPage(cp);
@@ -292,14 +244,114 @@ std::shared_ptr<IImage> CSpellWindow::createBigSpellBook()
 	Canvas tmp5 = Canvas(Point(409, 141));
 	tmp5.draw(img, Point(0, 0), Rect(100, 38 + 45, 509 - 15, 400 - 38));
 	canvas.drawScaled(tmp5, Point(90, 45), Point(615, 415));
+	// carpet
+	Canvas tmp6 = Canvas(Point(590, 59));
+	tmp6.draw(img, Point(0, 0), Rect(15, 484, 590, 59));
+	canvas.drawScaled(tmp6, Point(0, 545), Point(800, 59));
+	// remove bookmarks
+	for (int i = 0; i < 56; i++)
+		canvas.draw(Canvas(canvas, Rect(i < 30 ? 268 : 327, 464, 1, 46)), Point(269 + i, 464));
+	for (int i = 0; i < 56; i++)
+		canvas.draw(Canvas(canvas, Rect(469, 464, 1, 42)), Point(470 + i, 464));
+	for (int i = 0; i < 57; i++)
+		canvas.draw(Canvas(canvas, Rect(i < 30 ? 564 : 630, 464, 1, 44)), Point(565 + i, 464));
+	for (int i = 0; i < 56; i++)
+		canvas.draw(Canvas(canvas, Rect(656, 464, 1, 47)), Point(657 + i, 464));
+	// draw bookmarks
+	canvas.draw(img, Point(278, 464), Rect(220, 405, 37, 47));
+	canvas.draw(img, Point(481, 465), Rect(354, 406, 37, 41));
+	canvas.draw(img, Point(575, 465), Rect(417, 406, 37, 45));
+	canvas.draw(img, Point(667, 465), Rect(478, 406, 37, 47));
 
 	return GH.renderHandler().createImage(canvas.getInternalSurface());
 }
 
+void CSpellWindow::searchInput()
+{
+	if(searchBox)
+		searchBoxDescription->setEnabled(searchBox->getText().empty());
+
+	processSpells();
+
+	int cp = 0;
+	// spellbook last page battle index is not reset after battle, so this needs to stay here
+	vstd::abetween(cp, 0, std::max(0, pagesWithinCurrentTab() - 1));
+	setCurrentPage(cp);
+	computeSpellsPerArea();
+}
+
+void CSpellWindow::processSpells()
+{
+	mySpells.clear();
+
+	//initializing castable spells
+	mySpells.reserve(CGI->spellh->objects.size());
+	for(const CSpell * spell : CGI->spellh->objects)
+	{
+		bool searchTextFound = !searchBox || boost::algorithm::contains(boost::algorithm::to_lower_copy(spell->getNameTranslated()), boost::algorithm::to_lower_copy(searchBox->getText()));
+		if(!spell->isCreatureAbility() && myHero->canCastThisSpell(spell) && searchTextFound)
+			mySpells.push_back(spell);
+	}
+	std::sort(mySpells.begin(), mySpells.end(), spellsorter);
+
+	//initializing sizes of spellbook's parts
+	for(auto & elem : sitesPerTabAdv)
+		elem = 0;
+	for(auto & elem : sitesPerTabBattle)
+		elem = 0;
+
+	for(const auto spell : mySpells)
+	{
+		int * sitesPerOurTab = spell->isCombat() ? sitesPerTabBattle : sitesPerTabAdv;
+
+		++sitesPerOurTab[4];
+
+		spell->forEachSchool([&sitesPerOurTab](const SpellSchool & school, bool & stop)
+		{
+			++sitesPerOurTab[school];
+		});
+	}
+	if(sitesPerTabAdv[4] % spellsPerPage == 0)
+		sitesPerTabAdv[4]/=spellsPerPage;
+	else
+		sitesPerTabAdv[4] = sitesPerTabAdv[4]/spellsPerPage + 1;
+
+	for(int v=0; v<4; ++v)
+	{
+		if(sitesPerTabAdv[v] <= spellsPerPage - 2)
+			sitesPerTabAdv[v] = 1;
+		else
+		{
+			if((sitesPerTabAdv[v] - spellsPerPage - 2) % spellsPerPage == 0)
+				sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 1;
+			else
+				sitesPerTabAdv[v] = (sitesPerTabAdv[v] - spellsPerPage - 2) / spellsPerPage + 2;
+		}
+	}
+
+	if(sitesPerTabBattle[4] % spellsPerPage == 0)
+		sitesPerTabBattle[4]/=spellsPerPage;
+	else
+		sitesPerTabBattle[4] = sitesPerTabBattle[4]/spellsPerPage + 1;
+
+	for(int v=0; v<4; ++v)
+	{
+		if(sitesPerTabBattle[v] <= spellsPerPage - 2)
+			sitesPerTabBattle[v] = 1;
+		else
+		{
+			if((sitesPerTabBattle[v] - spellsPerPage - 2) % spellsPerPage == 0)
+				sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 1;
+			else
+				sitesPerTabBattle[v] = (sitesPerTabBattle[v] - spellsPerPage - 2) / spellsPerPage + 2;
+		}
+	}
+}
+
 void CSpellWindow::fexitb()
 {
 	(myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastTabBattle : myInt->localState->spellbookSettings.spellbookLastTabAdvmap) = selectedTab;
-	(myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbokLastPageAdvmap) = currentPage;
+	(myInt->battleInt ? myInt->localState->spellbookSettings.spellbookLastPageBattle : myInt->localState->spellbookSettings.spellbookLastPageAdvmap) = currentPage;
 
 	close();
 }
@@ -595,7 +647,7 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition)
 			auto guard = vstd::makeScopeGuard([this]()
 			{
 				owner->myInt->localState->spellbookSettings.spellbookLastTabAdvmap = owner->selectedTab;
-				owner->myInt->localState->spellbookSettings.spellbokLastPageAdvmap = owner->currentPage;
+				owner->myInt->localState->spellbookSettings.spellbookLastPageAdvmap = owner->currentPage;
 			});
 
 			if(mySpell->getTargetType() == spells::AimType::LOCATION)

+ 8 - 0
client/windows/CSpellWindow.h

@@ -26,6 +26,8 @@ class CLabel;
 class CGStatusBar;
 class CPlayerInterface;
 class CSpellWindow;
+class CTextInput;
+class TransparentFilledRectangle;
 
 /// The spell window
 class CSpellWindow : public CWindowObject
@@ -80,6 +82,10 @@ class CSpellWindow : public CWindowObject
 
 	std::vector<std::shared_ptr<InteractiveArea>> interactiveAreas;
 
+	std::shared_ptr<CTextInput> searchBox;
+	std::shared_ptr<TransparentFilledRectangle> searchBoxRectangle;
+	std::shared_ptr<CLabel> searchBoxDescription;
+
 	bool isBigSpellbook;
 	int spellsPerPage;
 	int offL;
@@ -99,6 +105,8 @@ class CSpellWindow : public CWindowObject
 	const CGHeroInstance * myHero; //hero whose spells are presented
 	CPlayerInterface * myInt;
 
+	void processSpells();
+	void searchInput();
 	void computeSpellsPerArea(); //recalculates spellAreas::mySpell
 
 	void setCurrentPage(int value);

+ 1 - 1
client/windows/CTradeWindow.cpp

@@ -331,7 +331,7 @@ void CTradeWindow::setMode(EMarketMode Mode)
 	}
 }
 
-void CTradeWindow::artifactSelected(CHeroArtPlace *slot)
+void CTradeWindow::artifactSelected(CArtPlace * slot)
 {
 	assert(mode == EMarketMode::ARTIFACT_RESOURCE);
 	items[1][0]->setArtInstance(slot->getArt());

+ 1 - 1
client/windows/CTradeWindow.h

@@ -40,7 +40,7 @@ public:
 	void getPositionsFor(std::vector<Rect> &poss, bool Left, EType type) const;
 	void setMode(EMarketMode Mode); //mode setter
 
-	void artifactSelected(CHeroArtPlace *slot); //used when selling artifacts -> called when user clicked on artifact slot
+	void artifactSelected(CArtPlace * slot); //used when selling artifacts -> called when user clicked on artifact slot
 	virtual void selectionChanged(bool side) = 0; //true == left
 	virtual Point selectionOffset(bool Left) const = 0;
 	virtual std::string updateSlotSubtitle(bool Left) const = 0;

+ 3 - 1
client/windows/GUIClasses.cpp

@@ -409,7 +409,9 @@ CLevelWindow::CLevelWindow(const CGHeroInstance * hero, PrimarySkill pskill, std
 		box = std::make_shared<CComponentBox>(comps, Rect(75, 300, pos.w - 150, 100));
 	}
 
-	portrait = std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsLarge"), hero->getIconIndex(), 0, 170, 66);
+	portrait = std::make_shared<CHeroArea>(170, 66, hero);
+	portrait->addClickCallback(nullptr);
+	portrait->addRClickCallback([hero](){ GH.windows().createAndPushWindow<CRClickPopupInt>(std::make_shared<CHeroWindow>(hero)); });
 	ok = std::make_shared<CButton>(Point(297, 413), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CLevelWindow::close, this), EShortcut::GLOBAL_ACCEPT);
 
 	//%s has gained a level.

+ 2 - 1
client/windows/GUIClasses.h

@@ -35,6 +35,7 @@ class CGStatusBar;
 class CTextBox;
 class CGarrisonInt;
 class CGarrisonSlot;
+class CHeroArea;
 
 enum class EUserEvent;
 
@@ -129,7 +130,7 @@ public:
 /// Raised up level window where you can select one out of two skills
 class CLevelWindow : public CWindowObject
 {
-	std::shared_ptr<CAnimImage> portrait;
+	std::shared_ptr<CHeroArea> portrait;
 	std::shared_ptr<CButton> ok;
 	std::shared_ptr<CLabel> mainTitle;
 	std::shared_ptr<CLabel> levelTitle;

+ 1 - 1
cmake_modules/VersionDefinition.cmake

@@ -1,6 +1,6 @@
 set(VCMI_VERSION_MAJOR 1)
 set(VCMI_VERSION_MINOR 4)
-set(VCMI_VERSION_PATCH 1)
+set(VCMI_VERSION_PATCH 2)
 add_definitions(
 	-DVCMI_VERSION_MAJOR=${VCMI_VERSION_MAJOR}
 	-DVCMI_VERSION_MINOR=${VCMI_VERSION_MINOR}

+ 2 - 0
config/artifacts.json

@@ -1873,6 +1873,7 @@
 			}
 		],
 		"index" : 127,
+		"class" : "SPECIAL",
 		"type" : ["HERO"]
 	},
 	"armageddonsBlade":
@@ -1917,6 +1918,7 @@
 			}
 		],
 		"index" : 128,
+		"class" : "SPECIAL",
 		"type" : ["HERO"]
 	},
 	"angelicAlliance":

+ 12 - 1
config/objects/generic.json

@@ -935,5 +935,16 @@
 	"grassHills"					: { "index" :208, "handler": "static", "types" : { "object" : { "index" : 0} } },
 	"roughHills"					: { "index" :209, "handler": "static", "types" : { "object" : { "index" : 0} } },
 	"subterraneanRocks"				: { "index" :210, "handler": "static", "types" : { "object" : { "index" : 0} } },
-	"swampFoliage" 					: { "index" :211, "handler": "static", "types" : { "object" : { "index" : 0} } }
+	"swampFoliage" 					: { "index" :211, "handler": "static", "types" : { "object" : { "index" : 0} } },
+	
+	/// special object to handle invalid / unknown objects on some user-made maps
+	"nothing" : {
+		"index" : 0,
+		"handler": "generic",
+		"types" : {
+			"nothing" : {
+				"index" : 0
+			}
+		}
+	}
 }

+ 1 - 1
config/schemas/settings.json

@@ -615,7 +615,7 @@
 				},
 				"infoBarCreatureManagement": {
 					"type" : "boolean",
-					"default" : false
+					"default" : true
 				},
 				"enableLargeSpellbook" : {
 					"type": "boolean",

+ 1 - 1
config/spells/timed.json

@@ -1221,7 +1221,7 @@
 				"effects" : {
 					"attacksNearestCreature" : {
 						"type" : "ATTACKS_NEAREST_CREATURE",
-						"duration" : "UNTIL_ATTACK"
+						"duration" : "UNTIL_OWN_ATTACK"
 					}
 				}
 			},

+ 2 - 1
config/terrainViewPatterns.json

@@ -85,7 +85,8 @@
 				"N", "N", "N",
 				"N", "N", "N"
 			],
-			"mapping" : { "normal" : "49-72", "dirt" : "21-44", "sand" : "0-23", "water" : "20-32", "rock": "0-7", "hota" : "77-117" }
+			"decoration" : true,
+			"mapping" : { "normal" : "49-56,57-72", "dirt" : "21-28,29-44", "sand" : "0-11,12-23", "water" : "20-32", "rock": "0-7", "hota" : "77-101,102-117" }
 		},
 		// Mixed transitions
 		{

+ 2 - 2
config/widgets/turnOptionsTab.json

@@ -222,7 +222,7 @@
 				{
 					"name": "chessFieldTurn",
 					"callback": "parseAndSetTimer_turn",
-					"help": "vcmi.optionsTab.chessFieldTurn.help"
+					"help": "vcmi.optionsTab.chessFieldTurnAccumulate.help"
 				},
 				{
 					"name": "chessFieldBattle",
@@ -232,7 +232,7 @@
 				{
 					"name": "chessFieldUnit",
 					"callback": "parseAndSetTimer_unit",
-					"help": "vcmi.optionsTab.chessFieldUnit.help"
+					"help": "vcmi.optionsTab.chessFieldUnitAccumulate.help"
 				}
 			]
 		},

+ 0 - 34
config/widgets/turnTimer.json

@@ -1,34 +0,0 @@
-{
-	"items":
-	[
-		{ //backgound color
-			"type": "drawRect",
-			"rect": {"x": 4, "y": 4, "w": 72, "h": 24},
-			"color": [10, 10, 10, 255]
-		},
-
-		{ //clocks icon
-			"type": "image",
-			"image": "VCMI/BATTLEQUEUE/STATESSMALL",
-			"frame": 1,
-			"position": {"x": 4, "y": 6}
-		},
-
-		{ //timer field label
-			"name": "timer",
-			"type": "label",
-			"font": "big",
-			"alignment": "left",
-			"color": "yellow",
-			"text": "",
-			"position": {"x": 26, "y": 2}
-		},
-	],
-
-	"variables":
-	{
-		"notificationTime": [0, 1, 2, 3, 4, 5, 20],
-		"notificationSound": "WE5",
-		"textColorFromPlayerColor": true
-	}
-}

+ 7 - 1
debian/changelog

@@ -1,9 +1,15 @@
-vcmi (1.4.1) jammy; urgency=medium
+vcmi (1.4.2) jammy; urgency=medium
 
   * New upstream release
 
  -- Ivan Savenko <[email protected]>  Fri, 22 Dec 2023 16:00:00 +0200
 
+vcmi (1.4.1) jammy; urgency=medium
+
+  * New upstream release
+
+ -- Ivan Savenko <[email protected]>  Tue, 12 Dec 2023 16:00:00 +0200
+
 vcmi (1.4.0) jammy; urgency=medium
 
   * New upstream release

+ 1 - 0
docs/Readme.md

@@ -1,5 +1,6 @@
 [![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.0/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.0)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.1)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 
 # VCMI Project

+ 2 - 1
docs/modders/Bonus/Bonus_Duration_Types.md

@@ -13,4 +13,5 @@ Bonus may have any of these durations. They acts in disjunction.
 -   UNTIL_BEING_ATTACKED: removed after any damage-inflicting attack
 -   UNTIL_ATTACK: removed after attack and counterattacks are performed
 -   STACK_GETS_TURN: removed when stack gets its turn - used for defensive stance
--   COMMANDER_KILLED
+-   COMMANDER_KILLED
+-   UNTIL_OWN_ATTACK: removed after attack (not counterattack) is performed

+ 15 - 3
docs/players/Cheat_Codes.md

@@ -36,7 +36,7 @@ Gives specific creature in every slot, with optional amount. Examples:
 
 ### Movement points
 
-`nwcnebuchadnezzar` or `vcminahar` or `vcmimove` - give 1000000 movement points and free ship boarding for 1 day  
+`nwcnebuchadnezzar` or `vcminahar` or `vcmimove` - give unlimited (or specified amount of) movement points and free ship boarding
 Alternative usage: `vcmimove <amount>` - gives specified amount of movement points
 
 ### Resources
@@ -57,10 +57,22 @@ Alternative usage: `vcmilevel <amount>` - advances hero by specified number of l
 - `vcmiolorin` or `vcmiexp` - gives selected hero 10000 experience
 Alternative usage: `vcmiexp <amount>` - gives selected hero specified amount of experience
 
+### Luck and morale
+
+`nwcfollowthewhiterabbit` or `vcmiluck` - the currently selected hero permanently gains maximum luck
+`nwcmorpheus` or `vcmimorale` - the currently selected hero permanently gains maximum morale
+
+### Puzzle map
+
+`nwcoracle` or `vcmiobelisk` - reveals the puzzle map
+
 ### Finishing the game
 
-`nwcredpill` or `vcmisilmaril` or `vcmiwin` - player wins  
-`nwcbluepill` or `vcmimelkor` or `vcmilose` - player loses  
+`nwcredpill` or `vcmisilmaril` or `vcmiwin` - player wins
+`nwcbluepill` or `vcmimelkor` or `vcmilose` - player loses
+
+### Misc
+`nwctheone` or `vcmigod` - reveals the whole map, gives 5 archangels in each empty slot, unlimited movement points and permanent flight
 
 ## Using cheat codes on other players
 By default, all cheat codes apply to current player. Alternatively, it is possible to specify player that you want to target:

+ 1 - 1
include/vstd/DateUtils.h

@@ -5,7 +5,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 namespace vstd
 {
 
-	DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt);
+	DLL_LINKAGE std::string getFormattedDateTime(std::time_t dt, std::string format);
 	DLL_LINKAGE std::string getDateTimeISO8601Basic(std::time_t dt);
 
 }

+ 61 - 37
launcher/eu.vcmi.VCMI.metainfo.xml

@@ -1,56 +1,94 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<component type="desktop-application">
+<?xml version='1.0' encoding='utf-8'?>
+<component type="desktop">
 	<id>eu.vcmi.VCMI</id>
-	<metadata_license>CC-BY-SA-4.0</metadata_license>
-	<project_license>GPL-2.0-or-later</project_license>
 	<name>VCMI</name>
 	<summary>Open-source game engine for Heroes of Might and Magic III</summary>
+	<summary xml:lang="cs">Herní engine s otevřeným kódem pro Heroes of Might and Magic III</summary>
+	<summary xml:lang="de">Open-Source-Spielengine für Heroes of Might and Magic III</summary>
+	<developer_name>VCMI Team</developer_name>
+	<developer_name xml:lang="cs">Tým VCMI</developer_name>
+	<metadata_license>CC-BY-SA-4.0</metadata_license>
+	<project_license>GPL-2.0-or-later</project_license>
 	<description>
 		<p>VCMI is an open-source engine for Heroes of Might and Magic III with new possibilities. Years of intensive work resulted in an impressive amount of features. Among the current features are:</p>
+		<p xml:lang="cs">VCMI je engine s otevřeným kódem a novými možnostmi pro Heroes of Might and Magic III. Roky usilovné práce vyústily v úchvatném počtu nových funkcí. Mezi současnými funkcemi jsou:</p>
+		<p xml:lang="de">VCMI ist eine Open-Source-Engine für Heroes of Might and Magic III mit neuen Möglichkeiten. Jahrelange intensive Arbeit führte zu einer beeindruckenden Anzahl von Features. Zu den aktuellen Features gehören:</p>
 		<ul>
 			<li>Complete gameplay mechanics</li>
+			<li xml:lang="cs">Kompletní herní mechaniky</li>
+			<li xml:lang="de">Vollständige Spielmechanik</li>
 			<li>Almost all objects, abilities, spells and other content</li>
+			<li xml:lang="cs">Skoro všechny předměty, schopnosti, kouzla a ostatní obsah</li>
+			<li xml:lang="de">Fast alle Objekte, Fähigkeiten, Zaubersprüche und andere Inhalte</li>
 			<li>Basic battle AI and adventure AI</li>
+			<li xml:lang="cs">Základní AI boje a mapy světa</li>
+			<li xml:lang="de">Grundlegende Kampf- und Abenteuer-KI</li>
 			<li>Many GUI improvements: high resolutions, stack queue, creature window</li>
+			<li xml:lang="cs">Mnoho vylepšení rozhraní: vyšší rozlišení, fronta oddílů a okno bojovníků</li>
+			<li xml:lang="de">Viele GUI-Verbesserungen: Hohe Auflösungen, Truppenwarteschlange, Kreaturenfenster</li>
 			<li>Advanced and easy mod support - add new towns, creatures, heroes, artifacts and spells without limits or conflicts</li>
+			<li xml:lang="cs">Pokročilá a jednoduchá podpora modifikací - přidání nových měst, bojovníků, hrdinů, artefaktů a kouzel bez limitů a konfliktů</li>
+			<li xml:lang="de">Erweiterte und einfache Mod-Unterstützung - füge neue Städte, Kreaturen, Helden, Artefakte und Zaubersprüche ohne Einschränkungen oder Konflikte hinzu</li>
 			<li>Launcher for easy configuration - download mods from our server and install them immediately!</li>
+			<li xml:lang="cs">Spouštěč pro jednoduché nastavení - stahujte modifikace z našeho serveru a hned je instalujte!</li>
+			<li xml:lang="de">Launcher für einfache Konfiguration - Mods von unserem Server herunterladen und sofort installieren!</li>
 			<li>Random map generator that supports objects added by mods</li>
+			<li xml:lang="cs">Náhodný generátor map, který podporuje předměty přidané modifikacemi</li>
+			<li xml:lang="de">Zufallsgenerator für Karten, der von Mods hinzugefügte Objekte unterstützt</li>
 		</ul>
 		<p>Note: In order to play the game using VCMI you need to own data files for Heroes of Might and Magic III: The Shadow of Death.</p>
+		<p xml:lang="cs">Poznámka: pokud chcete hrát hru přes VCMI, musíte vlastnit datové soubory Heroes of Might and Magic III: The Shadow of Death.</p>
+		<p xml:lang="de">Hinweis: Um das Spiel mit VCMI spielen zu können, sind die Originaldateien für Heroes of Might and Magic III: The Shadow of Death erforderlich.</p>
 		<p>If you want help, please check our forum, bug tracker or GitHub page.</p>
+		<p xml:lang="cs">Pokud chcete pomoct, prosíme, podívejte se na naše fórum nebo GitHub.</p>
+		<p xml:lang="de">Wird Hilfe benötigt, besucht bitte unser Forum, den Bugtracker oder die GitHub-Seite.</p>
 	</description>
 	<screenshots>
 		<screenshot type="default">
-			<image>https://play-lh.googleusercontent.com/-ZErg8hUyc3TScVQZYh30KzaUDWmnIGRcN3WPCQimdviOJVoMkjDdIW19WqX0KZO7mk=w2560-h1440</image>
+			<image type="source">https://play-lh.googleusercontent.com/-ZErg8hUyc3TScVQZYh30KzaUDWmnIGRcN3WPCQimdviOJVoMkjDdIW19WqX0KZO7mk=w2560-h1440</image>
 		</screenshot>
 		<screenshot>
-			<image>https://play-lh.googleusercontent.com/8Uuuir7_0pDCPohacS7nd6CQuvaOO7nKmKWo-A-EysRY8uIK5qLsv0cpMxT3AFu2DA=w2560-h1440</image>
+			<image type="source">https://play-lh.googleusercontent.com/8Uuuir7_0pDCPohacS7nd6CQuvaOO7nKmKWo-A-EysRY8uIK5qLsv0cpMxT3AFu2DA=w2560-h1440</image>
 		</screenshot>
 		<screenshot>
-			<image>https://play-lh.googleusercontent.com/993tSmsQh1qgb1_XQUxD1wmS8Zuiydf0LsQK_nU2fAiJl62n0_Vl_5JMVK-J9lvVeQ=w2560-h1440</image>
+			<image type="source">https://play-lh.googleusercontent.com/993tSmsQh1qgb1_XQUxD1wmS8Zuiydf0LsQK_nU2fAiJl62n0_Vl_5JMVK-J9lvVeQ=w2560-h1440</image>
 		</screenshot>
 		<screenshot>
-			<image>https://play-lh.googleusercontent.com/N3fn4cqX2iN0t9jzEo0vgEMBpUYWyRgOz8AtDsMLmF-BTyxO4l2uoAderEIhXPvPsg=w2560-h1440</image>
+			<image type="source">https://play-lh.googleusercontent.com/N3fn4cqX2iN0t9jzEo0vgEMBpUYWyRgOz8AtDsMLmF-BTyxO4l2uoAderEIhXPvPsg=w2560-h1440</image>
 		</screenshot>
 		<screenshot>
-			<image>https://play-lh.googleusercontent.com/9wSQsPlMPSBAzekEcw1OVpzeOvYiHrYRYHgpa1aRk31pvR752gu_cUZws5x8JMCACw=w2560-h1440</image>
+			<image type="source">https://play-lh.googleusercontent.com/9wSQsPlMPSBAzekEcw1OVpzeOvYiHrYRYHgpa1aRk31pvR752gu_cUZws5x8JMCACw=w2560-h1440</image>
 		</screenshot>
 		<screenshot>
-			<image>https://play-lh.googleusercontent.com/nU_MSwmEhiQ14Sx9xI3QFF1FhL7BYTDMbEYQg-XglSYwIMpYZDpP92_ybTtu4cVJSxo=w2560-h1440</image>
+			<image type="source">https://play-lh.googleusercontent.com/nU_MSwmEhiQ14Sx9xI3QFF1FhL7BYTDMbEYQg-XglSYwIMpYZDpP92_ybTtu4cVJSxo=w2560-h1440</image>
 		</screenshot>
 		<screenshot>
-			<image>https://play-lh.googleusercontent.com/rhI5moPCKQpYdjnRaJJ_CdIxSAXUkrH7ddghuPWaW0vcyaltd-ZGZG4Kl6v3YKGVlzU=w2560-h1440</image>
+			<image type="source">https://play-lh.googleusercontent.com/rhI5moPCKQpYdjnRaJJ_CdIxSAXUkrH7ddghuPWaW0vcyaltd-ZGZG4Kl6v3YKGVlzU=w2560-h1440</image>
 		</screenshot>
 		<screenshot>
-			<image>https://play-lh.googleusercontent.com/1zecL5SvayVkNoIHijeS_rm0Yr_XyHbp_dmSx70NXhjckvnezwvWkpINYBHAy4o8EaM=w2560-h1440</image>
+			<image type="source">https://play-lh.googleusercontent.com/1zecL5SvayVkNoIHijeS_rm0Yr_XyHbp_dmSx70NXhjckvnezwvWkpINYBHAy4o8EaM=w2560-h1440</image>
 		</screenshot>
 		<screenshot>
-			<image>https://play-lh.googleusercontent.com/e5ibvuWm442dYN2z_pAwDC3Ktc7XOY6FEI4N7BkFB7DqGi3Q-Vl46KqoJ_EFSrXRNw=w2560-h1440</image>
+			<image type="source">https://play-lh.googleusercontent.com/e5ibvuWm442dYN2z_pAwDC3Ktc7XOY6FEI4N7BkFB7DqGi3Q-Vl46KqoJ_EFSrXRNw=w2560-h1440</image>
 		</screenshot>
 		<screenshot>
-			<image>https://play-lh.googleusercontent.com/wD6xJK2nuzVyKUFODfQs2SEbUuSyEglojpp_FE6GeAuzdA_R44Dp6gTyN70KCwpRtD9M=w2560-h1440</image>
+			<image type="source">https://play-lh.googleusercontent.com/wD6xJK2nuzVyKUFODfQs2SEbUuSyEglojpp_FE6GeAuzdA_R44Dp6gTyN70KCwpRtD9M=w2560-h1440</image>
 		</screenshot>
 	</screenshots>
+	<releases>
+		<release version="1.4.2" date="2023-12-22" type="stable"/>
+		<release version="1.4.1" date="2023-12-12" type="stable"/>
+		<release version="1.4.0" date="2023-12-08" type="stable"/>
+		<release version="1.3.2" date="2023-09-15" type="stable"/>
+		<release version="1.3.1" date="2023-08-18" type="stable"/>
+		<release version="1.3.0" date="2023-08-04" type="stable"/>
+		<release version="1.2.1" date="2023-04-28" type="stable"/>
+		<release version="1.2.0" date="2023-04-14" type="stable"/>
+		<release version="1.1.1" date="2023-02-03" type="stable"/>
+		<release version="1.1.0" date="2022-12-23" type="stable"/>
+		<release version="1.0.0" date="2022-09-11" type="stable"/>
+		<release version="0.99" date="2016-11-01" type="stable"/>
+	</releases>
 	<url type="homepage">https://vcmi.eu/</url>
 	<url type="bugtracker">https://github.com/vcmi/vcmi/issues</url>
 	<url type="faq">https://vcmi.eu/faq/</url>
@@ -58,33 +96,15 @@
 	<url type="translate">https://github.com/vcmi/vcmi/blob/master/docs/modders/Translations.md</url>
 	<url type="contact">https://discord.gg/chBT42V</url>
 	<url type="vcs-browser">https://github.com/vcmi/vcmi</url>
-	<recommends>
-		<control>keyboard</control>
-		<control>pointing</control>
-		<control>touch</control>
-	</recommends>
 	<categories>
 		<category>Game</category>
 		<category>StrategyGame</category>
 	</categories>
-	<releases>
-		<release version="1.4.1" date="2023-12-22" />
-		<release version="1.4.0" date="2023-12-08" />
-		<release version="1.3.2" date="2023-09-15" />
-		<release version="1.3.1" date="2023-08-18" />
-		<release version="1.3.0" date="2023-08-04" />
-		<release version="1.2.1" date="2023-04-28" />
-		<release version="1.2.0" date="2023-04-14" />
-		<release version="1.1.1" date="2023-02-03" />
-		<release version="1.1.0" date="2022-12-23" />
-		<release version="1.0.0" date="2022-09-11" />
-		<release version="0.99" date="2016-11-01" />
-	</releases>
-	<keywords>
-		<keyword translate="no">heroes3</keyword>
-		<keyword translate="no">homm3</keyword>
-	</keywords>
-	<developer_name>VCMI Team</developer_name>
+	<recommends>
+		<control>pointing</control>
+		<control>keyboard</control>
+		<control>touch</control>
+	</recommends>
 	<content_rating type="oars-1.1">
 		<content_attribute id="violence-cartoon">moderate</content_attribute>
 		<content_attribute id="violence-fantasy">moderate</content_attribute>
@@ -98,4 +118,8 @@
 		<binary>vcmilauncher</binary>
 		<binary>vcmiserver</binary>
 	</provides>
+	<keywords>
+		<keyword>heroes3</keyword>
+		<keyword>homm3</keyword>
+	</keywords>
 </component>

+ 7 - 7
launcher/mainwindow_moc.cpp

@@ -136,14 +136,14 @@ void MainWindow::detectPreferredLanguage()
 		for (auto const & vcmiLang : Languages::getLanguageList())
 			if (vcmiLang.tagIETF == userLang.toStdString())
 				selectedLanguage = vcmiLang.identifier;
-	}
-
-	logGlobal->info("Selected language: %s", selectedLanguage);
 
-	if (!selectedLanguage.empty())
-	{
-		Settings node = settings.write["general"]["language"];
-		node->String() = selectedLanguage;
+		if (!selectedLanguage.empty())
+		{
+			logGlobal->info("Selected language: %s", selectedLanguage);
+			Settings node = settings.write["general"]["language"];
+			node->String() = selectedLanguage;
+			return;
+		}
 	}
 }
 

+ 9 - 1
launcher/modManager/cmodlistview_moc.cpp

@@ -591,7 +591,7 @@ void CModListView::downloadFile(QString file, QString url, QString description,
 			this, SLOT(downloadFinished(QStringList,QStringList,QStringList)));
 		
 		connect(manager.get(), SIGNAL(extractionProgress(qint64,qint64)),
-			this, SLOT(downloadProgress(qint64,qint64)));
+			this, SLOT(extractionProgress(qint64,qint64)));
 		
 		connect(modModel, &CModListModel::dataChanged, filterModel, &QAbstractItemModel::dataChanged);
 
@@ -613,6 +613,14 @@ void CModListView::downloadProgress(qint64 current, qint64 max)
 	ui->progressBar->setValue(current / (1024 * 1024));
 }
 
+void CModListView::extractionProgress(qint64 current, qint64 max)
+{
+	// display progress, in extracted files
+	ui->progressBar->setVisible(true);
+	ui->progressBar->setMaximum(max);
+	ui->progressBar->setValue(current);
+}
+
 void CModListView::downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors)
 {
 	QString title = tr("Download failed");

+ 1 - 0
launcher/modManager/cmodlistview_moc.h

@@ -98,6 +98,7 @@ private slots:
 	void dataChanged(const QModelIndex & topleft, const QModelIndex & bottomRight);
 	void modSelected(const QModelIndex & current, const QModelIndex & previous);
 	void downloadProgress(qint64 current, qint64 max);
+	void extractionProgress(qint64 current, qint64 max);
 	void downloadFinished(QStringList savedFiles, QStringList failedFiles, QStringList errors);
 	void modelReset();
 	void hideProgressBar();

+ 16 - 5
launcher/modManager/cmodmanager.cpp

@@ -24,7 +24,9 @@ namespace
 {
 QString detectModArchive(QString path, QString modName, std::vector<std::string> & filesToExtract)
 {
-	filesToExtract = ZipArchive::listFiles(qstringToPath(path));
+	ZipArchive archive(qstringToPath(path));
+
+	filesToExtract = archive.listFiles();
 
 	QString modDirName;
 
@@ -285,14 +287,23 @@ bool CModManager::doInstallMod(QString modname, QString archivePath)
 	if(!modDirName.size())
 		return addError(modname, "Mod archive is invalid or corrupted");
 	
-	auto futureExtract = std::async(std::launch::async, [&archivePath, &destDir, &filesToExtract]()
+	std::atomic<int> filesCounter = 0;
+
+	auto futureExtract = std::async(std::launch::async, [&archivePath, &destDir, &filesCounter, &filesToExtract]()
 	{
-		return ZipArchive::extract(qstringToPath(archivePath), qstringToPath(destDir), filesToExtract);
+		ZipArchive archive(qstringToPath(archivePath));
+		for (auto const & file : filesToExtract)
+		{
+			if (!archive.extract(qstringToPath(destDir), file))
+				return false;
+			++filesCounter;
+		}
+		return true;
 	});
 	
-	while(futureExtract.wait_for(std::chrono::milliseconds(50)) != std::future_status::ready)
+	while(futureExtract.wait_for(std::chrono::milliseconds(10)) != std::future_status::ready)
 	{
-		emit extractionProgress(0, 0);
+		emit extractionProgress(filesCounter, filesToExtract.size());
 		qApp->processEvents();
 	}
 	

+ 1207 - 0
launcher/translation/czech.ts

@@ -0,0 +1,1207 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!DOCTYPE TS>
+<TS version="2.1" language="cs_CZ">
+<context>
+    <name>AboutProjectView</name>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="22"/>
+        <source>VCMI on Discord</source>
+        <translation>VCMI na Discordu</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="29"/>
+        <source>Have a question? Found a bug? Want to help? Join us!</source>
+        <translation>Máte otázku? Našli jste chybu? Chcete pomoct? Připojte se k nám!</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="36"/>
+        <source>VCMI on Github</source>
+        <translation>VCMI na GitHubu</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="55"/>
+        <source>Our Community</source>
+        <translation>Naše komunita</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="62"/>
+        <source>VCMI on Slack</source>
+        <translation>VCMI na Slacku</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="98"/>
+        <source>Build Information</source>
+        <translation>Informace o sestavení</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="115"/>
+        <source>User data directory</source>
+        <translation>Složka uživatelských dat</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="122"/>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="129"/>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="193"/>
+        <source>Open</source>
+        <translation>Otevřít</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="136"/>
+        <source>Check for updates</source>
+        <translation>Zkontrolovat aktualizace</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="156"/>
+        <source>Game version</source>
+        <translation>Verze hry</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="163"/>
+        <source>Log files directory</source>
+        <translation>Složka záznamů hry</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="176"/>
+        <source>Data Directories</source>
+        <translation>Složky dat</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="213"/>
+        <source>Game data directory</source>
+        <translation>Složka herních dat</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="220"/>
+        <source>Operating System</source>
+        <translation>Operační systém</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="273"/>
+        <source>Project homepage</source>
+        <translation>Domovská stránka projektu</translation>
+    </message>
+    <message>
+        <location filename="../aboutProject/aboutproject_moc.ui" line="286"/>
+        <source>Report a bug</source>
+        <translation>Nahlásit chybu</translation>
+    </message>
+</context>
+<context>
+    <name>CModListModel</name>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="42"/>
+        <source>Translation</source>
+        <translation>Překlad</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="43"/>
+        <source>Town</source>
+        <translation>Město</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="44"/>
+        <source>Test</source>
+        <translation>Zkouška</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="45"/>
+        <source>Templates</source>
+        <translation>Šablony</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="46"/>
+        <source>Spells</source>
+        <translation>Kouzla</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="47"/>
+        <source>Music</source>
+        <translation>Hudba</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="48"/>
+        <source>Maps</source>
+        <translation>Mapy</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="49"/>
+        <source>Sounds</source>
+        <translation>Zvuky</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="50"/>
+        <source>Skills</source>
+        <translation>Schopnosti</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="51"/>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="69"/>
+        <source>Other</source>
+        <translation>Ostatní</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="52"/>
+        <source>Objects</source>
+        <translation>Objekty</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="53"/>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="54"/>
+        <source>Mechanics</source>
+        <translation>Mechaniky</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="55"/>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="56"/>
+        <source>Interface</source>
+        <translation>Rozhraní</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="57"/>
+        <source>Heroes</source>
+        <translation>Hrdinové</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="58"/>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="59"/>
+        <source>Graphical</source>
+        <translation>Grafika</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="60"/>
+        <source>Expansion</source>
+        <translation>Rozšíření</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="61"/>
+        <source>Creatures</source>
+        <translation>Bojovníci</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="62"/>
+        <source>Compatibility</source>
+        <translation>Kompabilita</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="63"/>
+        <source>Artifacts</source>
+        <translation>Artefakty</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="64"/>
+        <source>AI</source>
+        <translation>AI</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="172"/>
+        <source>Name</source>
+        <translation>Název</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="175"/>
+        <source>Type</source>
+        <translation>Druh</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistmodel_moc.cpp" line="176"/>
+        <source>Version</source>
+        <translation>Verze</translation>
+    </message>
+</context>
+<context>
+    <name>CModListView</name>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="43"/>
+        <source>Filter</source>
+        <translation>Filtrovat</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="66"/>
+        <source>All mods</source>
+        <translation>Všechny modifikace</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="71"/>
+        <source>Downloadable</source>
+        <translation>Stahovatelné</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="76"/>
+        <source>Installed</source>
+        <translation>Nainstalované</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="81"/>
+        <source>Updatable</source>
+        <translation>Aktualizovatelné</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="86"/>
+        <source>Active</source>
+        <translation>Aktivní</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="91"/>
+        <source>Inactive</source>
+        <translation>Neaktivní</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="105"/>
+        <source>Download &amp;&amp; refresh repositories</source>
+        <translation>Stáhnout a aktualizovat repozitáře</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="163"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="316"/>
+        <source>Description</source>
+        <translation>Popis</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="209"/>
+        <source>Changelog</source>
+        <translation>Seznam změn</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="231"/>
+        <source>Screenshots</source>
+        <translation>Snímky obrazovky</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="389"/>
+        <source>Uninstall</source>
+        <translation>Odinstalovat</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="424"/>
+        <source>Enable</source>
+        <translation>Povolit</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="459"/>
+        <source>Disable</source>
+        <translation>Zakázat</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="494"/>
+        <source>Update</source>
+        <translation>Aktualizovat</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="529"/>
+        <source>Install</source>
+        <translation>Instalovat</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="327"/>
+        <source> %p% (%v KB out of %m KB)</source>
+        <translation> %p% (%v KB z %m KB)</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.ui" line="340"/>
+        <source>Abort</source>
+        <translation>Zrušit</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="248"/>
+        <source>Mod name</source>
+        <translation>Název modifikace</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="249"/>
+        <source>Installed version</source>
+        <translation>Nainstalovaná verze</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="250"/>
+        <source>Latest version</source>
+        <translation>Nejnovější verze</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="253"/>
+        <source>Size</source>
+        <translation>Velikost</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="255"/>
+        <source>Download size</source>
+        <translation>Velikost ke stažení</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="257"/>
+        <source>Authors</source>
+        <translation>Autoři</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="260"/>
+        <source>License</source>
+        <translation>Licence</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="263"/>
+        <source>Contact</source>
+        <translation>Kontakt</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="272"/>
+        <source>Compatibility</source>
+        <translation>Kompabilita</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="274"/>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="282"/>
+        <source>Required VCMI version</source>
+        <translation>Vyžadovaná verze VCMI</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="280"/>
+        <source>Supported VCMI version</source>
+        <translation>Podporovaná verze VCMI</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="285"/>
+        <source>Supported VCMI versions</source>
+        <translation>Podporované verze VCMI</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="312"/>
+        <source>Languages</source>
+        <translation>Jazyky</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="314"/>
+        <source>Required mods</source>
+        <translation>Vyžadované modifikace VCMI</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="315"/>
+        <source>Conflicting mods</source>
+        <translation>Modifikace v kolizi</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="320"/>
+        <source>This mod can not be installed or enabled because the following dependencies are not present</source>
+        <translation>Tato modifikace nemůže být nainstalována nebo povolena, protože následující závislosti nejsou přítomny</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="321"/>
+        <source>This mod can not be enabled because the following mods are incompatible with it</source>
+        <translation>Tato modifikace nemůže být povolena, protože následující modifikace s ní nejsou kompatibilní</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="322"/>
+        <source>This mod cannot be disabled because it is required by the following mods</source>
+        <translation>Tato modifikace nemůže být zakázána, protože je vyžadována následujícími modifikacemi</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="323"/>
+        <source>This mod cannot be uninstalled or updated because it is required by the following mods</source>
+        <translation>Tato modifikace nemůže být odinstalována nebo aktualizována, protože je vyžadována následujícími modifikacemi</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="324"/>
+        <source>This is a submod and it cannot be installed or uninstalled separately from its parent mod</source>
+        <translation>Toto je podmodifikace, která nemůže být nainstalována nebo odinstalována bez její rodičovské modifikace</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="339"/>
+        <source>Notes</source>
+        <translation>Poznámky</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="599"/>
+        <source>Downloading %s%. %p% (%v MB out of %m MB) finished</source>
+        <translation>Stahování %s%. %p% (%v MB z %m MB) dokončeno</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="618"/>
+        <source>Download failed</source>
+        <translation>Stahování selhalo</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="619"/>
+        <source>Unable to download all files.
+
+Encountered errors:
+
+</source>
+        <translation>Nelze stáhnout všechny soubory.
+
+Vyskytly se chyby:
+
+</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="620"/>
+        <source>
+
+Install successfully downloaded?</source>
+        <translation>
+
+Nainstalovat úspěšně stažené?</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="759"/>
+        <source>Installing mod %1</source>
+        <translation>Instalování modifikace %1</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="811"/>
+        <source>Operation failed</source>
+        <translation>Operace selhala</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="812"/>
+        <source>Encountered errors:
+</source>
+        <translation>Vyskytly se chyby:
+</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="847"/>
+        <source>Screenshot %1</source>
+        <translation>Snímek obrazovky %1</translation>
+    </message>
+    <message>
+        <location filename="../modManager/cmodlistview_moc.cpp" line="243"/>
+        <source>Mod is incompatible</source>
+        <translation>Modifikace není kompatibilní</translation>
+    </message>
+</context>
+<context>
+    <name>CSettingsView</name>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="276"/>
+        <location filename="../settingsView/csettingsview_moc.ui" line="580"/>
+        <location filename="../settingsView/csettingsview_moc.ui" line="649"/>
+        <source>Off</source>
+        <translation>Vypnuto</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="78"/>
+        <location filename="../settingsView/csettingsview_moc.ui" line="195"/>
+        <source>Artificial Intelligence</source>
+        <translation>Umělá inteligence</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="83"/>
+        <location filename="../settingsView/csettingsview_moc.ui" line="484"/>
+        <source>Mod Repositories</source>
+        <translation>Repozitáře modifikací</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="144"/>
+        <source>Interface Scaling</source>
+        <translation>Škálování rozhraní</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="624"/>
+        <source>Neutral AI in battles</source>
+        <translation>Neutrální AI v bitvách</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="289"/>
+        <source>Enemy AI in battles</source>
+        <translation>Nepřátelská AI v bitvách</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="239"/>
+        <source>Additional repository</source>
+        <translation>Další repozitáře</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="158"/>
+        <source>Adventure Map Allies</source>
+        <translation>Spojenci na mapě světa</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="638"/>
+        <source>Adventure Map Enemies</source>
+        <translation>Nepřátelé na mapě světa</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="353"/>
+        <source>Windowed</source>
+        <translation>V okně</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="358"/>
+        <source>Borderless fullscreen</source>
+        <translation>Celá obrazovka bez okrajů</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="363"/>
+        <source>Exclusive fullscreen</source>
+        <translation>Exkluzivní celá obrazovka</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="229"/>
+        <source>Autosave limit (0 = off)</source>
+        <translation>Limit aut. uložení (0=vypnuto)</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="436"/>
+        <source>Friendly AI in battles</source>
+        <translation>Přátelské AI v bitvách</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="429"/>
+        <source>Framerate Limit</source>
+        <translation>Omezení snímků za sekundu</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="151"/>
+        <source>Autosave prefix</source>
+        <translation>Předpona aut. uložení</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="202"/>
+        <source>empty = map name prefix</source>
+        <translation>prázná = předpona - název mapy</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="593"/>
+        <source>Refresh now</source>
+        <translation>Obnovit nyní</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="405"/>
+        <source>Default repository</source>
+        <translation>Výchozí repozitář</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="281"/>
+        <location filename="../settingsView/csettingsview_moc.ui" line="585"/>
+        <location filename="../settingsView/csettingsview_moc.ui" line="654"/>
+        <source>On</source>
+        <translation>Zapnuto</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="525"/>
+        <source>Cursor</source>
+        <translation>Kurzor</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="412"/>
+        <source>Heroes III Data Language</source>
+        <translation>Jazyk dat Heroes III</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="340"/>
+        <source>Select display mode for game
+
+Windowed - game will run inside a window that covers part of your screen
+
+Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen.
+
+Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution.</source>
+        <translation>Vyberte režim zobrazení pro hru
+
+V okně - hra bude běžet v okně zakrývajícím část vaší obrazovky
+
+Celá obrazovka bez okrajů-  hra poběží v okně, které zakryje vaši celou obrazovku se stejným rozlišením.
+
+Exkluzivní celá obrazovka - hra zakryje vaši celou obrazovku a použije vybrané rozlišení.</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="631"/>
+        <source>Reserved screen area</source>
+        <translation>Vyhrazená část obrazovky</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="533"/>
+        <source>Hardware</source>
+        <translation>Hardware</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="538"/>
+        <source>Software</source>
+        <translation>Software</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="118"/>
+        <source>Heroes III Translation</source>
+        <translation>Překlad Heroes III</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="617"/>
+        <source>Check on startup</source>
+        <translation>Zkontrolovat při zapnutí</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="137"/>
+        <source>Fullscreen</source>
+        <translation>Celá obrazovka</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="68"/>
+        <location filename="../settingsView/csettingsview_moc.ui" line="130"/>
+        <source>General</source>
+        <translation>Všeobecné</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="443"/>
+        <source>VCMI Language</source>
+        <translation>Jazyk VCMI</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="498"/>
+        <source>Resolution</source>
+        <translation>Rozlišení</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="518"/>
+        <source>Autosave</source>
+        <translation>Automatické uložení</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="662"/>
+        <source>VSync</source>
+        <translation>VSync</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="374"/>
+        <source>Display index</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="549"/>
+        <source>Network port</source>
+        <translation>Síťový port</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="73"/>
+        <location filename="../settingsView/csettingsview_moc.ui" line="170"/>
+        <source>Video</source>
+        <translation>Zobrazení</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.ui" line="265"/>
+        <source>Show intro</source>
+        <translation>Zobrazit intro</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.cpp" line="409"/>
+        <source>Active</source>
+        <translation>Aktivní</translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.cpp" line="414"/>
+        <source>Disabled</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.cpp" line="415"/>
+        <source>Enable</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.cpp" line="420"/>
+        <source>Not Installed</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../settingsView/csettingsview_moc.cpp" line="421"/>
+        <source>Install</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>Chat</name>
+    <message>
+        <location filename="../lobby/chat_moc.ui" line="14"/>
+        <source>Form</source>
+        <translation>Formulář</translation>
+    </message>
+    <message>
+        <location filename="../lobby/chat_moc.ui" line="40"/>
+        <source>Users in lobby</source>
+        <translation>Uživatelé v předsíni</translation>
+    </message>
+    <message>
+        <location filename="../lobby/chat_moc.ui" line="50"/>
+        <source>Global chat</source>
+        <translation>Obecná konverzace</translation>
+    </message>
+    <message>
+        <location filename="../lobby/chat_moc.ui" line="104"/>
+        <source>type you message</source>
+        <translation>zadejte vaši zprávu</translation>
+    </message>
+    <message>
+        <location filename="../lobby/chat_moc.ui" line="111"/>
+        <source>send</source>
+        <translation>poslat</translation>
+    </message>
+</context>
+<context>
+    <name>FirstLaunchView</name>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="28"/>
+        <source>Language</source>
+        <translation>Jazyk</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="53"/>
+        <source>Heroes III Data</source>
+        <translation>Data Heroes III</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="78"/>
+        <source>Mods Preset</source>
+        <translation>Předvybrané modifikace</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="127"/>
+        <source>Select your language</source>
+        <translation>Vyberte váš jazyk</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="177"/>
+        <source>Have a question? Found a bug? Want to help? Join us!</source>
+        <translation>Máte otázku? Našli jste chybu? Chcete pomoct? Připojte se k nám!</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="186"/>
+        <source>Thank you for installing VCMI!
+
+Before you can start playing, there are a few more steps that need to be completed.
+
+Please keep in mind that in order to use VCMI you must own the original data files for Heroes® of Might and Magic® III: Complete or The Shadow of Death.
+
+Heroes® of Might and Magic® III HD is currently not supported!</source>
+        <translation>Děkujeme za instalaci VCMI!
+
+Před začátkem hraní musíte ještě dokončit pár kroků.
+
+Prosíme, mějte na paměti, že abyste mohli hrát VCMI, musíte vlastnit originální datové soubory Heroes® of Might and Magic® III: Complete nebo The Shadow of Death.
+
+Heroes® of Might and Magic® III HD není v současnosti podporovaný!</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="257"/>
+        <source>Locate Heroes III data files</source>
+        <translation>Najít soubory dat Heroes III</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="346"/>
+        <source>If you don&apos;t have a copy of Heroes III installed, you can use our automatic installation tool &apos;vcmibuilder&apos;, which only requires the GoG.com Heroes III installer. Please visit our wiki for detailed instructions.</source>
+        <translation>Pokud nemáte Heroes III nainstalované, můžete použít náš automatický instalační nástroj &apos;vcmibuilder&apos;, který vyžaduje pouze GOG instalátor Heroes III. Prosíme, navštivte naši wiki pro podrobné instrukce.</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="362"/>
+        <source>To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories.</source>
+        <translation>Pro běh VCMI, datové soubory Heroes III musí být přítomny v jednom z určených umístění. Prosíme, zkopírujte data Heroes do jedné z těchto složek.</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="397"/>
+        <source>Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically.</source>
+        <translation>Nebo můžete poskytnout složku s instalací Heroes III a VCMI zkopíruje existující data automaticky.</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="426"/>
+        <source>Your Heroes III data files have been successfully found.</source>
+        <translation>Vaše soubory dat Heroes III byly úspěšně nalezeny.</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="466"/>
+        <source>The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually</source>
+        <translation>Automatické rozpoznání jazyka Heroes III selhalo. Prosíme, vyberte jazyk vašich Heroes III ručně</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="702"/>
+        <source>Interface Improvements</source>
+        <translation>Vylepšení rozhraní</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="740"/>
+        <source>Install a translation of Heroes III in your preferred language</source>
+        <translation>Instalovat překlad Heroes III vašeho upřednostněného jazyka</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="756"/>
+        <source>Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher</source>
+        <translation>Nyní můžete volitelně nainstalovat další modifikace, nebo též kdykoliv potom pomocí spouštěče VCMI</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
+        <source>Install mod that provides various interface improvements, such as better interface for random maps and selectable actions in battles</source>
+        <translation>Instalovat modifikaci, která poskytuje různá vylepšení rozhraní, například lepší rozhraní pro náhodné mapy a volitelné akce v bitvách</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="788"/>
+        <source>Install compatible version of &quot;Horn of the Abyss&quot;, a fan-made Heroes III expansion ported by the VCMI team</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="804"/>
+        <source>Install compatible version of &quot;In The Wake of Gods&quot;, a fan-made Heroes III expansion</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="851"/>
+        <source>Finish</source>
+        <translation>Dokončit</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="156"/>
+        <source>VCMI on Github</source>
+        <translation>VCMI na GitHubu</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="163"/>
+        <source>VCMI on Slack</source>
+        <translation>VCMI na Slacku</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="170"/>
+        <source>VCMI on Discord</source>
+        <translation>VCMI na Discordu</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="220"/>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="535"/>
+        <source>Next</source>
+        <translation>Další</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="288"/>
+        <source>Open help in browser</source>
+        <translation>Otevřít nápovědu v prohlížeči</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="301"/>
+        <source>Search again</source>
+        <translation>Hledat znovu</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="384"/>
+        <source>Heroes III data files</source>
+        <translation>Soubory dat Heroes III</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="413"/>
+        <source>Copy existing data</source>
+        <translation>Kopírovat existující data</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="456"/>
+        <source>Your Heroes III language has been successfully detected.</source>
+        <translation>Váš jazyk Heroes III byl úspěšně zjištěn.</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="485"/>
+        <source>Heroes III language</source>
+        <translation>Jazyk Heroes III</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="528"/>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="844"/>
+        <source>Back</source>
+        <translation>Zpět</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="566"/>
+        <source>Install VCMI Mod Preset</source>
+        <translation>Instalovat předvybrané modifiakce VCMI</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="616"/>
+        <source>Horn of the Abyss</source>
+        <translation type="unfinished"></translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="638"/>
+        <source>Heroes III Translation</source>
+        <translation>Překlady Heroes III</translation>
+    </message>
+    <message>
+        <location filename="../firstLaunch/firstlaunch_moc.ui" line="724"/>
+        <source>In The Wake of Gods</source>
+        <translation type="unfinished"></translation>
+    </message>
+</context>
+<context>
+    <name>ImageViewer</name>
+    <message>
+        <location filename="../modManager/imageviewer_moc.ui" line="20"/>
+        <source>Image Viewer</source>
+        <translation>Prohlížeč obrázků</translation>
+    </message>
+</context>
+<context>
+    <name>Language</name>
+    <message>
+        <location filename="../languages.cpp" line="23"/>
+        <source>Czech</source>
+        <translation>Čeština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="24"/>
+        <source>Chinese</source>
+        <translation>Čínština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="25"/>
+        <source>English</source>
+        <translation>Angličtina</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="26"/>
+        <source>Finnish</source>
+        <translation>Finština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="27"/>
+        <source>French</source>
+        <translation>Francouzština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="28"/>
+        <source>German</source>
+        <translation>Němčina</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="29"/>
+        <source>Hungarian</source>
+        <translation>Maďarština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="30"/>
+        <source>Italian</source>
+        <translation>Italština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="31"/>
+        <source>Korean</source>
+        <translation>Korejština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="32"/>
+        <source>Polish</source>
+        <translation>Polština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="33"/>
+        <source>Portuguese</source>
+        <translation>Portugalština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="34"/>
+        <source>Russian</source>
+        <translation>Ruština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="35"/>
+        <source>Spanish</source>
+        <translation>Španělština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="36"/>
+        <source>Swedish</source>
+        <translation>Švédština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="37"/>
+        <source>Turkish</source>
+        <translation>Turečtina</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="38"/>
+        <source>Ukrainian</source>
+        <translation>Ukrajinština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="39"/>
+        <source>Vietnamese</source>
+        <translation>Vietnamština</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="40"/>
+        <source>Other (East European)</source>
+        <translation>Ostatní (východní Evropa)</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="41"/>
+        <source>Other (Cyrillic Script)</source>
+        <translation>Ostatní (azbuka)</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="42"/>
+        <source>Other (West European)</source>
+        <translation>Ostatní (západní Evropa)</translation>
+    </message>
+    <message>
+        <location filename="../languages.cpp" line="64"/>
+        <source>Auto (%1)</source>
+        <translation>Automaticky (%1)</translation>
+    </message>
+</context>
+<context>
+    <name>Lobby</name>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="42"/>
+        <location filename="../lobby/lobby_moc.cpp" line="402"/>
+        <source>Connect</source>
+        <translation>Připojit</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="29"/>
+        <source>Username</source>
+        <translation>Uživatelské jméno</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="59"/>
+        <source>Server</source>
+        <translation>Server</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="173"/>
+        <source>Session</source>
+        <translation>Relace</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="178"/>
+        <source>Players</source>
+        <translation>Hráči</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="268"/>
+        <source>Resolve</source>
+        <translation>Vyřešit</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="280"/>
+        <source>New game</source>
+        <translation>Nová hra</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="287"/>
+        <source>Load game</source>
+        <translation>Načíst hru</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="128"/>
+        <source>New room</source>
+        <translation>Nová místnost</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="138"/>
+        <source>Join room</source>
+        <translation>Připojit se do místnosti</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="261"/>
+        <source>Ready</source>
+        <translation>Připraven</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="244"/>
+        <source>Mods mismatch</source>
+        <translation>Nesoulad modifikací</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="237"/>
+        <source>Leave</source>
+        <translation>Odejít</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="210"/>
+        <source>Kick player</source>
+        <translation>Vyhodit hráče</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.ui" line="230"/>
+        <source>Players in the room</source>
+        <translation>Hráči v místnosti</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.cpp" line="369"/>
+        <source>Disconnect</source>
+        <translation>Odpojit</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobby_moc.cpp" line="462"/>
+        <source>No issues detected</source>
+        <translation>Bez problémů</translation>
+    </message>
+</context>
+<context>
+    <name>LobbyRoomRequest</name>
+    <message>
+        <location filename="../lobby/lobbyroomrequest_moc.ui" line="17"/>
+        <source>Room settings</source>
+        <translation>Nastavení místnosti</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobbyroomrequest_moc.ui" line="32"/>
+        <source>Room name</source>
+        <translation>Název místnosti</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobbyroomrequest_moc.ui" line="42"/>
+        <source>Maximum players</source>
+        <translation>Maximum hráčů</translation>
+    </message>
+    <message>
+        <location filename="../lobby/lobbyroomrequest_moc.ui" line="97"/>
+        <source>Password (optional)</source>
+        <translation>Heslo (volitelné)</translation>
+    </message>
+</context>
+<context>
+    <name>MainWindow</name>
+    <message>
+        <location filename="../mainwindow_moc.ui" line="20"/>
+        <source>VCMI Launcher</source>
+        <translation>Spouštěč VCMI</translation>
+    </message>
+    <message>
+        <location filename="../mainwindow_moc.ui" line="107"/>
+        <source>Settings</source>
+        <translation>Nastavení</translation>
+    </message>
+    <message>
+        <location filename="../mainwindow_moc.ui" line="207"/>
+        <source>Help</source>
+        <translation>Nápověda</translation>
+    </message>
+    <message>
+        <location filename="../mainwindow_moc.ui" line="276"/>
+        <source>Map Editor</source>
+        <translation>Editor map</translation>
+    </message>
+    <message>
+        <location filename="../mainwindow_moc.ui" line="329"/>
+        <source>Start game</source>
+        <translation>Spustit hru</translation>
+    </message>
+    <message>
+        <location filename="../mainwindow_moc.ui" line="157"/>
+        <source>Lobby</source>
+        <translation>Předsíň</translation>
+    </message>
+    <message>
+        <location filename="../mainwindow_moc.ui" line="57"/>
+        <source>Mods</source>
+        <translation>Modifikace</translation>
+    </message>
+</context>
+<context>
+    <name>UpdateDialog</name>
+    <message>
+        <location filename="../updatedialog_moc.ui" line="71"/>
+        <source>You have the latest version</source>
+        <translation>Máte nejnovější verzi</translation>
+    </message>
+    <message>
+        <location filename="../updatedialog_moc.ui" line="94"/>
+        <source>Close</source>
+        <translation>Zavřít</translation>
+    </message>
+    <message>
+        <location filename="../updatedialog_moc.ui" line="101"/>
+        <source>Check for updates on startup</source>
+        <translation>Zkontrolovat aktualizace při startu</translation>
+    </message>
+</context>
+</TS>

+ 20 - 6
lib/BasicTypes.cpp

@@ -93,6 +93,16 @@ int AFactionMember::getPrimSkillLevel(PrimarySkill id) const
 
 int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const
 {
+	int32_t maxGoodMorale = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE).size();
+	int32_t maxBadMorale = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE).size();
+
+	if(getBonusBearer()->hasBonusOfType(BonusType::MAX_MORALE))
+	{
+		if(bonusList && !bonusList->empty())
+			bonusList = std::make_shared<const BonusList>();
+		return maxGoodMorale;
+	}
+
 	static const auto unaffectedByMoraleSelector = Selector::type()(BonusType::NON_LIVING).Or(Selector::type()(BonusType::UNDEAD))
 													.Or(Selector::type()(BonusType::SIEGE_WEAPON)).Or(Selector::type()(BonusType::NO_MORALE));
 
@@ -109,14 +119,21 @@ int AFactionMember::moraleValAndBonusList(TConstBonusListPtr & bonusList) const
 	static const std::string cachingStrMor = "type_MORALE";
 	bonusList = getBonusBearer()->getBonuses(moraleSelector, cachingStrMor);
 
-	int32_t maxGoodMorale = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE).size();
-	int32_t maxBadMorale = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_MORALE_DICE).size();
-
 	return std::clamp(bonusList->totalValue(), maxBadMorale, maxGoodMorale);
 }
 
 int AFactionMember::luckValAndBonusList(TConstBonusListPtr & bonusList) const
 {
+	int32_t maxGoodLuck = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE).size();
+	int32_t maxBadLuck = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE).size();
+
+	if(getBonusBearer()->hasBonusOfType(BonusType::MAX_LUCK))
+	{
+		if(bonusList && !bonusList->empty())
+			bonusList = std::make_shared<const BonusList>();
+		return maxGoodLuck;
+	}
+
 	if(getBonusBearer()->hasBonusOfType(BonusType::NO_LUCK))
 	{
 		if(bonusList && !bonusList->empty())
@@ -128,9 +145,6 @@ int AFactionMember::luckValAndBonusList(TConstBonusListPtr & bonusList) const
 	static const std::string cachingStrLuck = "type_LUCK";
 	bonusList = getBonusBearer()->getBonuses(luckSelector, cachingStrLuck);
 
-	int32_t maxGoodLuck = VLC->settings()->getVector(EGameSettings::COMBAT_GOOD_LUCK_DICE).size();
-	int32_t maxBadLuck = - (int32_t) VLC->settings()->getVector(EGameSettings::COMBAT_BAD_LUCK_DICE).size();
-
 	return std::clamp(bonusList->totalValue(), maxBadLuck, maxGoodLuck);
 }
 

+ 1 - 13
lib/CArtHandler.cpp

@@ -625,7 +625,7 @@ void CArtHandler::makeItCommanderArt(CArtifact * a, bool onlyCommander)
 		a->possibleSlots[ArtBearer::COMMANDER].push_back(ArtifactPosition(slot));
 }
 
-bool CArtHandler::legalArtifact(const ArtifactID & id)
+bool CArtHandler::legalArtifact(const ArtifactID & id) const
 {
 	auto art = id.toArtifact();
 	//assert ( (!art->constituents) || art->constituents->size() ); //artifacts is not combined or has some components
@@ -648,18 +648,6 @@ bool CArtHandler::legalArtifact(const ArtifactID & id)
 	return false;
 }
 
-void CArtHandler::initAllowedArtifactsList(const std::set<ArtifactID> & allowed)
-{
-	allowedArtifacts.clear();
-
-	for (ArtifactID i : allowed)
-	{
-		if (legalArtifact(ArtifactID(i)))
-			allowedArtifacts.push_back(i.toArtifact());
-			//keep im mind that artifact can be worn by more than one type of bearer
-	}
-}
-
 std::set<ArtifactID> CArtHandler::getDefaultAllowed() const
 {
 	std::set<ArtifactID> allowedArtifacts;

+ 1 - 5
lib/CArtHandler.h

@@ -141,15 +141,11 @@ public:
 class DLL_LINKAGE CArtHandler : public CHandlerBase<ArtifactID, Artifact, CArtifact, ArtifactService>
 {
 public:
-	/// List of artifacts allowed on the map
-	std::vector<const CArtifact *> allowedArtifacts;
-
 	void addBonuses(CArtifact *art, const JsonNode &bonusList);
 
 	static CArtifact::EartClass stringToClass(const std::string & className); //TODO: rework EartClass to make this a constructor
 
-	bool legalArtifact(const ArtifactID & id);
-	void initAllowedArtifactsList(const std::set<ArtifactID> & allowed);
+	bool legalArtifact(const ArtifactID & id) const;
 	static void makeItCreatureArt(CArtifact * a, bool onlyCreature = true);
 	static void makeItCommanderArt(CArtifact * a, bool onlyCommander = true);
 

+ 5 - 3
lib/CArtifactInstance.cpp

@@ -64,10 +64,7 @@ SpellID CScrollArtifactInstance::getScrollSpellID() const
 	auto artInst = static_cast<const CArtifactInstance*>(this);
 	const auto bonus = artInst->getBonusLocalFirst(Selector::type()(BonusType::SPELL));
 	if(!bonus)
-	{
-		logMod->warn("Warning: %s doesn't bear any spell!", artInst->nodeName());
 		return SpellID::NONE;
-	}
 	return bonus->subtype.as<SpellID>();
 }
 
@@ -165,6 +162,11 @@ bool CArtifactInstance::isCombined() const
 	return artType->isCombined();
 }
 
+bool CArtifactInstance::isScroll() const
+{
+	return artType->isScroll();
+}
+
 void CArtifactInstance::putAt(CArtifactSet & set, const ArtifactPosition slot)
 {
 	auto placementMap = set.putArtifact(slot, this);

+ 1 - 0
lib/CArtifactInstance.h

@@ -87,6 +87,7 @@ public:
 	bool canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot = ArtifactPosition::FIRST_AVAILABLE,
 		bool assumeDestRemoved = false) const;
 	bool isCombined() const;
+	bool isScroll() const;
 	void putAt(CArtifactSet & set, const ArtifactPosition slot);
 	void removeFrom(CArtifactSet & set, const ArtifactPosition slot);
 	void move(CArtifactSet & srcSet, const ArtifactPosition srcSlot, CArtifactSet & dstSet, const ArtifactPosition dstSlot);

+ 5 - 0
lib/CGameInfoCallback.cpp

@@ -721,6 +721,11 @@ bool CGameInfoCallback::isPlayerMakingTurn(PlayerColor player) const
 	return gs->actingPlayers.count(player);
 }
 
+CGameInfoCallback::CGameInfoCallback():
+	gs(nullptr)
+{
+}
+
 CGameInfoCallback::CGameInfoCallback(CGameState * GS):
 	gs(GS)
 {

+ 1 - 1
lib/CGameInfoCallback.h

@@ -134,7 +134,7 @@ class DLL_LINKAGE CGameInfoCallback : public IGameInfoCallback
 protected:
 	CGameState * gs;//todo: replace with protected const getter, only actual Server and Client objects should hold game state
 
-	CGameInfoCallback() = default;
+	CGameInfoCallback();
 	CGameInfoCallback(CGameState * GS);
 	bool hasAccess(std::optional<PlayerColor> playerId) const;
 

+ 2 - 3
lib/GameSettings.cpp

@@ -118,11 +118,10 @@ void GameSettings::load(const JsonNode & input)
 
 const JsonNode & GameSettings::getValue(EGameSettings option) const
 {
-	assert(option < EGameSettings::OPTIONS_COUNT);
 	auto index = static_cast<size_t>(option);
 
-	assert(!gameSettings[index].isNull());
-	return gameSettings[index];
+	assert(!gameSettings.at(index).isNull());
+	return gameSettings.at(index);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 35 - 25
lib/JsonNode.cpp

@@ -310,63 +310,73 @@ JsonMap & JsonNode::Struct()
 const bool boolDefault = false;
 bool JsonNode::Bool() const
 {
-	if (getType() == JsonType::DATA_NULL)
-		return boolDefault;
-	assert(getType() == JsonType::DATA_BOOL);
-	return std::get<bool>(data);
+	assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_BOOL);
+
+	if (getType() == JsonType::DATA_BOOL)
+		return std::get<bool>(data);
+
+	return boolDefault;
 }
 
 const double floatDefault = 0;
 double JsonNode::Float() const
 {
-	if(getType() == JsonType::DATA_NULL)
-		return floatDefault;
+	assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_INTEGER || getType() == JsonType::DATA_FLOAT);
+
+	if(getType() == JsonType::DATA_FLOAT)
+		return std::get<double>(data);
 
 	if(getType() == JsonType::DATA_INTEGER)
 		return static_cast<double>(std::get<si64>(data));
 
-	assert(getType() == JsonType::DATA_FLOAT);
-	return std::get<double>(data);
+	return floatDefault;
 }
 
-const si64 integetDefault = 0;
+const si64 integerDefault = 0;
 si64 JsonNode::Integer() const
 {
-	if(getType() == JsonType::DATA_NULL)
-		return integetDefault;
+	assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_INTEGER || getType() == JsonType::DATA_FLOAT);
+
+	if(getType() == JsonType::DATA_INTEGER)
+		return std::get<si64>(data);
 
 	if(getType() == JsonType::DATA_FLOAT)
 		return static_cast<si64>(std::get<double>(data));
 
-	assert(getType() == JsonType::DATA_INTEGER);
-	return std::get<si64>(data);
+	return integerDefault;
 }
 
 const std::string stringDefault = std::string();
 const std::string & JsonNode::String() const
 {
-	if (getType() == JsonType::DATA_NULL)
-		return stringDefault;
-	assert(getType() == JsonType::DATA_STRING);
-	return std::get<std::string>(data);
+	assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_STRING);
+
+	if (getType() == JsonType::DATA_STRING)
+		return std::get<std::string>(data);
+
+	return stringDefault;
 }
 
 const JsonVector vectorDefault = JsonVector();
 const JsonVector & JsonNode::Vector() const
 {
-	if (getType() == JsonType::DATA_NULL)
-		return vectorDefault;
-	assert(getType() == JsonType::DATA_VECTOR);
-	return std::get<JsonVector>(data);
+	assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_VECTOR);
+
+	if (getType() == JsonType::DATA_VECTOR)
+		return std::get<JsonVector>(data);
+
+	return vectorDefault;
 }
 
 const JsonMap mapDefault = JsonMap();
 const JsonMap & JsonNode::Struct() const
 {
-	if (getType() == JsonType::DATA_NULL)
-		return mapDefault;
-	assert(getType() == JsonType::DATA_STRUCT);
-	return std::get<JsonMap>(data);
+	assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_STRUCT);
+
+	if (getType() == JsonType::DATA_STRUCT)
+		return std::get<JsonMap>(data);
+
+	return mapDefault;
 }
 
 JsonNode & JsonNode::operator[](const std::string & child)

+ 3 - 2
lib/JsonRandom.cpp

@@ -384,8 +384,9 @@ namespace JsonRandom
 	ArtifactID loadArtifact(const JsonNode & value, CRandomGenerator & rng, const Variables & variables)
 	{
 		std::set<ArtifactID> allowedArts;
-		for (auto const * artifact : VLC->arth->allowedArtifacts)
-			allowedArts.insert(artifact->getId());
+		for(const auto & artifact : VLC->arth->objects)
+			if (IObjectInterface::cb->isAllowed(artifact->getId()) && VLC->arth->legalArtifact(artifact->getId()))
+				allowedArts.insert(artifact->getId());
 
 		std::set<ArtifactID> potentialPicks = filterKeys(value, allowedArts, variables);
 

+ 24 - 21
lib/Languages.h

@@ -68,6 +68,9 @@ struct Options
 	/// primary IETF language tag
 	std::string tagIETF;
 
+	/// DateTime format
+	std::string dateTimeFormat;
+
 	/// Ruleset for plural forms in this language
 	EPluralForms pluralForms = EPluralForms::NONE;
 
@@ -79,27 +82,27 @@ inline const auto & getLanguageList()
 {
 	static const std::array<Options, 20> languages
 	{ {
-		{ "czech",       "Czech",       "Čeština",    "CP1250", "cs", EPluralForms::CZ_3, true },
-		{ "chinese",     "Chinese",     "简体中文",       "GBK",    "zh", EPluralForms::VI_1, true }, // Note: actually Simplified Chinese
-		{ "english",     "English",     "English",    "CP1252", "en", EPluralForms::EN_2, true },
-		{ "finnish",     "Finnish",     "Suomi",      "CP1252", "fi", EPluralForms::EN_2, true },
-		{ "french",      "French",      "Français",   "CP1252", "fr", EPluralForms::FR_2, true },
-		{ "german",      "German",      "Deutsch",    "CP1252", "de", EPluralForms::EN_2, true },
-		{ "hungarian",   "Hungarian",   "Magyar",     "CP1250", "hu", EPluralForms::EN_2, true },
-		{ "italian",     "Italian",     "Italiano",   "CP1250", "it", EPluralForms::EN_2, true },
-		{ "korean",      "Korean",      "한국어",        "CP949",  "ko", EPluralForms::VI_1, true },
-		{ "polish",      "Polish",      "Polski",     "CP1250", "pl", EPluralForms::PL_3, true },
-		{ "portuguese",  "Portuguese",  "Português",  "CP1252", "pt", EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese
-		{ "russian",     "Russian",     "Русский",    "CP1251", "ru", EPluralForms::UK_3, true },
-		{ "spanish",     "Spanish",     "Español",    "CP1252", "es", EPluralForms::EN_2, true },
-		{ "swedish",     "Swedish",     "Svenska",    "CP1252", "sv", EPluralForms::EN_2, true },
-		{ "turkish",     "Turkish",     "Türkçe",     "CP1254", "tr", EPluralForms::EN_2, true },
-		{ "ukrainian",   "Ukrainian",   "Українська", "CP1251", "uk", EPluralForms::UK_3, true },
-		{ "vietnamese",  "Vietnamese",  "Tiếng Việt", "UTF-8",  "vi", EPluralForms::VI_1, true }, // Fan translation uses special encoding
-
-		{ "other_cp1250", "Other (East European)",   "", "CP1250", "", EPluralForms::NONE, false },
-		{ "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", EPluralForms::NONE, false },
-		{ "other_cp1252", "Other (West European)",   "", "CP1252", "", EPluralForms::NONE, false }
+		{ "czech",       "Czech",       "Čeština",    "CP1250", "cs", "%d.%m.%Y %T", EPluralForms::CZ_3, true },
+		{ "chinese",     "Chinese",     "简体中文",       "GBK",    "zh", "%F %T", EPluralForms::VI_1, true }, // Note: actually Simplified Chinese
+		{ "english",     "English",     "English",    "CP1252", "en", "%F %T", EPluralForms::EN_2, true }, // English uses international date/time format here
+		{ "finnish",     "Finnish",     "Suomi",      "CP1252", "fi", "%d.%m.%Y %T", EPluralForms::EN_2, true },
+		{ "french",      "French",      "Français",   "CP1252", "fr", "%d/%m/%Y %T", EPluralForms::FR_2, true },
+		{ "german",      "German",      "Deutsch",    "CP1252", "de", "%d.%m.%Y %T", EPluralForms::EN_2, true },
+		{ "hungarian",   "Hungarian",   "Magyar",     "CP1250", "hu", "%Y. %m. %d. %T", EPluralForms::EN_2, true },
+		{ "italian",     "Italian",     "Italiano",   "CP1250", "it", "%d/%m/%Y %T", EPluralForms::EN_2, true },
+		{ "korean",      "Korean",      "한국어",        "CP949",  "ko", "%F %T", EPluralForms::VI_1, true },
+		{ "polish",      "Polish",      "Polski",     "CP1250", "pl", "%d.%m.%Y %T", EPluralForms::PL_3, true },
+		{ "portuguese",  "Portuguese",  "Português",  "CP1252", "pt", "%d/%m/%Y %T", EPluralForms::EN_2, true }, // Note: actually Brazilian Portuguese
+		{ "russian",     "Russian",     "Русский",    "CP1251", "ru", "%d.%m.%Y %T", EPluralForms::UK_3, true },
+		{ "spanish",     "Spanish",     "Español",    "CP1252", "es", "%d/%m/%Y %T", EPluralForms::EN_2, true },
+		{ "swedish",     "Swedish",     "Svenska",    "CP1252", "sv", "%F %T", EPluralForms::EN_2, true },
+		{ "turkish",     "Turkish",     "Türkçe",     "CP1254", "tr", "%d.%m.%Y %T", EPluralForms::EN_2, true },
+		{ "ukrainian",   "Ukrainian",   "Українська", "CP1251", "uk", "%d.%m.%Y %T", EPluralForms::UK_3, true },
+		{ "vietnamese",  "Vietnamese",  "Tiếng Việt", "UTF-8",  "vi", "%d/%m/%Y %T", EPluralForms::VI_1, true }, // Fan translation uses special encoding
+
+		{ "other_cp1250", "Other (East European)",   "", "CP1250", "", "", EPluralForms::NONE, false },
+		{ "other_cp1251", "Other (Cyrillic Script)", "", "CP1251", "", "", EPluralForms::NONE, false },
+		{ "other_cp1252", "Other (West European)",   "", "CP1252", "", "", EPluralForms::NONE, false }
 	} };
 	static_assert(languages.size() == static_cast<size_t>(ELanguages::COUNT), "Languages array is missing a value!");
 

+ 9 - 0
lib/TextOperations.cpp

@@ -11,6 +11,10 @@
 #include "TextOperations.h"
 
 #include "CGeneralTextHandler.h"
+#include "Languages.h"
+#include "CConfigHandler.h"
+
+#include <vstd/DateUtils.h>
 
 #include <boost/locale.hpp>
 
@@ -210,4 +214,9 @@ std::string TextOperations::escapeString(std::string input)
 	return input;
 }
 
+std::string TextOperations::getFormattedDateTimeLocal(std::time_t dt)
+{
+	return vstd::getFormattedDateTime(dt, Languages::getLanguageOptions(settings["general"]["language"].String()).dateTimeFormat);
+}
+
 VCMI_LIB_NAMESPACE_END

+ 3 - 0
lib/TextOperations.h

@@ -56,6 +56,9 @@ namespace TextOperations
 
 	/// replaces all symbols that normally need escaping with appropriate escape sequences
 	std::string escapeString(std::string input);
+
+	/// get formatted DateTime depending on the language selected
+	DLL_LINKAGE std::string getFormattedDateTimeLocal(std::time_t dt);
 };
 
 

Kaikkia tiedostoja ei voida näyttää, sillä liian monta tiedostoa muuttui tässä diffissä