浏览代码

Merge pull request #3388 from vcmi/beta

Merge beta -> master
Ivan Savenko 1 年之前
父节点
当前提交
a577a7466b
共有 100 个文件被更改,包括 1386 次插入714 次删除
  1. 3 2
      AI/BattleAI/BattleEvaluator.cpp
  2. 80 0
      ChangeLog.md
  3. 65 57
      Mods/vcmi/config/vcmi/czech.json
  4. 2 0
      Mods/vcmi/config/vcmi/english.json
  5. 4 0
      Mods/vcmi/config/vcmi/polish.json
  6. 4 0
      Mods/vcmi/config/vcmi/ukrainian.json
  7. 1 1
      android/vcmi-app/build.gradle
  8. 4 0
      client/NetPacksLobbyClient.cpp
  9. 8 6
      client/PlayerLocalState.cpp
  10. 2 2
      client/PlayerLocalState.h
  11. 7 2
      client/adventureMap/AdventureMapInterface.cpp
  12. 3 0
      client/adventureMap/AdventureMapInterface.h
  13. 27 21
      client/adventureMap/CList.cpp
  14. 1 1
      client/adventureMap/CList.h
  15. 140 111
      client/adventureMap/TurnTimerWidget.cpp
  16. 23 30
      client/adventureMap/TurnTimerWidget.h
  17. 3 3
      client/battle/BattleActionsController.cpp
  18. 2 0
      client/battle/BattleInterface.cpp
  19. 1 0
      client/battle/BattleInterface.h
  20. 22 2
      client/battle/BattleInterfaceClasses.cpp
  21. 4 2
      client/battle/BattleInterfaceClasses.h
  22. 2 0
      client/battle/BattleStacksController.cpp
  23. 43 4
      client/battle/BattleWindow.cpp
  24. 5 0
      client/battle/BattleWindow.h
  25. 2 4
      client/lobby/CBonusSelection.cpp
  26. 2 0
      client/lobby/CLobbyScreen.cpp
  27. 6 3
      client/lobby/CSelectionBase.cpp
  28. 2 0
      client/lobby/CSelectionBase.h
  29. 5 1
      client/lobby/RandomMapTab.cpp
  30. 3 0
      client/mapView/MapViewController.cpp
  31. 14 2
      client/widgets/MiscWidgets.cpp
  32. 3 0
      client/widgets/MiscWidgets.h
  33. 6 3
      client/widgets/TextControls.cpp
  34. 3 1
      client/windows/GUIClasses.cpp
  35. 2 1
      client/windows/GUIClasses.h
  36. 7 0
      client/windows/settings/AdventureOptionsTab.cpp
  37. 11 1
      client/windows/settings/GeneralOptionsTab.cpp
  38. 2 0
      config/artifacts.json
  39. 6 1
      config/schemas/settings.json
  40. 4 1
      config/spells/timed.json
  41. 2 1
      config/terrainViewPatterns.json
  42. 14 6
      config/widgets/settings/adventureOptionsTab.json
  43. 29 0
      config/widgets/settings/generalOptionsTab.json
  44. 0 34
      config/widgets/turnTimer.json
  45. 1 1
      debian/changelog
  46. 1 0
      docs/Readme.md
  47. 1 1
      launcher/eu.vcmi.VCMI.metainfo.xml
  48. 35 25
      lib/JsonNode.cpp
  49. 9 7
      lib/LoadProgress.cpp
  50. 37 0
      lib/TurnTimerInfo.cpp
  51. 5 10
      lib/TurnTimerInfo.h
  52. 15 21
      lib/battle/CBattleInfoCallback.cpp
  53. 1 7
      lib/battle/CBattleInfoCallback.h
  54. 7 0
      lib/bonuses/BonusSelector.cpp
  55. 1 0
      lib/bonuses/BonusSelector.h
  56. 1 1
      lib/bonuses/CBonusSystemNode.cpp
  57. 16 8
      lib/int3.h
  58. 20 0
      lib/mapObjectConstructors/AObjectTypeHandler.cpp
  59. 1 0
      lib/mapObjectConstructors/AObjectTypeHandler.h
  60. 4 1
      lib/mapObjectConstructors/CObjectClassesHandler.cpp
  61. 2 2
      lib/mapObjects/CArmedInstance.cpp
  62. 1 1
      lib/mapObjects/CArmedInstance.h
  63. 11 2
      lib/mapObjects/CRewardableObject.cpp
  64. 27 1
      lib/mapObjects/MiscObjects.cpp
  65. 2 0
      lib/mapObjects/MiscObjects.h
  66. 2 4
      lib/mapping/CMapEditManager.cpp
  67. 1 1
      lib/mapping/CMapEditManager.h
  68. 14 8
      lib/mapping/CMapOperation.cpp
  69. 2 1
      lib/mapping/CMapOperation.h
  70. 2 0
      lib/mapping/MapEditUtils.cpp
  71. 2 0
      lib/mapping/MapEditUtils.h
  72. 16 5
      lib/mapping/ObstacleProxy.cpp
  73. 1 1
      lib/networkPacks/PacksForLobby.h
  74. 1 0
      lib/rewardable/Reward.cpp
  75. 24 18
      lib/rmg/RmgArea.cpp
  76. 1 1
      lib/rmg/RmgArea.h
  77. 7 2
      lib/rmg/RmgMap.cpp
  78. 3 1
      lib/rmg/RmgMap.h
  79. 90 55
      lib/rmg/RmgObject.cpp
  80. 7 3
      lib/rmg/RmgObject.h
  81. 4 3
      lib/rmg/RmgPath.cpp
  82. 68 10
      lib/rmg/Zone.cpp
  83. 1 0
      lib/rmg/Zone.h
  84. 2 1
      lib/rmg/modificators/ConnectionsPlacer.cpp
  85. 91 31
      lib/rmg/modificators/ObjectManager.cpp
  86. 3 1
      lib/rmg/modificators/ObjectManager.h
  87. 2 2
      lib/rmg/modificators/ObstaclePlacer.cpp
  88. 92 68
      lib/rmg/modificators/TreasurePlacer.cpp
  89. 5 5
      lib/rmg/modificators/WaterProxy.cpp
  90. 13 13
      lib/rmg/threadpool/MapProxy.cpp
  91. 11 11
      lib/rmg/threadpool/MapProxy.h
  92. 3 1
      mapeditor/mapcontroller.cpp
  93. 32 32
      mapeditor/translation/czech.ts
  94. 8 5
      server/CGameHandler.cpp
  95. 5 5
      server/NetPacksLobbyServer.cpp
  96. 18 11
      server/TurnTimerHandler.cpp
  97. 1 3
      server/TurnTimerHandler.h
  98. 39 15
      server/battles/BattleActionProcessor.cpp
  99. 1 1
      server/processors/PlayerMessageProcessor.cpp
  100. 37 5
      server/processors/TurnOrderProcessor.cpp

+ 3 - 2
AI/BattleAI/BattleEvaluator.cpp

@@ -70,8 +70,9 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
 std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
 std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
 {
 {
 	//TODO: faerie dragon type spell should be selected by server
 	//TODO: faerie dragon type spell should be selected by server
-	SpellID creatureSpellToCast = cb->getBattle(battleID)->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED);
-	if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE)
+	SpellID creatureSpellToCast = cb->getBattle(battleID)->getRandomCastedSpell(CRandomGenerator::getDefault(), stack);
+
+	if(stack->canCast() && creatureSpellToCast != SpellID::NONE)
 	{
 	{
 		const CSpell * spell = creatureSpellToCast.toSpell();
 		const CSpell * spell = creatureSpellToCast.toSpell();
 
 

+ 80 - 0
ChangeLog.md

@@ -1,3 +1,83 @@
+# 1.4.1 -> 1.4.2
+
+### General
+* Restored support for Windows 7
+* Restored support for 32-bit builds
+* Implemented quick backpack window for slot-specific artifact selection, activated via mouse wheel / swipe gesture
+* Added option to search for specific spell in the spellbook
+* Added option to skip fading animation on adventure map
+* Using alt-tab to switch to another application will no longer activate in-game console/chat
+* Increased frequency of checks for server startup to improve server connection time
+* added nwcfollowthewhiterabbit / vcmiluck cheat: the currently selected hero permanently gains maximum luck.
+* added nwcmorpheus / vcmimorale cheat: the currently selected hero permanently gains maximum morale.
+* added nwcoracle / vcmiobelisk cheat: the puzzle map is permanently revealed.
+* added nwctheone / vcmigod cheat: reveals the whole map, gives 5 archangels in each empty slot, unlimited movement points and permanent flight to currently selected hero
+
+### Launcher
+* Launcher will now properly show mod installation progress
+* Launcher will now correctly select preferred language on first start
+
+### Multiplayer
+* Timers for all players will now be visible at once
+* Turn options menu will correctly open for guests when host switches to it
+* Guests will correctly see which roads are allowed for random maps by host
+* Game will now correctly deactivate unit when timer runs out in pvp battle
+* Game will show turn, battle and unit timers separately during battles
+* Timer in pvp battles will be only active if unit timer is non-zero
+* Timer during adventure map turn will be active only if turn timer is non-zero
+* Game will now send notifications to players when simultaneous turns end
+
+### Stability
+* Fixed crash on clicking town or hero list on MacOS and iOS
+* Fixed crash on closing vcmi on Android
+* Fixed crash on disconnection from multiplayer game
+* Fixed crash on finishing game on last day of the month
+* Fixed crash on loading h3m maps with mods that alter Witch Hut, Shrine or Scholar
+* Fixed crash on opening creature morale detalisation in some localizations
+* Fixed possible crash on starting a battle when opening sound from previous battle is still playing
+* Fixed crash on map loading in case if there is no suitable option for a random dwelling
+* Fixed crash on usage of radial wheel to reorder towns or heroes
+* Fixed possible crash on random map generation
+* Fixed crash on attempting to transfer last creature when stack experience is enabled
+* Fixed crash on accessing invalid settings options
+* Fixed server crash on receiving invalid message from player
+* Added check for presence of Armageddon Blade campaign files to avoid crash on some Heroes 3 versions
+
+### Random Maps Generator
+* Improved performance of random maps generation
+* Rebalance of treasure values and density
+* Improve junction zones generation by spacing Monoliths
+* Reduced amount of terrain decorations to level more in line with H3
+* Generator will now avoid path routing near map border
+* Generator will now check full object area for minimum distance requirement
+* Fixed routing of roads behind Subterranean Gates, Monoliths and Mines
+* Fixed remaining issues with placement of Corpse
+* Fixed placement of one-tile prisons from HotA
+* Fixed spawning of Armageddon's Blade and Vial of Dragon Blood on random maps
+
+### Interface
+* Right-clicking hero icon during levelup dialog will now show hero status window
+* Added indicator of current turn to unit turn order panel in battles
+* Reduces upscaling artifacts on large spellbook
+* Game will now display correct date of saved games on Android
+* Fixed black screen appearing during spellbook page flip animation 
+* Fixed description of "Start map with hero" bonus in campaigns
+* Fixed invisible chat text input in game lobby
+* Fixed positioning of chat history in game lobby
+* "Infobar Creature Management" option is now enabled by default
+* "Large Spellbook" option is now enabled by default
+
+### Mechanics
+* Anti-magic garrison now actually blocks spell casting
+* Berserk spell will no longer cancel if affected unit performs counterattack
+* Frenzy spell can no longer be casted on units that should be immune to it
+* Master Genie will no longer attempt to cast beneficial spell on creatures immune to it
+* Vitality and damage skills of a commander will now correctly grow with level
+
+### Modding
+* Added UNTIL_OWN_ATTACK duration type for bonuses
+* Configurable objects with visit mode "first" and "random" now respect "canRefuse" flag
+
 # 1.4.0 -> 1.4.1
 # 1.4.0 -> 1.4.1
 
 
 ### General
 ### General

+ 65 - 57
Mods/vcmi/config/vcmi/czech.json

@@ -54,6 +54,8 @@
 	"vcmi.radialWheel.moveDown" : "Move down",
 	"vcmi.radialWheel.moveDown" : "Move down",
 	"vcmi.radialWheel.moveBottom" : "Move to bottom",
 	"vcmi.radialWheel.moveBottom" : "Move to bottom",
 
 
+	"vcmi.spellBook.search" : "hledat...",
+
 	"vcmi.mainMenu.serverConnecting" : "Připojování...",
 	"vcmi.mainMenu.serverConnecting" : "Připojování...",
 	"vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:",
 	"vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:",
 	"vcmi.mainMenu.serverConnectionFailed" : "Připojování selhalo",
 	"vcmi.mainMenu.serverConnectionFailed" : "Připojování selhalo",
@@ -69,6 +71,7 @@
 	"vcmi.lobby.noPreview" : "bez náhledu",
 	"vcmi.lobby.noPreview" : "bez náhledu",
 	"vcmi.lobby.noUnderground" : "bez podzemí",
 	"vcmi.lobby.noUnderground" : "bez podzemí",
 
 
+	"vcmi.client.errors.missingCampaigns" : "{Chybějící datové soubory}\n\nDatové soubory kampaně nebyly nalezeny! Možná máte nekompletní nebo poškozené datové soubory Heroes 3. Prosíme, přeinstalujte hru.",
 	"vcmi.server.errors.existingProcess" : "Již běží jiný server VCMI. Prosím, ukončete ho před startem nové hry.",
 	"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.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.errors.modsToDisable"   : "{Následující modifikace musí být zakázány}",
@@ -78,11 +81,11 @@
 	"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.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.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",
+	"vcmi.settingsMainWindow.generalTab.help"     : "Přepne na kartu obecných nastavení, která obsahuje nastavení související s obecným chováním klienta hry.",
 	"vcmi.settingsMainWindow.battleTab.hover" : "Bitva",
 	"vcmi.settingsMainWindow.battleTab.hover" : "Bitva",
-	"vcmi.settingsMainWindow.battleTab.help"     : "Přepne na kartu nastavení bitvy, která umožňuje konfiguraci chování hry v bitvách",
+	"vcmi.settingsMainWindow.battleTab.help"     : "Přepne na kartu nastavení bitvy, která umožňuje konfiguraci chování hry v bitvách.",
 	"vcmi.settingsMainWindow.adventureTab.hover" : "Mapa světa",
 	"vcmi.settingsMainWindow.adventureTab.hover" : "Mapa světa",
-	"vcmi.settingsMainWindow.adventureTab.help"  : "Přepne na kartu nastavení mapy světa (mapa světa je sekce hry, ve které hráči mohou ovládat pohyb hrdinů)",
+	"vcmi.settingsMainWindow.adventureTab.help"  : "Přepne na kartu nastavení mapy světa (mapa světa je sekce hry, ve které hráči mohou ovládat pohyb hrdinů).",
 
 
 	"vcmi.systemOptions.videoGroup" : "Nastavení obrazu",
 	"vcmi.systemOptions.videoGroup" : "Nastavení obrazu",
 	"vcmi.systemOptions.audioGroup" : "Nastavení zvuku",
 	"vcmi.systemOptions.audioGroup" : "Nastavení zvuku",
@@ -107,28 +110,32 @@
 	"vcmi.systemOptions.longTouchMenu.help"      : "Změnit dobu dlouhého podržení.",
 	"vcmi.systemOptions.longTouchMenu.help"      : "Změnit dobu dlouhého podržení.",
 	"vcmi.systemOptions.longTouchMenu.entry"     : "%d milisekund",
 	"vcmi.systemOptions.longTouchMenu.entry"     : "%d milisekund",
 	"vcmi.systemOptions.framerateButton.hover"  : "Zobrazit FPS",
 	"vcmi.systemOptions.framerateButton.hover"  : "Zobrazit FPS",
-	"vcmi.systemOptions.framerateButton.help"   : "{Zobrazit FPS}\n\nPřepne viditelnost počitadla snímků za sekundu v rohu obrazovky hry",
+	"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.hover"  : "Vibrace",
-	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Vibrace}\n\nPřepnout stav vibrací při dotykovém ovládání",
+	"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.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.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.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.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.systemOptions.audioMuteFocus.hover"  : "Ztlumit při neaktivitě",
+	"vcmi.systemOptions.audioMuteFocus.help"   : "{Ztlumit při neaktivitě}\n\nZtlumit zvuk, pokud je okno hry v pozadí. Výjimkou jsou zprávy ve hře a zvuk nového tahu.",
 
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "Zobrazit zprávy v panelu informací",
 	"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ě.",
 	"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ě.",
 	"vcmi.adventureOptions.numericQuantities.hover" : "Číselné množství jednotek",
 	"vcmi.adventureOptions.numericQuantities.hover" : "Číselné množství jednotek",
 	"vcmi.adventureOptions.numericQuantities.help" : "{Číselné množství jednotek}\n\nZobrazit přibližné množství nepřátelských jednotek ve formátu A-B.",
 	"vcmi.adventureOptions.numericQuantities.help" : "{Číselné množství jednotek}\n\nZobrazit přibližné množství nepřátelských jednotek ve formátu A-B.",
 	"vcmi.adventureOptions.forceMovementInfo.hover" : "Vždy zobrazit cenu pohybu",
 	"vcmi.adventureOptions.forceMovementInfo.hover" : "Vždy zobrazit cenu pohybu",
-	"vcmi.adventureOptions.forceMovementInfo.help" : "{Vždy zobrazit cenu pohybu}\n\nVždy zobrazit informace o bodech pohybu v panelu informací. (Místo zobrazení pouze při stisknuté klávese ALT)",
+	"vcmi.adventureOptions.forceMovementInfo.help" : "{Vždy zobrazit cenu pohybu}\n\nVždy zobrazit informace o bodech pohybu v panelu informací. (Místo zobrazení pouze při stisknuté klávese ALT).",
 	"vcmi.adventureOptions.showGrid.hover" : "Zobrazit mřížku",
 	"vcmi.adventureOptions.showGrid.hover" : "Zobrazit mřížku",
 	"vcmi.adventureOptions.showGrid.help" : "{Zobrazit mřížku}\n\nZobrazit překrytí mřížkou, zvýrazňuje hranice mezi dlaždicemi mapy světa.",
 	"vcmi.adventureOptions.showGrid.help" : "{Zobrazit mřížku}\n\nZobrazit překrytí mřížkou, zvýrazňuje hranice mezi dlaždicemi mapy světa.",
 	"vcmi.adventureOptions.borderScroll.hover" : "Posouvání okraji",
 	"vcmi.adventureOptions.borderScroll.hover" : "Posouvání okraji",
 	"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.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.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.infoBarCreatureManagement.help" : "{Info Panel Creature Management}\n\nAllows rearranging creatures in info panel instead of cycling between default components.",
 	"vcmi.adventureOptions.leftButtonDrag.hover" : "Posouvání mapy levým kliknutím",
 	"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.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.smoothDragging.hover" : "Plynulé posouvání mapy",
+	"vcmi.adventureOptions.smoothDragging.help" : "{Plynulé posouvání mapy}\n\nWhen enabled, map dragging has a modern run out effect.", // TODO
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
@@ -141,16 +148,16 @@
 	"vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO",
 	"vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO",
 	"vcmi.battleOptions.queueSizeSmallButton.hover": "MALÁ",
 	"vcmi.battleOptions.queueSizeSmallButton.hover": "MALÁ",
 	"vcmi.battleOptions.queueSizeBigButton.hover": "VELKÁ",
 	"vcmi.battleOptions.queueSizeBigButton.hover": "VELKÁ",
-	"vcmi.battleOptions.queueSizeNoneButton.help": "Nezobrazovat frontu pořadí tahů",
+	"vcmi.battleOptions.queueSizeNoneButton.help": "Nezobrazovat frontu pořadí tahů.",
 	"vcmi.battleOptions.queueSizeAutoButton.help": "Nastavit automaticky velikost fronty pořadí tahů podle rozlišení obrazovky hry (Při výšce herního rozlišení menší než 700 pixelů je použita velikost MALÁ, jinak velikost VELKÁ)",
 	"vcmi.battleOptions.queueSizeAutoButton.help": "Nastavit automaticky velikost fronty pořadí tahů podle rozlišení obrazovky hry (Při výšce herního rozlišení menší než 700 pixelů je použita velikost MALÁ, jinak velikost VELKÁ)",
-	"vcmi.battleOptions.queueSizeSmallButton.help": "Zobrazit malou frontu pořadí tahů",
-	"vcmi.battleOptions.queueSizeBigButton.help": "Zobrazit velkou frontu pořadí tahů (není podporováno, pokud výška rozlišení hry není alespoň 700 pixelů)",
+	"vcmi.battleOptions.queueSizeSmallButton.help": "Zobrazit MALOU frontu pořadí tahů.",
+	"vcmi.battleOptions.queueSizeBigButton.help": "Zobrazit VELKOU frontu pořadí tahů (není podporováno, pokud výška rozlišení hry není alespoň 700 pixelů).",
 	"vcmi.battleOptions.animationsSpeed1.hover": "",
 	"vcmi.battleOptions.animationsSpeed1.hover": "",
 	"vcmi.battleOptions.animationsSpeed5.hover": "",
 	"vcmi.battleOptions.animationsSpeed5.hover": "",
 	"vcmi.battleOptions.animationsSpeed6.hover": "",
 	"vcmi.battleOptions.animationsSpeed6.hover": "",
-	"vcmi.battleOptions.animationsSpeed1.help": "Nastavit rychlost animací na velmi pomalé",
-	"vcmi.battleOptions.animationsSpeed5.help": "Nastavit rychlost animací na velmi rychlé",
-	"vcmi.battleOptions.animationsSpeed6.help": "Nastavit rychlost animací na okamžité",
+	"vcmi.battleOptions.animationsSpeed1.help": "Nastavit rychlost animací na velmi pomalé.",
+	"vcmi.battleOptions.animationsSpeed5.help": "Nastavit rychlost animací na velmi rychlé.",
+	"vcmi.battleOptions.animationsSpeed6.help": "Nastavit rychlost animací na okamžité.",
 	"vcmi.battleOptions.movementHighlightOnHover.hover": "Zvýraznění pohybu při najetí",
 	"vcmi.battleOptions.movementHighlightOnHover.hover": "Zvýraznění pohybu při najetí",
 	"vcmi.battleOptions.movementHighlightOnHover.help": "{Zvýraznění pohybu při najetí}\n\nZvýraznit rozsah pohybu jednotky při najetí na něj.",
 	"vcmi.battleOptions.movementHighlightOnHover.help": "{Zvýraznění pohybu při najetí}\n\nZvýraznit rozsah pohybu jednotky při najetí na něj.",
 	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Zobrazit omezení dostřelu střelců",
 	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Zobrazit omezení dostřelu střelců",
@@ -159,6 +166,7 @@
 	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Zobrazit okno statistik hrdinů}\n\nTrvale zapne okno statistiky hrdinů, které ukazuje hlavní schopnosti a magickou energii.",
 	"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.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.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.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.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.",
 
 
@@ -211,26 +219,26 @@
 	"vcmi.logicalExpressions.noneOf" : "Žádné z následujících:",
 	"vcmi.logicalExpressions.noneOf" : "Žádné z následujících:",
 
 
 	"vcmi.heroWindow.openCommander.hover" : "Open commander info window",
 	"vcmi.heroWindow.openCommander.hover" : "Open commander info window",
-	"vcmi.heroWindow.openCommander.help"  : "Shows details about the commander of this hero",
+	"vcmi.heroWindow.openCommander.help"  : "Shows details about the commander of this hero.",
 	"vcmi.heroWindow.openBackpack.hover" : "Open artifact backpack window",
 	"vcmi.heroWindow.openBackpack.hover" : "Open artifact backpack window",
-	"vcmi.heroWindow.openBackpack.help"  : "Opens window that allows easier artifact backpack management",
+	"vcmi.heroWindow.openBackpack.help"  : "Opens window that allows easier artifact backpack management.",
 
 
 	"vcmi.commanderWindow.artifactMessage" : "Chcete navrátit tento artefakt hrdinovi?",
 	"vcmi.commanderWindow.artifactMessage" : "Chcete navrátit tento artefakt hrdinovi?",
 
 
 	"vcmi.creatureWindow.showBonuses.hover"    : "Přepnout na zobrazení bonusů",
 	"vcmi.creatureWindow.showBonuses.hover"    : "Přepnout na zobrazení bonusů",
-	"vcmi.creatureWindow.showBonuses.help"     : "Display all active bonuses of the commander",
+	"vcmi.creatureWindow.showBonuses.help"     : "Display all active bonuses of the commander.",
 	"vcmi.creatureWindow.showSkills.hover"     : "Přepnout na zobrazení schoostí",
 	"vcmi.creatureWindow.showSkills.hover"     : "Přepnout na zobrazení schoostí",
-	"vcmi.creatureWindow.showSkills.help"      : "Display all learned skills of the commander",
+	"vcmi.creatureWindow.showSkills.help"      : "Display all learned skills of the commander.",
 	"vcmi.creatureWindow.returnArtifact.hover" : "Vrátit artefakt",
 	"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.creatureWindow.returnArtifact.help"  : "Klikněte na toto tlačítko pro navrácení artefaktů do hrdinova batohu.",
 
 
 	"vcmi.questLog.hideComplete.hover" : "Skrýt dokončené úkoly",
 	"vcmi.questLog.hideComplete.hover" : "Skrýt dokončené úkoly",
-	"vcmi.questLog.hideComplete.help"  : "Skrýt všechny dokončené úkoly",
+	"vcmi.questLog.hideComplete.help"  : "Skrýt všechny dokončené úkoly.",
 
 
-	"vcmi.randomMapTab.widgets.randomTemplate"      : "(Random)",
+	"vcmi.randomMapTab.widgets.randomTemplate"      : "(Náhodná)",
 	"vcmi.randomMapTab.widgets.templateLabel"        : "Šablona",
 	"vcmi.randomMapTab.widgets.templateLabel"        : "Šablona",
-	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Setup...",
-	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Team Alignments",
+	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Nastavit...",
+	"vcmi.randomMapTab.widgets.teamAlignmentsLabel"  : "Přiřazení týmů",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Druhy cest",
 	"vcmi.randomMapTab.widgets.roadTypesLabel"       : "Druhy cest",
 
 
 	"vcmi.optionsTab.turnOptions.hover" : "Možnosti tahu",
 	"vcmi.optionsTab.turnOptions.hover" : "Možnosti tahu",
@@ -251,37 +259,37 @@
 	"vcmi.optionsTab.accumulate" : "Accumulate",
 	"vcmi.optionsTab.accumulate" : "Accumulate",
 
 
 	"vcmi.optionsTab.simturnsTitle" : "Souběžné tahy",
 	"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",
+	"vcmi.optionsTab.simturnsMin.hover" : "Alespoň po",
+	"vcmi.optionsTab.simturnsMax.hover" : "Nejvíce po",
+	"vcmi.optionsTab.simturnsAI.hover" : "(Experimentální) Souběžné tahy AI",
+	"vcmi.optionsTab.simturnsMin.help" : "Hrát souběžně po určený počet dní. Setkání mezi hráči je v této době zablokováno",
+	"vcmi.optionsTab.simturnsMax.help" : "Hrát souběžně po určený počet dní nebo do setkání s jiným hráčem",
+	"vcmi.optionsTab.simturnsAI.help" : "{Souběžné tahy AI}\nExperimentální volba. Dovoluje AI hráčům hrát souběžně s lidskými hráči, když jsou souběžné tahy povoleny.",
+
+	"vcmi.optionsTab.turnTime.select"     : "Vyberte šablonu nastavení časovače",
+	"vcmi.optionsTab.turnTime.unlimited"  : "Neomezený čas tahu",
+	"vcmi.optionsTab.turnTime.classic.1"  : "Klasický časovač: 1 minuta",
+	"vcmi.optionsTab.turnTime.classic.2"  : "Klasický časovač: 2 minuty",
+	"vcmi.optionsTab.turnTime.classic.5"  : "Klasický časovač: 5 minut",
+	"vcmi.optionsTab.turnTime.classic.10" : "Klasický časovač: 10 minut",
+	"vcmi.optionsTab.turnTime.classic.20" : "Klasický časovač: 20 minut",
+	"vcmi.optionsTab.turnTime.classic.30" : "Klasický časovač: 30 minut",
+	"vcmi.optionsTab.turnTime.chess.20"   : "Šachová: 20:00 + 10:00 + 02:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.16"   : "Šachová: 16:00 + 08:00 + 01:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.8"    : "Šachová: 08:00 + 04:00 + 01:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.4"    : "Šachová: 04:00 + 02:00 + 00:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.2"    : "Šachová: 02:00 + 01:00 + 00:15 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.1"    : "Šachová: 01:00 + 01:00 + 00:00 + 00:00",
+
+	"vcmi.optionsTab.simturns.select"         : "Vyberte šablonu souběžných tahů",
+	"vcmi.optionsTab.simturns.none"           : "Bez souběžných tahů",
+	"vcmi.optionsTab.simturns.tillContactMax" : "Souběžně: Do setkání",
+	"vcmi.optionsTab.simturns.tillContact1"   : "Souběžně: 1 týden, přerušit při setkání",
+	"vcmi.optionsTab.simturns.tillContact2"   : "Souběžně: 2 týdny, přerušit při setkání",
+	"vcmi.optionsTab.simturns.tillContact4"   : "Souběžně: 1 mšsíc, přerušit při setkání",
+	"vcmi.optionsTab.simturns.blocked1"       : "Souběžně: 1 týden, setkání zablokována",
+	"vcmi.optionsTab.simturns.blocked2"       : "Souběžně: 2 týdny, setkání zablokována",
+	"vcmi.optionsTab.simturns.blocked4"       : "Souběžně: 1 měsíc, setkání zablokována",
 	
 	
 	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
 	// 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
 	// Using this information, VCMI will automatically select correct plural form for every possible amount
@@ -305,7 +313,7 @@
 	"vcmi.map.victoryCondition.angelicAlliance.message" : "Porazte všechny nepřátele a utužte 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
 	// 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", //TODO
+	"vcmi.stackExperience.description" : "» P o d r o b n o s t i   z k u š e n o s t í   o d d í l u «\n\nDruh bojovníka ................... : %s\nÚroveň hodnosti ................. : %s (%i)\nBody zkušeností ............... : %i\nZkušenostních bodů do další úrovně hodnosti .. : %i\nMaximum zkušeností na bitvu ... : %i%% (%i)\nPočet bojovníků v oddílu .... : %i\nMaximum nových rekrutů\n bez ztráty současné hodnosti .... : %i\nNásobič zkušeností ........... : %.2f\nNásobič vylepšení .............. : %.2f\nZkušnosti po 10. úrovně hodnosti ........ : %i\nMaximální počet nových rekrutů pro zachování\n 10. úrovně hodnosti s maximálními zkušenostmi: %i",
 	"vcmi.stackExperience.rank.0" : "Začátečník",
 	"vcmi.stackExperience.rank.0" : "Začátečník",
 	"vcmi.stackExperience.rank.1" : "Učeň",
 	"vcmi.stackExperience.rank.1" : "Učeň",
 	"vcmi.stackExperience.rank.2" : "Trénovaný",
 	"vcmi.stackExperience.rank.2" : "Trénovaný",
@@ -315,7 +323,7 @@
 	"vcmi.stackExperience.rank.6" : "Adept",
 	"vcmi.stackExperience.rank.6" : "Adept",
 	"vcmi.stackExperience.rank.7" : "Expert",
 	"vcmi.stackExperience.rank.7" : "Expert",
 	"vcmi.stackExperience.rank.8" : "Elitní",
 	"vcmi.stackExperience.rank.8" : "Elitní",
-	"vcmi.stackExperience.rank.9" : "Master",
+	"vcmi.stackExperience.rank.9" : "Mistr",
 	"vcmi.stackExperience.rank.10" : "Eso",
 	"vcmi.stackExperience.rank.10" : "Eso",
 	
 	
 	"core.bonus.ADDITIONAL_ATTACK.name": "Dvojitý úder",
 	"core.bonus.ADDITIONAL_ATTACK.name": "Dvojitý úder",
@@ -392,7 +400,7 @@
 	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Nevystřelí na jednotky dále než ${val} polí",
 	"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.name": "Vysátí životů (${val}%)",
 	"core.bonus.LIFE_DRAIN.description": "Vysaje ${val}% uděleného poškození",
 	"core.bonus.LIFE_DRAIN.description": "Vysaje ${val}% uděleného poškození",
-	"core.bonus.MANA_CHANNELING.name": "Magic Channel ${val}%", // TODO
+	"core.bonus.MANA_CHANNELING.name": "${val}% kouzelný kanál",
 	"core.bonus.MANA_CHANNELING.description": "Dá vašemu hrdinovi ${val} % many využité nepřítelem",
 	"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.name": "Vysátí many",
 	"core.bonus.MANA_DRAIN.description": "Každé kolo vysaje ${val} many",
 	"core.bonus.MANA_DRAIN.description": "Každé kolo vysaje ${val} many",

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

@@ -136,6 +136,8 @@
 	"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.help" : "{Left Click Drag Map}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view.",
 	"vcmi.adventureOptions.smoothDragging.hover" : "Smooth Map Dragging",
 	"vcmi.adventureOptions.smoothDragging.hover" : "Smooth Map Dragging",
 	"vcmi.adventureOptions.smoothDragging.help" : "{Smooth Map Dragging}\n\nWhen enabled, map dragging has a modern run out effect.",
 	"vcmi.adventureOptions.smoothDragging.help" : "{Smooth Map Dragging}\n\nWhen enabled, map dragging has a modern run out effect.",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Skip fading effects",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Skip fading effects}\n\nWhen enabled, Skips object fadeout and similar effects (resource collection, ship embark etc). Makes UI more reactive in some cases at the expense of aesthetics. Especially useful in PvP games. For maximum movement speed skipping is active regardless of this setting.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",

+ 4 - 0
Mods/vcmi/config/vcmi/polish.json

@@ -114,6 +114,8 @@
 	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Ulepszenia interfejsu}\n\nWłącza różne ulepszenia interfejsu poprawiające wygodę rozgrywki. Takie jak przycisk sakwy bohatera itp. Wyłącz jeśli szukasz bardziej klasycznej wersji gry.",
 	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Ulepszenia interfejsu}\n\nWłącza różne ulepszenia interfejsu poprawiające wygodę rozgrywki. Takie jak przycisk sakwy bohatera itp. Wyłącz jeśli szukasz bardziej klasycznej wersji gry.",
 	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "Duża księga zaklęć",
 	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "Duża księga zaklęć",
 	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Duża księga zaklęć}\n\nWłącza dużą księgę czarów, która mieści więcej zaklęć na stronę. Animacja zmiany strony nie działa gdy ta opcja jest włączona.",
 	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Duża księga zaklęć}\n\nWłącza dużą księgę czarów, która mieści więcej zaklęć na stronę. Animacja zmiany strony nie działa gdy ta opcja jest włączona.",
+	"vcmi.systemOptions.audioMuteFocus.hover"  : "Wycisz przy zdezaktywowaniu",
+	"vcmi.systemOptions.audioMuteFocus.help"   : "{Wycisz przy zdezaktywowaniu}\n\nWycisz dźwięk gdy okno gry staje się nieaktywne. Wyjątkiem są dźwięki wiadomości i nowej tury.",
 
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym",
 	"vcmi.adventureOptions.infoBarPick.hover" : "Pokaż komunikaty w panelu informacyjnym",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Pokaż komunikaty w panelu informacyjnym}\n\nGdy to możliwe, wiadomości z odwiedzania obiektów będą pokazywane w panelu informacyjnym zamiast w osobnym okienku.",
@@ -129,6 +131,8 @@
 	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Zarządzanie armią w panelu informacyjnym}\n\nPozwala zarządzać jednostkami w panelu informacyjnym, zamiast przełączać między domyślnymi informacjami.",
 	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Zarządzanie armią w panelu informacyjnym}\n\nPozwala zarządzać jednostkami w panelu informacyjnym, zamiast przełączać między domyślnymi informacjami.",
 	"vcmi.adventureOptions.leftButtonDrag.hover" : "Przeciąganie mapy lewym kliknięciem",
 	"vcmi.adventureOptions.leftButtonDrag.hover" : "Przeciąganie mapy lewym kliknięciem",
 	"vcmi.adventureOptions.leftButtonDrag.help" : "{Przeciąganie mapy lewym kliknięciem}\n\nGdy włączone, umożliwia przesuwanie mapy przygody poprzez przeciąganie myszy z wciśniętym lewym przyciskiem.",
 	"vcmi.adventureOptions.leftButtonDrag.help" : "{Przeciąganie mapy lewym kliknięciem}\n\nGdy włączone, umożliwia przesuwanie mapy przygody poprzez przeciąganie myszy z wciśniętym lewym przyciskiem.",
+	"vcmi.adventureOptions.smoothDragging.hover" : "'Pływające' przeciąganie mapy",
+	"vcmi.adventureOptions.smoothDragging.help" : "{'Pływające' przeciąganie mapy}\n\nGdy włączone, przeciąganie mapy następuje ze stopniowo zanikającym przyspieszeniem.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",

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

@@ -54,6 +54,8 @@
 	"vcmi.radialWheel.moveDown" : "Перемістити вниз",
 	"vcmi.radialWheel.moveDown" : "Перемістити вниз",
 	"vcmi.radialWheel.moveBottom" : "Перемістити у кінець",
 	"vcmi.radialWheel.moveBottom" : "Перемістити у кінець",
 
 
+	"vcmi.spellBook.search" : "шукати...",
+
 	"vcmi.mainMenu.serverConnecting" : "Підключення...",
 	"vcmi.mainMenu.serverConnecting" : "Підключення...",
 	"vcmi.mainMenu.serverAddressEnter" : "Вкажіть адресу:",
 	"vcmi.mainMenu.serverAddressEnter" : "Вкажіть адресу:",
 	"vcmi.mainMenu.serverConnectionFailed" : "Помилка з'єднання",
 	"vcmi.mainMenu.serverConnectionFailed" : "Помилка з'єднання",
@@ -134,6 +136,8 @@
 	"vcmi.adventureOptions.leftButtonDrag.help" : "{Переміщення мапи лівою кнопкою}\n\nЯкщо увімкнено, переміщення миші з натиснутою лівою кнопкою буде перетягувати мапу пригод",
 	"vcmi.adventureOptions.leftButtonDrag.help" : "{Переміщення мапи лівою кнопкою}\n\nЯкщо увімкнено, переміщення миші з натиснутою лівою кнопкою буде перетягувати мапу пригод",
 	"vcmi.adventureOptions.smoothDragging.hover" : "Плавне перетягування мапи",
 	"vcmi.adventureOptions.smoothDragging.hover" : "Плавне перетягування мапи",
 	"vcmi.adventureOptions.smoothDragging.help" : "{Плавне перетягування мапи}\n\nЯкщо увімкнено, перетягування мапи має сучасний ефект завершення.",
 	"vcmi.adventureOptions.smoothDragging.help" : "{Плавне перетягування мапи}\n\nЯкщо увімкнено, перетягування мапи має сучасний ефект завершення.",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Вимкнути ефекти зникнення",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Вимкнути ефекти зникнення}\n\nЯкщо увімкнено, пропускає зникання об'єктів та подібні ефекти (збирання ресурсів, посадка на корабель тощо). У деяких випадках робить інтерфейс більш реактивним за рахунок естетики. Особливо корисно в PvP-іграх. При максимальній швидкості пересування цей параметр увімкнено завжди, незалежно від цього параметра.",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",

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

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

+ 4 - 0
client/NetPacksLobbyClient.cpp

@@ -15,6 +15,7 @@
 
 
 #include "lobby/OptionsTab.h"
 #include "lobby/OptionsTab.h"
 #include "lobby/RandomMapTab.h"
 #include "lobby/RandomMapTab.h"
+#include "lobby/TurnOptionsTab.h"
 #include "lobby/SelectionTab.h"
 #include "lobby/SelectionTab.h"
 #include "lobby/CBonusSelection.h"
 #include "lobby/CBonusSelection.h"
 
 
@@ -95,6 +96,9 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyGuiAction(LobbyGuiAction & pack
 	case LobbyGuiAction::OPEN_RANDOM_MAP_OPTIONS:
 	case LobbyGuiAction::OPEN_RANDOM_MAP_OPTIONS:
 		lobby->toggleTab(lobby->tabRand);
 		lobby->toggleTab(lobby->tabRand);
 		break;
 		break;
+	case LobbyGuiAction::OPEN_TURN_OPTIONS:
+		lobby->toggleTab(lobby->tabTurnOptions);
+		break;
 	}
 	}
 }
 }
 
 

+ 8 - 6
client/PlayerLocalState.cpp

@@ -204,7 +204,7 @@ const CGHeroInstance * PlayerLocalState::getWanderingHero(size_t index)
 {
 {
 	if(index < wanderingHeroes.size())
 	if(index < wanderingHeroes.size())
 		return wanderingHeroes[index];
 		return wanderingHeroes[index];
-	return nullptr;
+	throw std::runtime_error("No hero with index " + std::to_string(index));
 }
 }
 
 
 void PlayerLocalState::addWanderingHero(const CGHeroInstance * hero)
 void PlayerLocalState::addWanderingHero(const CGHeroInstance * hero)
@@ -235,10 +235,12 @@ void PlayerLocalState::removeWanderingHero(const CGHeroInstance * hero)
 		setSelection(ownedTowns.front());
 		setSelection(ownedTowns.front());
 }
 }
 
 
-void PlayerLocalState::swapWanderingHero(int pos1, int pos2)
+void PlayerLocalState::swapWanderingHero(size_t pos1, size_t pos2)
 {
 {
 	assert(wanderingHeroes[pos1] && wanderingHeroes[pos2]);
 	assert(wanderingHeroes[pos1] && wanderingHeroes[pos2]);
-	std::swap(wanderingHeroes[pos1], wanderingHeroes[pos2]);
+	std::swap(wanderingHeroes.at(pos1), wanderingHeroes.at(pos2));
+
+	adventureInt->onHeroOrderChanged();
 }
 }
 
 
 const std::vector<const CGTownInstance *> & PlayerLocalState::getOwnedTowns()
 const std::vector<const CGTownInstance *> & PlayerLocalState::getOwnedTowns()
@@ -250,7 +252,7 @@ const CGTownInstance * PlayerLocalState::getOwnedTown(size_t index)
 {
 {
 	if(index < ownedTowns.size())
 	if(index < ownedTowns.size())
 		return ownedTowns[index];
 		return ownedTowns[index];
-	return nullptr;
+	throw std::runtime_error("No town with index " + std::to_string(index));
 }
 }
 
 
 void PlayerLocalState::addOwnedTown(const CGTownInstance * town)
 void PlayerLocalState::addOwnedTown(const CGTownInstance * town)
@@ -276,10 +278,10 @@ void PlayerLocalState::removeOwnedTown(const CGTownInstance * town)
 		setSelection(ownedTowns.front());
 		setSelection(ownedTowns.front());
 }
 }
 
 
-void PlayerLocalState::swapOwnedTowns(int pos1, int pos2)
+void PlayerLocalState::swapOwnedTowns(size_t pos1, size_t pos2)
 {
 {
 	assert(ownedTowns[pos1] && ownedTowns[pos2]);
 	assert(ownedTowns[pos1] && ownedTowns[pos2]);
-	std::swap(ownedTowns[pos1], ownedTowns[pos2]);
+	std::swap(ownedTowns.at(pos1), ownedTowns.at(pos2));
 
 
 	adventureInt->onTownOrderChanged();
 	adventureInt->onTownOrderChanged();
 }
 }

+ 2 - 2
client/PlayerLocalState.h

@@ -66,14 +66,14 @@ public:
 	const CGTownInstance * getOwnedTown(size_t index);
 	const CGTownInstance * getOwnedTown(size_t index);
 	void addOwnedTown(const CGTownInstance * hero);
 	void addOwnedTown(const CGTownInstance * hero);
 	void removeOwnedTown(const CGTownInstance * hero);
 	void removeOwnedTown(const CGTownInstance * hero);
-	void swapOwnedTowns(int pos1, int pos2);
+	void swapOwnedTowns(size_t pos1, size_t pos2);
 
 
 	const std::vector<const CGHeroInstance *> & getWanderingHeroes();
 	const std::vector<const CGHeroInstance *> & getWanderingHeroes();
 	const CGHeroInstance * getWanderingHero(size_t index);
 	const CGHeroInstance * getWanderingHero(size_t index);
 	const CGHeroInstance * getNextWanderingHero(const CGHeroInstance * hero);
 	const CGHeroInstance * getNextWanderingHero(const CGHeroInstance * hero);
 	void addWanderingHero(const CGHeroInstance * hero);
 	void addWanderingHero(const CGHeroInstance * hero);
 	void removeWanderingHero(const CGHeroInstance * hero);
 	void removeWanderingHero(const CGHeroInstance * hero);
-	void swapWanderingHero(int pos1, int pos2);
+	void swapWanderingHero(size_t pos1, size_t pos2);
 
 
 	void setPath(const CGHeroInstance * h, const CGPath & path);
 	void setPath(const CGHeroInstance * h, const CGPath & path);
 	bool setPath(const CGHeroInstance * h, const int3 & destination);
 	bool setPath(const CGHeroInstance * h, const int3 & destination);

+ 7 - 2
client/adventureMap/AdventureMapInterface.cpp

@@ -65,8 +65,8 @@ AdventureMapInterface::AdventureMapInterface():
 	shortcuts->setState(EAdventureState::MAKING_TURN);
 	shortcuts->setState(EAdventureState::MAKING_TURN);
 	widget->getMapView()->onViewMapActivated();
 	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);
 	addUsedEvents(KEYBOARD | TIME);
 }
 }
@@ -331,6 +331,11 @@ void AdventureMapInterface::onTownOrderChanged()
 	widget->getTownList()->updateWidget();
 	widget->getTownList()->updateWidget();
 }
 }
 
 
+void AdventureMapInterface::onHeroOrderChanged()
+{
+	widget->getHeroList()->updateWidget();
+}
+
 void AdventureMapInterface::onMapTilesChanged(boost::optional<std::unordered_set<int3>> positions)
 void AdventureMapInterface::onMapTilesChanged(boost::optional<std::unordered_set<int3>> positions)
 {
 {
 	if (positions)
 	if (positions)

+ 3 - 0
client/adventureMap/AdventureMapInterface.h

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

+ 27 - 21
client/adventureMap/CList.cpp

@@ -279,14 +279,14 @@ void CHeroList::CHeroItem::gesture(bool on, const Point & initialPosition, const
 	if(heroes.size() < 2)
 	if(heroes.size() < 2)
 		return;
 		return;
 
 
-	int heroPos = vstd::find_pos(heroes, hero);
-	const CGHeroInstance * heroUpper = (heroPos < 1) ? nullptr : heroes[heroPos - 1];
-	const CGHeroInstance * heroLower = (heroPos > heroes.size() - 2) ? nullptr : heroes[heroPos + 1];
+	size_t heroPos = vstd::find_pos(heroes, hero);
+	const CGHeroInstance * heroUpper = (heroPos < 1) ? nullptr : heroes.at(heroPos - 1);
+	const CGHeroInstance * heroLower = (heroPos > heroes.size() - 2) ? nullptr : heroes.at(heroPos + 1);
 
 
 	std::vector<RadialMenuConfig> menuElements = {
 	std::vector<RadialMenuConfig> menuElements = {
 		{ RadialMenuConfig::ITEM_ALT_NN, heroUpper != nullptr, "altUpTop", "vcmi.radialWheel.moveTop", [heroPos]()
 		{ RadialMenuConfig::ITEM_ALT_NN, heroUpper != nullptr, "altUpTop", "vcmi.radialWheel.moveTop", [heroPos]()
 		{
 		{
-			for (int i = heroPos; i > 0; i--)
+			for (size_t i = heroPos; i > 0; i--)
 				LOCPLINT->localState->swapWanderingHero(i, i - 1);
 				LOCPLINT->localState->swapWanderingHero(i, i - 1);
 		} },
 		} },
 		{ RadialMenuConfig::ITEM_ALT_NW, heroUpper != nullptr, "altUp", "vcmi.radialWheel.moveUp", [heroPos](){LOCPLINT->localState->swapWanderingHero(heroPos, heroPos - 1); } },
 		{ RadialMenuConfig::ITEM_ALT_NW, heroUpper != nullptr, "altUp", "vcmi.radialWheel.moveUp", [heroPos](){LOCPLINT->localState->swapWanderingHero(heroPos, heroPos - 1); } },
@@ -326,7 +326,7 @@ void CHeroList::updateElement(const CGHeroInstance * hero)
 
 
 void CHeroList::updateWidget()
 void CHeroList::updateWidget()
 {
 {
-	auto & heroes = LOCPLINT->localState->getWanderingHeroes();
+	const auto & heroes = LOCPLINT->localState->getWanderingHeroes();
 
 
 	listBox->resize(heroes.size());
 	listBox->resize(heroes.size());
 
 
@@ -337,7 +337,7 @@ void CHeroList::updateWidget()
 		if (!item)
 		if (!item)
 			continue;
 			continue;
 
 
-		if (item->hero == heroes[i])
+		if (item->hero == heroes.at(i))
 		{
 		{
 			item->update();
 			item->update();
 		}
 		}
@@ -362,11 +362,9 @@ std::shared_ptr<CIntObject> CTownList::createItem(size_t index)
 }
 }
 
 
 CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town):
 CTownList::CTownItem::CTownItem(CTownList *parent, const CGTownInstance *Town):
-	CListItem(parent)
+	CListItem(parent),
+	town(Town)
 {
 {
-	const std::vector<const CGTownInstance *> towns = LOCPLINT->localState->getOwnedTowns();
-	townIndex = std::distance(towns.begin(), std::find(towns.begin(), towns.end(), Town));
-
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 	picture = std::make_shared<CAnimImage>(AnimationPath::builtin("ITPA"), 0);
 	picture = std::make_shared<CAnimImage>(AnimationPath::builtin("ITPA"), 0);
 	pos = picture->pos;
 	pos = picture->pos;
@@ -382,7 +380,6 @@ std::shared_ptr<CIntObject> CTownList::CTownItem::genSelection()
 
 
 void CTownList::CTownItem::update()
 void CTownList::CTownItem::update()
 {
 {
-	const CGTownInstance * town = LOCPLINT->localState->getOwnedTowns()[townIndex];
 	size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->builded >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)];
 	size_t iconIndex = town->town->clientInfo.icons[town->hasFort()][town->builded >= CGI->settings()->getInteger(EGameSettings::TOWNS_BUILDINGS_PER_TURN_CAP)];
 
 
 	picture->setFrame(iconIndex + 2);
 	picture->setFrame(iconIndex + 2);
@@ -392,17 +389,17 @@ void CTownList::CTownItem::update()
 void CTownList::CTownItem::select(bool on)
 void CTownList::CTownItem::select(bool on)
 {
 {
 	if(on)
 	if(on)
-		LOCPLINT->localState->setSelection(LOCPLINT->localState->getOwnedTowns()[townIndex]);
+		LOCPLINT->localState->setSelection(town);
 }
 }
 
 
 void CTownList::CTownItem::open()
 void CTownList::CTownItem::open()
 {
 {
-	LOCPLINT->openTownWindow(LOCPLINT->localState->getOwnedTowns()[townIndex]);
+	LOCPLINT->openTownWindow(town);
 }
 }
 
 
 void CTownList::CTownItem::showTooltip()
 void CTownList::CTownItem::showTooltip()
 {
 {
-	CRClickPopup::createAndPush(LOCPLINT->localState->getOwnedTowns()[townIndex], GH.getCursorPosition());
+	CRClickPopup::createAndPush(town, GH.getCursorPosition());
 }
 }
 
 
 void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition)
 void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const Point & finalPosition)
@@ -411,8 +408,9 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const
 		return;
 		return;
 
 
 	const std::vector<const CGTownInstance *> towns = LOCPLINT->localState->getOwnedTowns();
 	const std::vector<const CGTownInstance *> towns = LOCPLINT->localState->getOwnedTowns();
+	size_t townIndex = vstd::find_pos(towns, town);
 
 
-	if(townIndex < 0 || townIndex > towns.size() - 1 || !towns[townIndex])
+	if(townIndex + 1 > towns.size() || !towns.at(townIndex))
 		return;
 		return;
 
 
 	if(towns.size() < 2)
 	if(towns.size() < 2)
@@ -422,14 +420,14 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const
 	int townLowerPos = (townIndex > towns.size() - 2) ? -1 : townIndex + 1;
 	int townLowerPos = (townIndex > towns.size() - 2) ? -1 : townIndex + 1;
 
 
 	std::vector<RadialMenuConfig> menuElements = {
 	std::vector<RadialMenuConfig> menuElements = {
-		{ RadialMenuConfig::ITEM_ALT_NN, townUpperPos > -1, "altUpTop", "vcmi.radialWheel.moveTop", [this]()
+		{ RadialMenuConfig::ITEM_ALT_NN, townUpperPos > -1, "altUpTop", "vcmi.radialWheel.moveTop", [townIndex]()
 		{
 		{
 			for (int i = townIndex; i > 0; i--)
 			for (int i = townIndex; i > 0; i--)
 				LOCPLINT->localState->swapOwnedTowns(i, i - 1);
 				LOCPLINT->localState->swapOwnedTowns(i, i - 1);
 		} },
 		} },
-		{ 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]()
+		{ RadialMenuConfig::ITEM_ALT_NW, townUpperPos > -1, "altUp", "vcmi.radialWheel.moveUp", [townIndex, townUpperPos](){LOCPLINT->localState->swapOwnedTowns(townIndex, townUpperPos); } },
+		{ RadialMenuConfig::ITEM_ALT_SW, townLowerPos > -1, "altDown", "vcmi.radialWheel.moveDown", [townIndex, townLowerPos](){ LOCPLINT->localState->swapOwnedTowns(townIndex, townLowerPos); } },
+		{ RadialMenuConfig::ITEM_ALT_SS, townLowerPos > -1, "altDownBottom", "vcmi.radialWheel.moveBottom", [townIndex, towns]()
 		{
 		{
 			for (int i = townIndex; i < towns.size() - 1; i++)
 			for (int i = townIndex; i < towns.size() - 1; i++)
 				LOCPLINT->localState->swapOwnedTowns(i, i + 1);
 				LOCPLINT->localState->swapOwnedTowns(i, i + 1);
@@ -441,7 +439,7 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const
 
 
 std::string CTownList::CTownItem::getHoverText()
 std::string CTownList::CTownItem::getHoverText()
 {
 {
-	return LOCPLINT->localState->getOwnedTowns()[townIndex]->getObjectName();
+	return town->getObjectName();
 }
 }
 
 
 CTownList::CTownList(int visibleItemsCount, Rect widgetPosition, Point firstItemOffset, Point itemOffsetDelta, size_t initialItemsCount)
 CTownList::CTownList(int visibleItemsCount, Rect widgetPosition, Point firstItemOffset, Point itemOffsetDelta, size_t initialItemsCount)
@@ -473,7 +471,15 @@ void CTownList::updateWidget()
 		if (!item)
 		if (!item)
 			continue;
 			continue;
 
 
-		listBox->reset();
+		if (item->town == towns[i])
+		{
+			item->update();
+		}
+		else
+		{
+			listBox->reset();
+			break;
+		}
 	}
 	}
 
 
 	if (LOCPLINT->localState->getCurrentTown())
 	if (LOCPLINT->localState->getCurrentTown())

+ 1 - 1
client/adventureMap/CList.h

@@ -152,7 +152,7 @@ class CTownList	: public CList
 	{
 	{
 		std::shared_ptr<CAnimImage> picture;
 		std::shared_ptr<CAnimImage> picture;
 	public:
 	public:
-		int townIndex;
+		const CGTownInstance * const town;
 
 
 		CTownItem(CTownList *parent, const CGTownInstance * town);
 		CTownItem(CTownList *parent, const CGTownInstance * town);
 
 

+ 140 - 111
client/adventureMap/TurnTimerWidget.cpp

@@ -15,54 +15,86 @@
 #include "../CPlayerInterface.h"
 #include "../CPlayerInterface.h"
 #include "../battle/BattleInterface.h"
 #include "../battle/BattleInterface.h"
 #include "../battle/BattleStacksController.h"
 #include "../battle/BattleStacksController.h"
-
-#include "../render/EFont.h"
-#include "../render/Graphics.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/CGuiHandler.h"
-#include "../gui/TextAlignment.h"
+#include "../render/Graphics.h"
 #include "../widgets/Images.h"
 #include "../widgets/Images.h"
+#include "../widgets/MiscWidgets.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/TextControls.h"
+
 #include "../../CCallback.h"
 #include "../../CCallback.h"
-#include "../../lib/CStack.h"
 #include "../../lib/CPlayerState.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;
 	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)
 void TurnTimerWidget::show(Canvas & to)
@@ -70,98 +102,95 @@ void TurnTimerWidget::show(Canvas & to)
 	showAll(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
 		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
 #pragma once
 
 
 #include "../gui/CIntObject.h"
 #include "../gui/CIntObject.h"
-#include "../gui/InterfaceObjectConfigurable.h"
-#include "../render/Canvas.h"
 #include "../render/Colors.h"
 #include "../render/Colors.h"
+#include "../../lib/TurnTimerInfo.h"
 
 
 class CAnimImage;
 class CAnimImage;
 class CLabel;
 class CLabel;
+class CFilledTexture;
+class TransparentFilledRectangle;
 
 
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
-
 class PlayerColor;
 class PlayerColor;
-
 VCMI_LIB_NAMESPACE_END
 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 show(Canvas & to) override;
 	void tick(uint32_t msPassed) 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);
 };
 };

+ 3 - 3
client/battle/BattleActionsController.cpp

@@ -616,8 +616,8 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
 		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
 			if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures
 			if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures
 			{
 			{
-				int spellID = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), targetStack, CBattleInfoCallback::RANDOM_GENIE);
-				return spellID > -1;
+				SpellID spellID = owner.getBattle()->getRandomBeneficialSpell(CRandomGenerator::getDefault(), owner.stacksController->getActiveStack(), targetStack);
+				return spellID != SpellID::NONE;
 			}
 			}
 			return false;
 			return false;
 
 
@@ -887,7 +887,7 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS
 	{
 	{
 		// faerie dragon can cast only one, randomly selected spell until their next move
 		// faerie dragon can cast only one, randomly selected spell until their next move
 		//TODO: faerie dragon type spell should be selected by server
 		//TODO: faerie dragon type spell should be selected by server
-		const auto * spellToCast = owner.getBattle()->battleGetRandomStackSpell(CRandomGenerator::getDefault(), casterStack, CBattleInfoCallback::RANDOM_AIMED).toSpell();
+		const auto * spellToCast = owner.getBattle()->getRandomCastedSpell(CRandomGenerator::getDefault(), casterStack).toSpell();
 
 
 		if (spellToCast)
 		if (spellToCast)
 			creatureSpells.push_back(spellToCast);
 			creatureSpells.push_back(spellToCast);

+ 2 - 0
client/battle/BattleInterface.cpp

@@ -58,6 +58,7 @@ BattleInterface::BattleInterface(const BattleID & battleID, const CCreatureSet *
 	, curInt(att)
 	, curInt(att)
 	, battleID(battleID)
 	, battleID(battleID)
 	, battleOpeningDelayActive(true)
 	, battleOpeningDelayActive(true)
+	, round(0)
 {
 {
 	if(spectatorInt)
 	if(spectatorInt)
 	{
 	{
@@ -235,6 +236,7 @@ void BattleInterface::newRoundFirst()
 void BattleInterface::newRound()
 void BattleInterface::newRound()
 {
 {
 	console->addText(CGI->generaltexth->allTexts[412]);
 	console->addText(CGI->generaltexth->allTexts[412]);
+	round++;
 }
 }
 
 
 void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID spell)
 void BattleInterface::giveCommand(EActionType action, BattleHex tile, SpellID spell)

+ 1 - 0
client/battle/BattleInterface.h

@@ -136,6 +136,7 @@ public:
 	const CGHeroInstance *defendingHeroInstance;
 	const CGHeroInstance *defendingHeroInstance;
 
 
 	bool tacticsMode;
 	bool tacticsMode;
+	ui32 round;
 
 
 	std::unique_ptr<BattleProjectileController> projectilesController;
 	std::unique_ptr<BattleProjectileController> projectilesController;
 	std::unique_ptr<BattleSiegeController> siegeController;
 	std::unique_ptr<BattleSiegeController> siegeController;

+ 22 - 2
client/battle/BattleInterfaceClasses.cpp

@@ -34,6 +34,7 @@
 #include "../widgets/Buttons.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Images.h"
 #include "../widgets/Images.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/TextControls.h"
+#include "../widgets/MiscWidgets.h"
 #include "../windows/CMessage.h"
 #include "../windows/CMessage.h"
 #include "../windows/CSpellWindow.h"
 #include "../windows/CSpellWindow.h"
 #include "../render/CAnimation.h"
 #include "../render/CAnimation.h"
@@ -787,11 +788,16 @@ void StackQueue::update()
 	owner.getBattle()->battleGetTurnOrder(queueData, stackBoxes.size(), 0);
 	owner.getBattle()->battleGetTurnOrder(queueData, stackBoxes.size(), 0);
 
 
 	size_t boxIndex = 0;
 	size_t boxIndex = 0;
+	ui32 tmpTurn = -1;
 
 
 	for(size_t turn = 0; turn < queueData.size() && boxIndex < stackBoxes.size(); turn++)
 	for(size_t turn = 0; turn < queueData.size() && boxIndex < stackBoxes.size(); turn++)
 	{
 	{
 		for(size_t unitIndex = 0; unitIndex < queueData[turn].size() && boxIndex < stackBoxes.size(); boxIndex++, unitIndex++)
 		for(size_t unitIndex = 0; unitIndex < queueData[turn].size() && boxIndex < stackBoxes.size(); boxIndex++, unitIndex++)
-			stackBoxes[boxIndex]->setUnit(queueData[turn][unitIndex], turn);
+		{
+			ui32 currentTurn = owner.round + turn;
+			stackBoxes[boxIndex]->setUnit(queueData[turn][unitIndex], turn, tmpTurn != currentTurn && owner.round != 0 && (!embedded || tmpTurn != -1) ? (std::optional<ui32>)currentTurn : std::nullopt);
+			tmpTurn = currentTurn;
+		}
 	}
 	}
 
 
 	for(; boxIndex < stackBoxes.size(); boxIndex++)
 	for(; boxIndex < stackBoxes.size(); boxIndex++)
@@ -829,11 +835,14 @@ StackQueue::StackBox::StackBox(StackQueue * owner):
 	{
 	{
 		icon = std::make_shared<CAnimImage>(owner->icons, 0, 0, 5, 2);
 		icon = std::make_shared<CAnimImage>(owner->icons, 0, 0, 5, 2);
 		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 7, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
 		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 7, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+		roundRect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, 2, 48), ColorRGBA(0, 0, 0, 255), ColorRGBA(0, 255, 0, 255));
 	}
 	}
 	else
 	else
 	{
 	{
 		icon = std::make_shared<CAnimImage>(owner->icons, 0, 0, 9, 1);
 		icon = std::make_shared<CAnimImage>(owner->icons, 0, 0, 9, 1);
 		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
 		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
+		roundRect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, 15, 18), ColorRGBA(0, 0, 0, 255), ColorRGBA(241, 216, 120, 255));
+		round = std::make_shared<CLabel>(4, 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE);
 
 
 		int icon_x = pos.w - 17;
 		int icon_x = pos.w - 17;
 		int icon_y = pos.h - 18;
 		int icon_y = pos.h - 18;
@@ -841,9 +850,10 @@ StackQueue::StackBox::StackBox(StackQueue * owner):
 		stateIcon = std::make_shared<CAnimImage>(owner->stateIcons, 0, 0, icon_x, icon_y);
 		stateIcon = std::make_shared<CAnimImage>(owner->stateIcons, 0, 0, icon_x, icon_y);
 		stateIcon->visible = false;
 		stateIcon->visible = false;
 	}
 	}
+	roundRect->disable();
 }
 }
 
 
-void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn)
+void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std::optional<ui32> currentTurn)
 {
 {
 	if(unit)
 	if(unit)
 	{
 	{
@@ -861,6 +871,16 @@ void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn)
 			icon->setFrame(owner->getSiegeShooterIconID(), 1);
 			icon->setFrame(owner->getSiegeShooterIconID(), 1);
 
 
 		amount->setText(TextOperations::formatMetric(unit->getCount(), 4));
 		amount->setText(TextOperations::formatMetric(unit->getCount(), 4));
+		if(currentTurn && !owner->embedded)
+		{
+			std::string tmp = std::to_string(*currentTurn);
+			int len = graphics->fonts[FONT_SMALL]->getStringWidth(tmp);
+			roundRect->pos.w = len + 6;
+			round->setText(tmp);
+		}
+		roundRect->setEnabled(currentTurn.has_value());
+		if(!owner->embedded)
+			round->setEnabled(currentTurn.has_value());
 
 
 		if(stateIcon)
 		if(stateIcon)
 		{
 		{

+ 4 - 2
client/battle/BattleInterfaceClasses.h

@@ -39,6 +39,7 @@ class CToggleButton;
 class CLabel;
 class CLabel;
 class CTextBox;
 class CTextBox;
 class CAnimImage;
 class CAnimImage;
+class TransparentFilledRectangle;
 class CPlayerInterface;
 class CPlayerInterface;
 class BattleRenderer;
 class BattleRenderer;
 
 
@@ -206,6 +207,8 @@ class StackQueue : public CIntObject
 		std::shared_ptr<CAnimImage> icon;
 		std::shared_ptr<CAnimImage> icon;
 		std::shared_ptr<CLabel> amount;
 		std::shared_ptr<CLabel> amount;
 		std::shared_ptr<CAnimImage> stateIcon;
 		std::shared_ptr<CAnimImage> stateIcon;
+		std::shared_ptr<CLabel> round;
+		std::shared_ptr<TransparentFilledRectangle> roundRect;
 
 
 		void show(Canvas & to) override;
 		void show(Canvas & to) override;
 		void showAll(Canvas & to) override;
 		void showAll(Canvas & to) override;
@@ -213,9 +216,8 @@ class StackQueue : public CIntObject
 		bool isBoundUnitHighlighted() const;
 		bool isBoundUnitHighlighted() const;
 	public:
 	public:
 		StackBox(StackQueue * owner);
 		StackBox(StackQueue * owner);
-		void setUnit(const battle::Unit * unit, size_t turn = 0);
+		void setUnit(const battle::Unit * unit, size_t turn = 0, std::optional<ui32> currentTurn = std::nullopt);
 		std::optional<uint32_t> getBoundUnitID() const;
 		std::optional<uint32_t> getBoundUnitID() const;
-
 	};
 	};
 
 
 	static const int QUEUE_SIZE = 10;
 	static const int QUEUE_SIZE = 10;

+ 2 - 0
client/battle/BattleStacksController.cpp

@@ -691,6 +691,8 @@ void BattleStacksController::endAction(const BattleAction & action)
 
 
 void BattleStacksController::startAction(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();
 	removeExpiredColorFilters();
 }
 }
 
 

+ 43 - 4
client/battle/BattleWindow.cpp

@@ -31,6 +31,7 @@
 #include "../render/Canvas.h"
 #include "../render/Canvas.h"
 #include "../render/IRenderHandler.h"
 #include "../render/IRenderHandler.h"
 #include "../adventureMap/CInGameConsole.h"
 #include "../adventureMap/CInGameConsole.h"
+#include "../adventureMap/TurnTimerWidget.h"
 
 
 #include "../../CCallback.h"
 #include "../../CCallback.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/CGeneralTextHandler.h"
@@ -39,6 +40,7 @@
 #include "../../lib/CStack.h"
 #include "../../lib/CStack.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/filesystem/ResourcePath.h"
 #include "../../lib/filesystem/ResourcePath.h"
+#include "../../lib/StartInfo.h"
 #include "../windows/settings/SettingsMainWindow.h"
 #include "../windows/settings/SettingsMainWindow.h"
 
 
 BattleWindow::BattleWindow(BattleInterface & owner):
 BattleWindow::BattleWindow(BattleInterface & owner):
@@ -83,6 +85,7 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 
 
 	createQueue();
 	createQueue();
 	createStickyHeroInfoWindows();
 	createStickyHeroInfoWindows();
+	createTimerInfoWindows();
 
 
 	if ( owner.tacticsMode )
 	if ( owner.tacticsMode )
 		tacticPhaseStarted();
 		tacticPhaseStarted();
@@ -128,8 +131,8 @@ void BattleWindow::createStickyHeroInfoWindows()
 		InfoAboutHero info;
 		InfoAboutHero info;
 		info.initFromHero(owner.defendingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
 		info.initFromHero(owner.defendingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
 		Point position = (GH.screenDimensions().x >= 1000)
 		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);
 		defenderHeroWindow = std::make_shared<HeroInfoBasicPanel>(info, &position);
 	}
 	}
 	if(owner.attackingHeroInstance)
 	if(owner.attackingHeroInstance)
@@ -137,8 +140,8 @@ void BattleWindow::createStickyHeroInfoWindows()
 		InfoAboutHero info;
 		InfoAboutHero info;
 		info.initFromHero(owner.attackingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
 		info.initFromHero(owner.attackingHeroInstance, InfoAboutHero::EInfoLevel::INBATTLE);
 		Point position = (GH.screenDimensions().x >= 1000)
 		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);
 		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()
 BattleWindow::~BattleWindow()
 {
 {
 	CPlayerInterface::battleInt = nullptr;
 	CPlayerInterface::battleInt = nullptr;
@@ -557,6 +587,15 @@ void BattleWindow::bSpellf()
 			LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[683])
 			LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[683])
 										% heroName % CGI->artifacts()->getByIndex(artID)->getNameTranslated()));
 										% 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 BattleConsole;
 class BattleRenderer;
 class BattleRenderer;
 class StackQueue;
 class StackQueue;
+class TurnTimerWidget;
 class HeroInfoBasicPanel;
 class HeroInfoBasicPanel;
 
 
 /// GUI object that handles functionality of panel at the bottom of combat screen
 /// 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> attackerHeroWindow;
 	std::shared_ptr<HeroInfoBasicPanel> defenderHeroWindow;
 	std::shared_ptr<HeroInfoBasicPanel> defenderHeroWindow;
 
 
+	std::shared_ptr<TurnTimerWidget> attackerTimerWidget;
+	std::shared_ptr<TurnTimerWidget> defenderTimerWidget;
+
 	/// button press handling functions
 	/// button press handling functions
 	void bOptionsf();
 	void bOptionsf();
 	void bSurrenderf();
 	void bSurrenderf();
@@ -65,6 +69,7 @@ class BattleWindow : public InterfaceObjectConfigurable
 
 
 	void toggleStickyHeroWindowsVisibility();
 	void toggleStickyHeroWindowsVisibility();
 	void createStickyHeroInfoWindows();
 	void createStickyHeroInfoWindows();
+	void createTimerInfoWindows();
 
 
 	std::shared_ptr<BattleConsole> buildBattleConsole(const JsonNode &) const;
 	std::shared_ptr<BattleConsole> buildBattleConsole(const JsonNode &) const;
 
 

+ 2 - 4
client/lobby/CBonusSelection.cpp

@@ -273,17 +273,15 @@ void CBonusSelection::createBonusesIcons()
 		}
 		}
 
 
 		case CampaignBonusType::HERO:
 		case CampaignBonusType::HERO:
-
-			desc.appendLocalString(EMetaText::GENERAL_TXT, 718);
-			desc.replaceTextID(TextIdentifier("core", "genrltxt", "capColors", bonDescs[i].info1).get());
 			if(bonDescs[i].info2 == 0xFFFF)
 			if(bonDescs[i].info2 == 0xFFFF)
 			{
 			{
-				desc.replaceLocalString(EMetaText::GENERAL_TXT, 101);
+				desc.appendLocalString(EMetaText::GENERAL_TXT, 720); // Start with random hero
 				picNumber = -1;
 				picNumber = -1;
 				picName = "CBONN1A3.BMP";
 				picName = "CBONN1A3.BMP";
 			}
 			}
 			else
 			else
 			{
 			{
+				desc.appendLocalString(EMetaText::GENERAL_TXT, 715); // Start with %s
 				desc.replaceTextID(CGI->heroh->objects[bonDescs[i].info2]->getNameTextID());
 				desc.replaceTextID(CGI->heroh->objects[bonDescs[i].info2]->getNameTextID());
 			}
 			}
 			break;
 			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);
 		CSH->sendGuiAction(LobbyGuiAction::OPEN_SCENARIO_LIST);
 	else if(tab == tabRand)
 	else if(tab == tabRand)
 		CSH->sendGuiAction(LobbyGuiAction::OPEN_RANDOM_MAP_OPTIONS);
 		CSH->sendGuiAction(LobbyGuiAction::OPEN_RANDOM_MAP_OPTIONS);
+	else if(tab == tabTurnOptions)
+		CSH->sendGuiAction(LobbyGuiAction::OPEN_TURN_OPTIONS);
 	CSelectionBase::toggleTab(tab);
 	CSelectionBase::toggleTab(tab);
 }
 }
 
 

+ 6 - 3
client/lobby/CSelectionBase.cpp

@@ -135,7 +135,7 @@ InfoCard::InfoCard()
 	Rect descriptionRect(26, 149, 320, 115);
 	Rect descriptionRect(26, 149, 320, 115);
 	mapDescription = std::make_shared<CTextBox>("", descriptionRect, 1);
 	mapDescription = std::make_shared<CTextBox>("", descriptionRect, 1);
 	playerListBg = std::make_shared<CPicture>(ImagePath::builtin("CHATPLUG.bmp"), 16, 276);
 	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)
 	if(SEL->screenType == ESelectionScreen::campaignList)
 	{
 	{
@@ -332,9 +332,12 @@ CChatBox::CChatBox(const Rect & rect)
 	setRedrawParent(true);
 	setRedrawParent(true);
 
 
 	const int height = static_cast<int>(graphics->fonts[FONT_SMALL]->getLineHeight());
 	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);
 	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;
 	chatHistory->label->color = Colors::GREEN;
 }
 }

+ 2 - 0
client/lobby/CSelectionBase.h

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

+ 5 - 1
client/lobby/RandomMapTab.cpp

@@ -358,7 +358,11 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr<CMapGenOptions> opts)
 	}
 	}
 	for(auto r : VLC->roadTypeHandler->objects)
 	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()));
 			w->setSelected(opts->isRoadEnabled(r->getId()));
 		}
 		}

+ 3 - 0
client/mapView/MapViewController.cpp

@@ -269,6 +269,9 @@ void MapViewController::afterRender()
 
 
 bool MapViewController::isEventInstant(const CGObjectInstance * obj, const PlayerColor & initiator)
 bool MapViewController::isEventInstant(const CGObjectInstance * obj, const PlayerColor & initiator)
 {
 {
+	if(settings["gameTweaks"]["skipAdventureMapAnimations"].Bool())
+		return true;
+
 	if (!isEventVisible(obj, initiator))
 	if (!isEventVisible(obj, initiator))
 		return true;
 		return true;
 
 

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

+ 3 - 0
client/widgets/MiscWidgets.h

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

+ 6 - 3
client/widgets/TextControls.cpp

@@ -375,7 +375,7 @@ void CTextBox::setText(const std::string & text)
 	else if(slider)
 	else if(slider)
 	{
 	{
 		// decrease width again if slider still used
 		// decrease width again if slider still used
-		label->pos.w = pos.w - 32;
+		label->pos.w = pos.w - 16;
 		assert(label->pos.w > 0);
 		assert(label->pos.w > 0);
 		label->setText(text);
 		label->setText(text);
 		slider->setAmount(label->textSize.y);
 		slider->setAmount(label->textSize.y);
@@ -383,12 +383,12 @@ void CTextBox::setText(const std::string & text)
 	else if(label->textSize.y > label->pos.h)
 	else if(label->textSize.y > label->pos.h)
 	{
 	{
 		// create slider and update widget
 		// create slider and update widget
-		label->pos.w = pos.w - 32;
+		label->pos.w = pos.w - 16;
 		assert(label->pos.w > 0);
 		assert(label->pos.w > 0);
 		label->setText(text);
 		label->setText(text);
 
 
 		OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
 		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));
 			label->pos.h, label->textSize.y, 0, Orientation::VERTICAL, CSlider::EStyle(sliderStyle));
 		slider->setScrollStep((int)graphics->fonts[label->font]->getLineHeight());
 		slider->setScrollStep((int)graphics->fonts[label->font]->getLineHeight());
 		slider->setPanningStep(1);
 		slider->setPanningStep(1);
@@ -801,6 +801,9 @@ void CFocusable::moveFocus()
 		if(i == focusables.end())
 		if(i == focusables.end())
 			i = focusables.begin();
 			i = focusables.begin();
 
 
+		if (*i == this)
+			return;
+
 		if((*i)->isActive())
 		if((*i)->isActive())
 		{
 		{
 			(*i)->giveFocus();
 			(*i)->giveFocus();

+ 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));
 		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);
 	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.
 	//%s has gained a level.

+ 2 - 1
client/windows/GUIClasses.h

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

+ 7 - 0
client/windows/settings/AdventureOptionsTab.cpp

@@ -130,6 +130,10 @@ AdventureOptionsTab::AdventureOptionsTab()
 	{
 	{
 		return setBoolSetting("adventure", "smoothDragging", value);
 		return setBoolSetting("adventure", "smoothDragging", value);
 	});
 	});
+	addCallback("skipAdventureMapAnimationsChanged", [](bool value)
+	{
+		return setBoolSetting("gameTweaks", "skipAdventureMapAnimations", value);
+	});
 	build(config);
 	build(config);
 
 
 	std::shared_ptr<CToggleGroup> playerHeroSpeedToggle = widget<CToggleGroup>("heroMovementSpeedPicker");
 	std::shared_ptr<CToggleGroup> playerHeroSpeedToggle = widget<CToggleGroup>("heroMovementSpeedPicker");
@@ -172,4 +176,7 @@ AdventureOptionsTab::AdventureOptionsTab()
 	std::shared_ptr<CToggleButton> smoothDraggingCheckbox = widget<CToggleButton>("smoothDraggingCheckbox");
 	std::shared_ptr<CToggleButton> smoothDraggingCheckbox = widget<CToggleButton>("smoothDraggingCheckbox");
 	if (smoothDraggingCheckbox)
 	if (smoothDraggingCheckbox)
 		smoothDraggingCheckbox->setSelected(settings["adventure"]["smoothDragging"].Bool());
 		smoothDraggingCheckbox->setSelected(settings["adventure"]["smoothDragging"].Bool());
+
+	std::shared_ptr<CToggleButton> skipAdventureMapAnimationsCheckbox = widget<CToggleButton>("skipAdventureMapAnimationsCheckbox");
+	skipAdventureMapAnimationsCheckbox->setSelected(settings["gameTweaks"]["skipAdventureMapAnimations"].Bool());
 }
 }

+ 11 - 1
client/windows/settings/GeneralOptionsTab.cpp

@@ -162,9 +162,15 @@ GeneralOptionsTab::GeneralOptionsTab()
 		setBoolSetting("general", "enableUiEnhancements", value);
 		setBoolSetting("general", "enableUiEnhancements", value);
 	});
 	});
 
 
-	addCallback("enableLargeSpellbookChanged", [](bool value)
+	addCallback("enableLargeSpellbookChanged", [this](bool value)
 	{
 	{
 		setBoolSetting("gameTweaks", "enableLargeSpellbook", value);
 		setBoolSetting("gameTweaks", "enableLargeSpellbook", value);
+		std::shared_ptr<CToggleButton> spellbookAnimationCheckbox = widget<CToggleButton>("spellbookAnimationCheckbox");
+		if(value)
+			spellbookAnimationCheckbox->disable();
+		else
+			spellbookAnimationCheckbox->enable();
+		redraw();
 	});
 	});
 
 
 	addCallback("audioMuteFocusChanged", [](bool value)
 	addCallback("audioMuteFocusChanged", [](bool value)
@@ -196,6 +202,10 @@ GeneralOptionsTab::GeneralOptionsTab()
 
 
 	std::shared_ptr<CToggleButton> spellbookAnimationCheckbox = widget<CToggleButton>("spellbookAnimationCheckbox");
 	std::shared_ptr<CToggleButton> spellbookAnimationCheckbox = widget<CToggleButton>("spellbookAnimationCheckbox");
 	spellbookAnimationCheckbox->setSelected(settings["video"]["spellbookAnimation"].Bool());
 	spellbookAnimationCheckbox->setSelected(settings["video"]["spellbookAnimation"].Bool());
+	if(settings["gameTweaks"]["enableLargeSpellbook"].Bool())
+		spellbookAnimationCheckbox->disable();
+	else
+		spellbookAnimationCheckbox->enable();
 
 
 	std::shared_ptr<CToggleButton> fullscreenBorderlessCheckbox = widget<CToggleButton>("fullscreenBorderlessCheckbox");
 	std::shared_ptr<CToggleButton> fullscreenBorderlessCheckbox = widget<CToggleButton>("fullscreenBorderlessCheckbox");
 	if (fullscreenBorderlessCheckbox)
 	if (fullscreenBorderlessCheckbox)

+ 2 - 0
config/artifacts.json

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

+ 6 - 1
config/schemas/settings.json

@@ -582,7 +582,8 @@
 				"infoBarPick",
 				"infoBarPick",
 				"skipBattleIntroMusic",
 				"skipBattleIntroMusic",
 				"infoBarCreatureManagement",
 				"infoBarCreatureManagement",
-				"enableLargeSpellbook"
+				"enableLargeSpellbook",
+				"skipAdventureMapAnimations"
 			],
 			],
 			"properties" : {
 			"properties" : {
 				"showGrid" : {
 				"showGrid" : {
@@ -618,6 +619,10 @@
 					"default" : true
 					"default" : true
 				},
 				},
 				"enableLargeSpellbook" : {
 				"enableLargeSpellbook" : {
+					"type": "boolean",
+					"default": true
+				},
+				"skipAdventureMapAnimations": {
 					"type": "boolean",
 					"type": "boolean",
 					"default": false
 					"default": false
 				}
 				}

+ 4 - 1
config/spells/timed.json

@@ -1153,7 +1153,10 @@
 		},
 		},
 		"targetCondition" : {
 		"targetCondition" : {
 			"noneOf" : {
 			"noneOf" : {
-				"bonus.SIEGE_WEAPON" : "absolute"
+				"bonus.MIND_IMMUNITY" : "absolute",
+				"bonus.NON_LIVING" : "absolute",
+				"bonus.SIEGE_WEAPON" : "absolute",
+				"bonus.UNDEAD" : "absolute"
 			}
 			}
 		}
 		}
 	},
 	},

+ 2 - 1
config/terrainViewPatterns.json

@@ -85,7 +85,8 @@
 				"N", "N", "N",
 				"N", "N", "N",
 				"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
 		// Mixed transitions
 		{
 		{

+ 14 - 6
config/widgets/settings/adventureOptionsTab.json

@@ -279,7 +279,7 @@
 			"callback": "mapScrollSpeedChanged"
 			"callback": "mapScrollSpeedChanged"
 		},
 		},
 		
 		
-/////////////////////////////////////// Right section - Original H3 options
+/////////////////////////////////////// Right section - Original H3 options + some custom
 		{
 		{
 			"type" : "verticalLayout",
 			"type" : "verticalLayout",
 			"customType" : "labelDescription",
 			"customType" : "labelDescription",
@@ -294,6 +294,9 @@
 				},
 				},
 				{
 				{
 					"text": "core.genrltxt.574" // quick combat
 					"text": "core.genrltxt.574" // quick combat
+				},
+				{
+					"text": "vcmi.adventureOptions.showGrid.hover"
 				}
 				}
 			]
 			]
 		},
 		},
@@ -305,7 +308,7 @@
 			[
 			[
 				{
 				{
 					"name": "showMovePathPlaceholder",
 					"name": "showMovePathPlaceholder",
-					"type": "checkboxFake",
+					"type": "checkboxFake"
 				},
 				},
 				{
 				{
 					"name": "heroReminderCheckbox",
 					"name": "heroReminderCheckbox",
@@ -317,6 +320,11 @@
 					"help": "core.help.362",
 					"help": "core.help.362",
 					"callback": "quickCombatChanged"
 					"callback": "quickCombatChanged"
 				},
 				},
+				{
+					"name": "showGridCheckbox",
+					"help": "vcmi.adventureOptions.showGrid",
+					"callback": "showGridChanged"
+				}
 			]
 			]
 		},
 		},
 /////////////////////////////////////// Bottom section - VCMI Options
 /////////////////////////////////////// Bottom section - VCMI Options
@@ -333,7 +341,7 @@
 					"text": "vcmi.adventureOptions.forceMovementInfo.hover"
 					"text": "vcmi.adventureOptions.forceMovementInfo.hover"
 				},
 				},
 				{
 				{
-					"text": "vcmi.adventureOptions.showGrid.hover"
+					"text": "vcmi.adventureOptions.skipAdventureMapAnimations.hover"
 				},
 				},
 				{
 				{
 					"text": "vcmi.adventureOptions.infoBarPick.hover"
 					"text": "vcmi.adventureOptions.infoBarPick.hover"
@@ -370,9 +378,9 @@
 					"callback": "forceMovementInfoChanged"
 					"callback": "forceMovementInfoChanged"
 				},
 				},
 				{
 				{
-					"name": "showGridCheckbox",
-					"help": "vcmi.adventureOptions.showGrid",
-					"callback": "showGridChanged"
+					"name": "skipAdventureMapAnimationsCheckbox",
+					"help": "vcmi.adventureOptions.skipAdventureMapAnimations",
+					"callback": "skipAdventureMapAnimationsChanged"
 				},
 				},
 				{
 				{
 					"name": "infoBarPickCheckbox",
 					"name": "infoBarPickCheckbox",

+ 29 - 0
config/widgets/settings/generalOptionsTab.json

@@ -70,6 +70,35 @@
 				}
 				}
 			]
 			]
 		},
 		},
+		{
+			"type" : "verticalLayout",
+			"customType" : "checkboxFake",
+			"position" : {"x": 10, "y": 83},
+			"items" : [
+				{
+					"created" : "desktop"
+				},
+				{},
+				{
+					"created" : "desktop"
+				},
+				{
+					"created" : "desktop"
+				},
+				{},
+				{},
+				{
+					"name": "spellbookAnimationCheckboxPlaceholder"
+				},
+				{
+					"created" : "touchscreen"
+				},
+				{
+					"created" : "mobile"
+				},
+				{}
+			]
+		},
 		{
 		{
 			"type" : "verticalLayout",
 			"type" : "verticalLayout",
 			"customType" : "checkbox",
 			"customType" : "checkbox",

+ 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
-	}
-}

+ 1 - 1
debian/changelog

@@ -2,7 +2,7 @@ vcmi (1.4.2) jammy; urgency=medium
 
 
   * New upstream release
   * New upstream release
 
 
- -- Ivan Savenko <[email protected]>  Fri, 22 Dec 2023 16:00:00 +0200
+ -- Ivan Savenko <[email protected]>  Mon, 25 Dec 2023 16:00:00 +0200
 
 
 vcmi (1.4.1) jammy; urgency=medium
 vcmi (1.4.1) jammy; urgency=medium
 
 

+ 1 - 0
docs/Readme.md

@@ -1,6 +1,7 @@
 [![GitHub](https://github.com/vcmi/vcmi/actions/workflows/github.yml/badge.svg)](https://github.com/vcmi/vcmi/actions/workflows/github.yml)
 [![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.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/1.4.1/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.1)
+[![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/1.4.2/total)](https://github.com/vcmi/vcmi/releases/tag/1.4.2)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 [![Github Downloads](https://img.shields.io/github/downloads/vcmi/vcmi/total)](https://github.com/vcmi/vcmi/releases)
 
 
 # VCMI Project
 # VCMI Project

+ 1 - 1
launcher/eu.vcmi.VCMI.metainfo.xml

@@ -76,7 +76,7 @@
 		</screenshot>
 		</screenshot>
 	</screenshots>
 	</screenshots>
 	<releases>
 	<releases>
-		<release version="1.4.2" date="2023-12-22" type="stable"/>
+		<release version="1.4.2" date="2023-12-25" type="stable"/>
 		<release version="1.4.1" date="2023-12-12" 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.4.0" date="2023-12-08" type="stable"/>
 		<release version="1.3.2" date="2023-09-15" type="stable"/>
 		<release version="1.3.2" date="2023-09-15" type="stable"/>

+ 35 - 25
lib/JsonNode.cpp

@@ -310,63 +310,73 @@ JsonMap & JsonNode::Struct()
 const bool boolDefault = false;
 const bool boolDefault = false;
 bool JsonNode::Bool() const
 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;
 const double floatDefault = 0;
 double JsonNode::Float() const
 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)
 	if(getType() == JsonType::DATA_INTEGER)
 		return static_cast<double>(std::get<si64>(data));
 		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
 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)
 	if(getType() == JsonType::DATA_FLOAT)
 		return static_cast<si64>(std::get<double>(data));
 		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 stringDefault = std::string();
 const std::string & JsonNode::String() const
 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 vectorDefault = JsonVector();
 const JsonVector & JsonNode::Vector() const
 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 mapDefault = JsonMap();
 const JsonMap & JsonNode::Struct() const
 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)
 JsonNode & JsonNode::operator[](const std::string & child)

+ 9 - 7
lib/LoadProgress.cpp

@@ -13,14 +13,16 @@
 
 
 using namespace Load;
 using namespace Load;
 
 
-Progress::Progress(): _progress(std::numeric_limits<Type>::min())
+Progress::Progress()
+	: Progress(100)
+{}
+
+Progress::Progress(int steps)
+	: _progress(std::numeric_limits<Type>::min())
+	, _target(std::numeric_limits<Type>::max())
+	, _step(std::numeric_limits<Type>::min())
+	, _maxSteps(steps)
 {
 {
-	setupSteps(100);
-}
-
-Progress::Progress(int steps): _progress(std::numeric_limits<Type>::min())
-{
-	setupSteps(steps);
 }
 }
 
 
 Type Progress::get() const
 Type Progress::get() const

+ 37 - 0
lib/TurnTimerInfo.cpp

@@ -22,4 +22,41 @@ bool TurnTimerInfo::isBattleEnabled() const
 	return turnTimer > 0 || baseTimer > 0 || unitTimer > 0 || battleTimer > 0;
 	return turnTimer > 0 || baseTimer > 0 || unitTimer > 0 || battleTimer > 0;
 }
 }
 
 
+void TurnTimerInfo::substractTimer(int timeMs)
+{
+	auto const & substractTimer = [&timeMs](int & targetTimer)
+	{
+		if (targetTimer > timeMs)
+		{
+			targetTimer -= timeMs;
+			timeMs = 0;
+		}
+		else
+		{
+			timeMs -= targetTimer;
+			targetTimer = 0;
+		}
+	};
+
+	substractTimer(unitTimer);
+	substractTimer(battleTimer);
+	substractTimer(turnTimer);
+	substractTimer(baseTimer);
+}
+
+int TurnTimerInfo::valueMs() const
+{
+	return baseTimer + turnTimer + battleTimer + unitTimer;
+}
+
+bool TurnTimerInfo::operator == (const TurnTimerInfo & other) const
+{
+	return turnTimer == other.turnTimer &&
+			baseTimer == other.baseTimer &&
+			battleTimer == other.battleTimer &&
+			unitTimer == other.unitTimer &&
+			accumulatingTurnTimer == other.accumulatingTurnTimer &&
+			accumulatingUnitTimer == other.accumulatingUnitTimer;
+}
+
 VCMI_LIB_NAMESPACE_END
 VCMI_LIB_NAMESPACE_END

+ 5 - 10
lib/TurnTimerInfo.h

@@ -28,16 +28,11 @@ struct DLL_LINKAGE TurnTimerInfo
 	bool isEnabled() const;
 	bool isEnabled() const;
 	bool isBattleEnabled() const;
 	bool isBattleEnabled() const;
 
 
-	bool operator == (const TurnTimerInfo & other) const
-	{
-		return turnTimer == other.turnTimer &&
-				baseTimer == other.baseTimer &&
-				battleTimer == other.battleTimer &&
-				unitTimer == other.unitTimer &&
-				accumulatingTurnTimer == other.accumulatingTurnTimer &&
-				accumulatingUnitTimer == other.accumulatingUnitTimer;
-	}
-	
+	void substractTimer(int timeMs);
+	int valueMs() const;
+
+	bool operator == (const TurnTimerInfo & other) const;
+
 	template <typename Handler>
 	template <typename Handler>
 	void serialize(Handler &h, const int version)
 	void serialize(Handler &h, const int version)
 	{
 	{

+ 15 - 21
lib/battle/CBattleInfoCallback.cpp

@@ -324,22 +324,6 @@ std::set<BattleHex> CBattleInfoCallback::battleGetAttackedHexes(const battle::Un
 	return attackedHexes;
 	return attackedHexes;
 }
 }
 
 
-SpellID CBattleInfoCallback::battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const
-{
-	switch (mode)
-	{
-	case RANDOM_GENIE:
-		return getRandomBeneficialSpell(rand, stack); //target
-		break;
-	case RANDOM_AIMED:
-		return getRandomCastedSpell(rand, stack); //caster
-		break;
-	default:
-		logGlobal->error("Incorrect mode of battleGetRandomSpell (%d)", static_cast<int>(mode));
-		return SpellID::NONE;
-	}
-}
-
 const CStack* CBattleInfoCallback::battleGetStackByPos(BattleHex pos, bool onlyAlive) const
 const CStack* CBattleInfoCallback::battleGetStackByPos(BattleHex pos, bool onlyAlive) const
 {
 {
 	RETURN_IF_NOT_BATTLE(nullptr);
 	RETURN_IF_NOT_BATTLE(nullptr);
@@ -1610,7 +1594,7 @@ std::set<const battle::Unit *> CBattleInfoCallback::battleAdjacentUnits(const ba
 	return ret;
 	return ret;
 }
 }
 
 
-SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const
+SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, const battle::Unit * caster, const battle::Unit * subject) const
 {
 {
 	RETURN_IF_NOT_BATTLE(SpellID::NONE);
 	RETURN_IF_NOT_BATTLE(SpellID::NONE);
 	//This is complete list. No spells from mods.
 	//This is complete list. No spells from mods.
@@ -1658,9 +1642,19 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c
 		std::stringstream cachingStr;
 		std::stringstream cachingStr;
 		cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << spellID.num;
 		cachingStr << "source_" << vstd::to_underlying(BonusSource::SPELL_EFFECT) << "id_" << spellID.num;
 
 
-		if(subject->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(spellID)), Selector::all, cachingStr.str())
-		 //TODO: this ability has special limitations
-		|| !(spellID.toSpell()->canBeCast(this, spells::Mode::CREATURE_ACTIVE, subject)))
+		if(subject->hasBonus(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(spellID)), Selector::all, cachingStr.str()))
+			continue;
+
+		auto spellPtr = spellID.toSpell();
+		spells::Target target;
+		target.emplace_back(subject);
+
+		spells::BattleCast cast(this, caster, spells::Mode::CREATURE_ACTIVE, spellPtr);
+
+		auto m = spellPtr->battleMechanics(&cast);
+		spells::detail::ProblemImpl problem;
+
+		if (!m->canBeCastAt(target, problem))
 			continue;
 			continue;
 
 
 		switch (spellID.toEnum())
 		switch (spellID.toEnum())
@@ -1703,7 +1697,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c
 		case SpellID::CURE: //only damaged units
 		case SpellID::CURE: //only damaged units
 		{
 		{
 			//do not cast on affected by debuffs
 			//do not cast on affected by debuffs
-			if(!subject->canBeHealed())
+			if(subject->getFirstHPleft() == subject->getMaxHealth())
 				continue;
 				continue;
 		}
 		}
 			break;
 			break;

+ 1 - 7
lib/battle/CBattleInfoCallback.h

@@ -52,11 +52,6 @@ struct DLL_LINKAGE BattleClientInterfaceData
 class DLL_LINKAGE CBattleInfoCallback : public virtual CBattleInfoEssentials
 class DLL_LINKAGE CBattleInfoCallback : public virtual CBattleInfoEssentials
 {
 {
 public:
 public:
-	enum ERandomSpell
-	{
-		RANDOM_GENIE, RANDOM_AIMED
-	};
-
 	std::optional<int> battleIsFinished() const override; //return none if battle is ongoing; otherwise the victorious side (0/1) or 2 if it is a draw
 	std::optional<int> battleIsFinished() const override; //return none if battle is ongoing; otherwise the victorious side (0/1) or 2 if it is a draw
 
 
 	std::vector<std::shared_ptr<const CObstacleInstance>> battleGetAllObstaclesOnPos(BattleHex tile, bool onlyBlocking = true) const override;
 	std::vector<std::shared_ptr<const CObstacleInstance>> battleGetAllObstaclesOnPos(BattleHex tile, bool onlyBlocking = true) const override;
@@ -121,8 +116,7 @@ public:
 	int32_t battleGetSpellCost(const spells::Spell * sp, const CGHeroInstance * caster) const; //returns cost of given spell
 	int32_t battleGetSpellCost(const spells::Spell * sp, const CGHeroInstance * caster) const; //returns cost of given spell
 	ESpellCastProblem battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const; //returns true if there are no general issues preventing from casting a spell
 	ESpellCastProblem battleCanCastSpell(const spells::Caster * caster, spells::Mode mode) const; //returns true if there are no general issues preventing from casting a spell
 
 
-	SpellID battleGetRandomStackSpell(CRandomGenerator & rand, const CStack * stack, ERandomSpell mode) const;
-	SpellID getRandomBeneficialSpell(CRandomGenerator & rand, const CStack * subject) const;
+	SpellID getRandomBeneficialSpell(CRandomGenerator & rand, const battle::Unit * caster, const battle::Unit * target) const;
 	SpellID getRandomCastedSpell(CRandomGenerator & rand, const CStack * caster) const; //called at the beginning of turn for Faerie Dragon
 	SpellID getRandomCastedSpell(CRandomGenerator & rand, const CStack * caster) const; //called at the beginning of turn for Faerie Dragon
 
 
 	std::vector<PossiblePlayerBattleAction> getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data);
 	std::vector<PossiblePlayerBattleAction> getClientActionsForStack(const CStack * stack, const BattleClientInterfaceData & data);

+ 7 - 0
lib/bonuses/BonusSelector.cpp

@@ -82,6 +82,13 @@ namespace Selector
 		return CSelectFieldEqual<BonusValueType>(&Bonus::valType)(valType);
 		return CSelectFieldEqual<BonusValueType>(&Bonus::valType)(valType);
 	}
 	}
 
 
+	CSelector DLL_LINKAGE typeSubtypeValueType(BonusType Type, BonusSubtypeID Subtype, BonusValueType valType)
+	{
+		return type()(Type)
+				.And(subtype()(Subtype))
+				.And(valueType(valType));
+	}
+
 	DLL_LINKAGE CSelector all([](const Bonus * b){return true;});
 	DLL_LINKAGE CSelector all([](const Bonus * b){return true;});
 	DLL_LINKAGE CSelector none([](const Bonus * b){return false;});
 	DLL_LINKAGE CSelector none([](const Bonus * b){return false;});
 }
 }

+ 1 - 0
lib/bonuses/BonusSelector.h

@@ -139,6 +139,7 @@ namespace Selector
 	CSelector DLL_LINKAGE source(BonusSource source, BonusSourceID sourceID);
 	CSelector DLL_LINKAGE source(BonusSource source, BonusSourceID sourceID);
 	CSelector DLL_LINKAGE sourceTypeSel(BonusSource source);
 	CSelector DLL_LINKAGE sourceTypeSel(BonusSource source);
 	CSelector DLL_LINKAGE valueType(BonusValueType valType);
 	CSelector DLL_LINKAGE valueType(BonusValueType valType);
+	CSelector DLL_LINKAGE typeSubtypeValueType(BonusType Type, BonusSubtypeID Subtype, BonusValueType valType);
 
 
 	/**
 	/**
 	 * Selects all bonuses
 	 * Selects all bonuses

+ 1 - 1
lib/bonuses/CBonusSystemNode.cpp

@@ -356,7 +356,7 @@ void CBonusSystemNode::addNewBonus(const std::shared_ptr<Bonus>& b)
 
 
 void CBonusSystemNode::accumulateBonus(const std::shared_ptr<Bonus>& b)
 void CBonusSystemNode::accumulateBonus(const std::shared_ptr<Bonus>& b)
 {
 {
-	auto bonus = exportedBonuses.getFirst(Selector::typeSubtype(b->type, b->subtype)); //only local bonuses are interesting //TODO: what about value type?
+	auto bonus = exportedBonuses.getFirst(Selector::typeSubtypeValueType(b->type, b->subtype, b->valType)); //only local bonuses are interesting
 	if(bonus)
 	if(bonus)
 		bonus->val += b->val;
 		bonus->val += b->val;
 	else
 	else

+ 16 - 8
lib/int3.h

@@ -180,6 +180,19 @@ public:
 		return { { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0),
 		return { { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0),
 			int3(1,1,0),int3(-1,1,0),int3(1,-1,0),int3(-1,-1,0) } };
 			int3(1,1,0),int3(-1,1,0),int3(1,-1,0),int3(-1,-1,0) } };
 	}
 	}
+
+	// Solution by ChatGPT
+
+	// Assume values up to +- 1000
+    friend std::size_t hash_value(const int3& v) {
+        // Since the range is [-1000, 1000], offsetting by 1000 maps it to [0, 2000]
+        std::size_t hx = v.x + 1000;
+        std::size_t hy = v.y + 1000;
+        std::size_t hz = v.z + 1000;
+
+        // Combine the hash values, multiplying them by prime numbers
+        return ((hx * 4000037u) ^ (hy * 2003u)) + hz;
+    }
 };
 };
 
 
 template<typename Container>
 template<typename Container>
@@ -204,14 +217,9 @@ int3 findClosestTile (Container & container, int3 dest)
 
 
 VCMI_LIB_NAMESPACE_END
 VCMI_LIB_NAMESPACE_END
 
 
-
 template<>
 template<>
 struct std::hash<VCMI_LIB_WRAP_NAMESPACE(int3)> {
 struct std::hash<VCMI_LIB_WRAP_NAMESPACE(int3)> {
-	size_t operator()(VCMI_LIB_WRAP_NAMESPACE(int3) const& pos) const
-	{
-		size_t ret = std::hash<int>()(pos.x);
-		VCMI_LIB_WRAP_NAMESPACE(vstd)::hash_combine(ret, pos.y);
-		VCMI_LIB_WRAP_NAMESPACE(vstd)::hash_combine(ret, pos.z);
-		return ret;
+	std::size_t operator()(VCMI_LIB_WRAP_NAMESPACE(int3) const& pos) const noexcept {
+		return hash_value(pos);
 	}
 	}
-};
+};

+ 20 - 0
lib/mapObjectConstructors/AObjectTypeHandler.cpp

@@ -223,6 +223,26 @@ std::vector<std::shared_ptr<const ObjectTemplate>>AObjectTypeHandler::getTemplat
 		return filtered;
 		return filtered;
 }
 }
 
 
+std::vector<std::shared_ptr<const ObjectTemplate>>AObjectTypeHandler::getMostSpecificTemplates(TerrainId terrainType) const
+{
+	auto templates = getTemplates(terrainType);
+	if (!templates.empty())
+	{
+		//Get terrain-specific template if possible
+		int leastTerrains = (*boost::min_element(templates, [](const std::shared_ptr<const ObjectTemplate> & tmp1, const std::shared_ptr<const ObjectTemplate> & tmp2)
+		{
+			return tmp1->getAllowedTerrains().size() < tmp2->getAllowedTerrains().size();
+		}))->getAllowedTerrains().size();
+
+		vstd::erase_if(templates, [leastTerrains](const std::shared_ptr<const ObjectTemplate> & tmp)
+		{
+			return tmp->getAllowedTerrains().size() > leastTerrains;
+		});
+	}
+
+	return templates;
+}
+
 std::shared_ptr<const ObjectTemplate> AObjectTypeHandler::getOverride(TerrainId terrainType, const CGObjectInstance * object) const
 std::shared_ptr<const ObjectTemplate> AObjectTypeHandler::getOverride(TerrainId terrainType, const CGObjectInstance * object) const
 {
 {
 	std::vector<std::shared_ptr<const ObjectTemplate>> ret = getTemplates(terrainType);
 	std::vector<std::shared_ptr<const ObjectTemplate>> ret = getTemplates(terrainType);

+ 1 - 0
lib/mapObjectConstructors/AObjectTypeHandler.h

@@ -79,6 +79,7 @@ public:
 	/// returns all templates matching parameters
 	/// returns all templates matching parameters
 	std::vector<std::shared_ptr<const ObjectTemplate>> getTemplates() const;
 	std::vector<std::shared_ptr<const ObjectTemplate>> getTemplates() const;
 	std::vector<std::shared_ptr<const ObjectTemplate>> getTemplates(const TerrainId terrainType) const;
 	std::vector<std::shared_ptr<const ObjectTemplate>> getTemplates(const TerrainId terrainType) const;
+	std::vector<std::shared_ptr<const ObjectTemplate>> getMostSpecificTemplates(TerrainId terrainType) const;
 
 
 	/// returns preferred template for this object, if present (e.g. one of 3 possible templates for town - village, fort and castle)
 	/// returns preferred template for this object, if present (e.g. one of 3 possible templates for town - village, fort and castle)
 	/// note that appearance will not be changed - this must be done separately (either by assignment or via pack from server)
 	/// note that appearance will not be changed - this must be done separately (either by assignment or via pack from server)

+ 4 - 1
lib/mapObjectConstructors/CObjectClassesHandler.cpp

@@ -314,7 +314,10 @@ TObjectTypeHandler CObjectClassesHandler::getHandlerFor(MapObjectID type, MapObj
 		if (objects.at(type.getNum()) == nullptr)
 		if (objects.at(type.getNum()) == nullptr)
 			return objects.front()->objects.front();
 			return objects.front()->objects.front();
 
 
-		auto result = objects.at(type.getNum())->objects.at(subtype.getNum());
+		auto subID = subtype.getNum();
+		if (type == Obj::PRISON)
+			subID = 0;
+		auto result = objects.at(type.getNum())->objects.at(subID);
 
 
 		if (result != nullptr)
 		if (result != nullptr)
 			return result;
 			return result;

+ 2 - 2
lib/mapObjects/CArmedInstance.cpp

@@ -45,8 +45,8 @@ CArmedInstance::CArmedInstance()
 {
 {
 }
 }
 
 
-CArmedInstance::CArmedInstance(bool isHypotetic):
-	CBonusSystemNode(isHypotetic),
+CArmedInstance::CArmedInstance(bool isHypothetic):
+	CBonusSystemNode(isHypothetic),
 	nonEvilAlignmentMix(this, nonEvilAlignmentMixSelector),
 	nonEvilAlignmentMix(this, nonEvilAlignmentMixSelector),
 	battle(nullptr)
 	battle(nullptr)
 {
 {

+ 1 - 1
lib/mapObjects/CArmedInstance.h

@@ -43,7 +43,7 @@ public:
 	//////////////////////////////////////////////////////////////////////////
 	//////////////////////////////////////////////////////////////////////////
 
 
 	CArmedInstance();
 	CArmedInstance();
-	CArmedInstance(bool isHypotetic);
+	CArmedInstance(bool isHypothetic);
 
 
 	PlayerColor getOwner() const override
 	PlayerColor getOwner() const override
 	{
 	{

+ 11 - 2
lib/mapObjects/CRewardableObject.cpp

@@ -111,11 +111,20 @@ void CRewardableObject::onHeroVisit(const CGHeroInstance *h) const
 						selectRewardWthMessage(h, rewards, configuration.onSelect);
 						selectRewardWthMessage(h, rewards, configuration.onSelect);
 						break;
 						break;
 					case Rewardable::SELECT_FIRST: // give first available
 					case Rewardable::SELECT_FIRST: // give first available
-						grantRewardWithMessage(h, rewards.front(), true);
+						if (configuration.canRefuse)
+							selectRewardWthMessage(h, { rewards.front() }, configuration.info.at(rewards.front()).message);
+						else
+							grantRewardWithMessage(h, rewards.front(), true);
 						break;
 						break;
 					case Rewardable::SELECT_RANDOM: // give random
 					case Rewardable::SELECT_RANDOM: // give random
-						grantRewardWithMessage(h, *RandomGeneratorUtil::nextItem(rewards, cb->gameState()->getRandomGenerator()), true);
+					{
+						ui32 rewardIndex = *RandomGeneratorUtil::nextItem(rewards, cb->gameState()->getRandomGenerator());
+						if (configuration.canRefuse)
+							selectRewardWthMessage(h, { rewardIndex }, configuration.info.at(rewardIndex).message);
+						else
+							grantRewardWithMessage(h, rewardIndex, true);
 						break;
 						break;
+					}
 				}
 				}
 				break;
 				break;
 			}
 			}

+ 27 - 1
lib/mapObjects/MiscObjects.cpp

@@ -12,6 +12,7 @@
 #include "MiscObjects.h"
 #include "MiscObjects.h"
 
 
 #include "../ArtifactUtils.h"
 #include "../ArtifactUtils.h"
+#include "../bonuses/Propagators.h"
 #include "../constants/StringConstants.h"
 #include "../constants/StringConstants.h"
 #include "../CConfigHandler.h"
 #include "../CConfigHandler.h"
 #include "../CGeneralTextHandler.h"
 #include "../CGeneralTextHandler.h"
@@ -116,7 +117,15 @@ void CGMine::initObj(CRandomGenerator & rand)
 		putStack(SlotID(0), troglodytes);
 		putStack(SlotID(0), troglodytes);
 
 
 		assert(!abandonedMineResources.empty());
 		assert(!abandonedMineResources.empty());
-		producedResource = *RandomGeneratorUtil::nextItem(abandonedMineResources, rand);
+		if (!abandonedMineResources.empty())
+		{
+			producedResource = *RandomGeneratorUtil::nextItem(abandonedMineResources, rand);
+		}
+		else
+		{
+			logGlobal->error("Abandoned mine at (%s) has no valid resource candidates!", pos.toString());
+			producedResource = GameResID::GOLD;
+		}
 	}
 	}
 	else
 	else
 	{
 	{
@@ -1005,6 +1014,23 @@ void CGGarrison::serializeJsonOptions(JsonSerializeFormat& handler)
 	CArmedInstance::serializeJsonOptions(handler);
 	CArmedInstance::serializeJsonOptions(handler);
 }
 }
 
 
+void CGGarrison::initObj(CRandomGenerator &rand)
+{
+	if(this->subID == MapObjectSubID::decode(this->ID, "antiMagic"))
+		addAntimagicGarrisonBonus();
+}
+
+void CGGarrison::addAntimagicGarrisonBonus()
+{
+	auto bonus = std::make_shared<Bonus>();
+	bonus->type = BonusType::BLOCK_ALL_MAGIC;
+	bonus->source = BonusSource::OBJECT_TYPE;
+	bonus->sid = BonusSourceID(this->ID);
+	bonus->propagator = std::make_shared<CPropagatorNodeType>(CBonusSystemNode::BATTLE);
+	bonus->duration = BonusDuration::PERMANENT;
+	this->addNewBonus(bonus);
+}
+
 void CGMagi::initObj(CRandomGenerator & rand)
 void CGMagi::initObj(CRandomGenerator & rand)
 {
 {
 	if (ID == Obj::EYE_OF_MAGI)
 	if (ID == Obj::EYE_OF_MAGI)

+ 2 - 0
lib/mapObjects/MiscObjects.h

@@ -60,6 +60,7 @@ class DLL_LINKAGE CGGarrison : public CArmedInstance
 public:
 public:
 	bool removableUnits;
 	bool removableUnits;
 
 
+	void initObj(CRandomGenerator &rand) override;
 	bool passableFor(PlayerColor color) const override;
 	bool passableFor(PlayerColor color) const override;
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
@@ -71,6 +72,7 @@ public:
 	}
 	}
 protected:
 protected:
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;
+	void addAntimagicGarrisonBonus();
 };
 };
 
 
 class DLL_LINKAGE CGArtifact : public CArmedInstance
 class DLL_LINKAGE CGArtifact : public CArmedInstance

+ 2 - 4
lib/mapping/CMapEditManager.cpp

@@ -125,9 +125,9 @@ void CMapEditManager::clearTerrain(CRandomGenerator * gen)
 	execute(std::make_unique<CClearTerrainOperation>(map, gen ? gen : &(this->gen)));
 	execute(std::make_unique<CClearTerrainOperation>(map, gen ? gen : &(this->gen)));
 }
 }
 
 
-void CMapEditManager::drawTerrain(TerrainId terType, CRandomGenerator * gen)
+void CMapEditManager::drawTerrain(TerrainId terType, int decorationsPercentage, CRandomGenerator * gen)
 {
 {
-	execute(std::make_unique<CDrawTerrainOperation>(map, terrainSel, terType, gen ? gen : &(this->gen)));
+	execute(std::make_unique<CDrawTerrainOperation>(map, terrainSel, terType, decorationsPercentage, gen ? gen : &(this->gen)));
 	terrainSel.clearSelection();
 	terrainSel.clearSelection();
 }
 }
 
 
@@ -143,8 +143,6 @@ void CMapEditManager::drawRiver(RiverId riverType, CRandomGenerator* gen)
 	terrainSel.clearSelection();
 	terrainSel.clearSelection();
 }
 }
 
 
-
-
 void CMapEditManager::insertObject(CGObjectInstance * obj)
 void CMapEditManager::insertObject(CGObjectInstance * obj)
 {
 {
 	execute(std::make_unique<CInsertObjectOperation>(map, obj));
 	execute(std::make_unique<CInsertObjectOperation>(map, obj));

+ 1 - 1
lib/mapping/CMapEditManager.h

@@ -70,7 +70,7 @@ public:
 	void clearTerrain(CRandomGenerator * gen = nullptr);
 	void clearTerrain(CRandomGenerator * gen = nullptr);
 
 
 	/// Draws terrain at the current terrain selection. The selection will be cleared automatically.
 	/// Draws terrain at the current terrain selection. The selection will be cleared automatically.
-	void drawTerrain(TerrainId terType, CRandomGenerator * gen = nullptr);
+	void drawTerrain(TerrainId terType, int decorationsPercentage, CRandomGenerator * gen = nullptr);
 
 
 	/// Draws roads at the current terrain selection. The selection will be cleared automatically.
 	/// Draws roads at the current terrain selection. The selection will be cleared automatically.
 	void drawRoad(RoadId roadType, CRandomGenerator * gen = nullptr);
 	void drawRoad(RoadId roadType, CRandomGenerator * gen = nullptr);

+ 14 - 8
lib/mapping/CMapOperation.cpp

@@ -83,10 +83,11 @@ void CComposedOperation::addOperation(std::unique_ptr<CMapOperation>&& operation
 	operations.push_back(std::move(operation));
 	operations.push_back(std::move(operation));
 }
 }
 
 
-CDrawTerrainOperation::CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, CRandomGenerator * gen):
+CDrawTerrainOperation::CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, int decorationsPercentage, CRandomGenerator * gen):
 	CMapOperation(map),
 	CMapOperation(map),
 	terrainSel(std::move(terrainSel)),
 	terrainSel(std::move(terrainSel)),
 	terType(terType),
 	terType(terType),
+	decorationsPercentage(decorationsPercentage),
 	gen(gen)
 	gen(gen)
 {
 {
 
 
@@ -286,14 +287,19 @@ void CDrawTerrainOperation::updateTerrainViews()
 		// Get mapping
 		// Get mapping
 		const TerrainViewPattern& pattern = patterns[bestPattern][valRslt.flip];
 		const TerrainViewPattern& pattern = patterns[bestPattern][valRslt.flip];
 		std::pair<int, int> mapping;
 		std::pair<int, int> mapping;
-		if(valRslt.transitionReplacement.empty())
+
+		mapping = pattern.mapping[0];
+
+		if(pattern.decoration)
 		{
 		{
-			mapping = pattern.mapping[0];
+			if (pattern.mapping.size() < 2 || gen->nextInt(100) > decorationsPercentage)
+				mapping = pattern.mapping[0];
+			else
+				mapping = pattern.mapping[1];
 		}
 		}
-		else
-		{
+
+		if (!valRslt.transitionReplacement.empty())
 			mapping = valRslt.transitionReplacement == TerrainViewPattern::RULE_DIRT ? pattern.mapping[0] : pattern.mapping[1];
 			mapping = valRslt.transitionReplacement == TerrainViewPattern::RULE_DIRT ? pattern.mapping[0] : pattern.mapping[1];
-		}
 
 
 		// Set terrain view
 		// Set terrain view
 		auto & tile = map->getTile(pos);
 		auto & tile = map->getTile(pos);
@@ -555,12 +561,12 @@ CClearTerrainOperation::CClearTerrainOperation(CMap* map, CRandomGenerator* gen)
 {
 {
 	CTerrainSelection terrainSel(map);
 	CTerrainSelection terrainSel(map);
 	terrainSel.selectRange(MapRect(int3(0, 0, 0), map->width, map->height));
 	terrainSel.selectRange(MapRect(int3(0, 0, 0), map->width, map->height));
-	addOperation(std::make_unique<CDrawTerrainOperation>(map, terrainSel, ETerrainId::WATER, gen));
+	addOperation(std::make_unique<CDrawTerrainOperation>(map, terrainSel, ETerrainId::WATER, 0, gen));
 	if(map->twoLevel)
 	if(map->twoLevel)
 	{
 	{
 		terrainSel.clearSelection();
 		terrainSel.clearSelection();
 		terrainSel.selectRange(MapRect(int3(0, 0, 1), map->width, map->height));
 		terrainSel.selectRange(MapRect(int3(0, 0, 1), map->width, map->height));
-		addOperation(std::make_unique<CDrawTerrainOperation>(map, terrainSel, ETerrainId::ROCK, gen));
+		addOperation(std::make_unique<CDrawTerrainOperation>(map, terrainSel, ETerrainId::ROCK, 0, gen));
 	}
 	}
 }
 }
 
 

+ 2 - 1
lib/mapping/CMapOperation.h

@@ -63,7 +63,7 @@ private:
 class CDrawTerrainOperation : public CMapOperation
 class CDrawTerrainOperation : public CMapOperation
 {
 {
 public:
 public:
-	CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, CRandomGenerator * gen);
+	CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, int decorationsPercentage, CRandomGenerator * gen);
 
 
 	void execute() override;
 	void execute() override;
 	void undo() override;
 	void undo() override;
@@ -101,6 +101,7 @@ private:
 
 
 	CTerrainSelection terrainSel;
 	CTerrainSelection terrainSel;
 	TerrainId terType;
 	TerrainId terType;
+	int decorationsPercentage;
 	CRandomGenerator* gen;
 	CRandomGenerator* gen;
 	std::set<int3> invalidatedTerViews;
 	std::set<int3> invalidatedTerViews;
 };
 };

+ 2 - 0
lib/mapping/MapEditUtils.cpp

@@ -145,6 +145,7 @@ const std::string TerrainViewPattern::RULE_ANY = "?";
 TerrainViewPattern::TerrainViewPattern()
 TerrainViewPattern::TerrainViewPattern()
 	: diffImages(false)
 	: diffImages(false)
 	, rotationTypesCount(0)
 	, rotationTypesCount(0)
+	, decoration(false)
 	, minPoints(0)
 	, minPoints(0)
 	, maxPoints(std::numeric_limits<int>::max())
 	, maxPoints(std::numeric_limits<int>::max())
 {
 {
@@ -209,6 +210,7 @@ CTerrainViewPatternConfig::CTerrainViewPatternConfig()
 			// Read various properties
 			// Read various properties
 			pattern.id = ptrnNode["id"].String();
 			pattern.id = ptrnNode["id"].String();
 			assert(!pattern.id.empty());
 			assert(!pattern.id.empty());
+			pattern.decoration = ptrnNode["decoration"].Bool();
 			pattern.minPoints = static_cast<int>(ptrnNode["minPoints"].Float());
 			pattern.minPoints = static_cast<int>(ptrnNode["minPoints"].Float());
 			pattern.maxPoints = static_cast<int>(ptrnNode["maxPoints"].Float());
 			pattern.maxPoints = static_cast<int>(ptrnNode["maxPoints"].Float());
 			if (pattern.maxPoints == 0)
 			if (pattern.maxPoints == 0)

+ 2 - 0
lib/mapping/MapEditUtils.h

@@ -199,6 +199,8 @@ struct DLL_LINKAGE TerrainViewPattern
 	/// If diffImages is true, different images/frames are used to place a rotated terrain view. If it's false
 	/// If diffImages is true, different images/frames are used to place a rotated terrain view. If it's false
 	/// the same frame will be used and rotated.
 	/// the same frame will be used and rotated.
 	bool diffImages;
 	bool diffImages;
+	/// If true, then this pattern describes decoration tiles and should be used with specified probability
+	bool decoration;
 	/// The rotationTypesCount is only used if diffImages is true and holds the number how many rotation types(horizontal, etc...)
 	/// The rotationTypesCount is only used if diffImages is true and holds the number how many rotation types(horizontal, etc...)
 	/// are supported.
 	/// are supported.
 	int rotationTypesCount;
 	int rotationTypesCount;

+ 16 - 5
lib/mapping/ObstacleProxy.cpp

@@ -84,16 +84,27 @@ int ObstacleProxy::getWeightedObjects(const int3 & tile, CRandomGenerator & rand
 			rmg::Object * rmgObject = &allObjects.back();
 			rmg::Object * rmgObject = &allObjects.back();
 			for(const auto & offset : obj->getBlockedOffsets())
 			for(const auto & offset : obj->getBlockedOffsets())
 			{
 			{
-				rmgObject->setPosition(tile - offset);
+				auto newPos = tile - offset;
 
 
-				if(!isInTheMap(rmgObject->getPosition()))
+				if(!isInTheMap(newPos))
 					continue;
 					continue;
 
 
-				if(!rmgObject->getArea().getSubarea([this](const int3 & t)
+				rmgObject->setPosition(newPos);
+
+				bool isInTheMapEntirely = true;
+				for (const auto & t : rmgObject->getArea().getTiles())
+				{
+					if (!isInTheMap(t))
+					{
+						isInTheMapEntirely = false;
+						break;
+					}
+
+				}
+				if (!isInTheMapEntirely)
 				{
 				{
-					return !isInTheMap(t);
-				}).empty())
 					continue;
 					continue;
+				}
 
 
 				if(isProhibited(rmgObject->getArea()))
 				if(isProhibited(rmgObject->getArea()))
 					continue;
 					continue;

+ 1 - 1
lib/networkPacks/PacksForLobby.h

@@ -86,7 +86,7 @@ struct DLL_LINKAGE LobbyChatMessage : public CLobbyPackToPropagate
 struct DLL_LINKAGE LobbyGuiAction : public CLobbyPackToPropagate
 struct DLL_LINKAGE LobbyGuiAction : public CLobbyPackToPropagate
 {
 {
 	enum EAction : ui8 {
 	enum EAction : ui8 {
-		NONE, NO_TAB, OPEN_OPTIONS, OPEN_SCENARIO_LIST, OPEN_RANDOM_MAP_OPTIONS
+		NONE, NO_TAB, OPEN_OPTIONS, OPEN_SCENARIO_LIST, OPEN_RANDOM_MAP_OPTIONS, OPEN_TURN_OPTIONS
 	} action = NONE;
 	} action = NONE;
 
 
 
 

+ 1 - 0
lib/rewardable/Reward.cpp

@@ -33,6 +33,7 @@ Rewardable::Reward::Reward()
 	, heroLevel(0)
 	, heroLevel(0)
 	, manaDiff(0)
 	, manaDiff(0)
 	, manaPercentage(-1)
 	, manaPercentage(-1)
+	, manaOverflowFactor(0)
 	, movePoints(0)
 	, movePoints(0)
 	, movePercentage(-1)
 	, movePercentage(-1)
 	, primary(4, 0)
 	, primary(4, 0)

+ 24 - 18
lib/rmg/RmgArea.cpp

@@ -19,12 +19,12 @@ namespace rmg
 
 
 void toAbsolute(Tileset & tiles, const int3 & position)
 void toAbsolute(Tileset & tiles, const int3 & position)
 {
 {
-	Tileset temp;
-	for(const auto & tile : tiles)
+	std::vector vec(tiles.begin(), tiles.end());
+	tiles.clear();
+	std::transform(vec.begin(), vec.end(), vstd::set_inserter(tiles), [position](const int3 & tile)
 	{
 	{
-		temp.insert(tile + position);
-	}
-	tiles = std::move(temp);
+		return tile + position;
+	});
 }
 }
 
 
 void toRelative(Tileset & tiles, const int3 & position)
 void toRelative(Tileset & tiles, const int3 & position)
@@ -161,6 +161,7 @@ const Tileset & Area::getBorder() const
 		return dBorderCache;
 		return dBorderCache;
 	
 	
 	//compute border cache
 	//compute border cache
+	dBorderCache.reserve(dTiles.bucket_count());
 	for(const auto & t : dTiles)
 	for(const auto & t : dTiles)
 	{
 	{
 		for(auto & i : int3::getDirs())
 		for(auto & i : int3::getDirs())
@@ -182,6 +183,7 @@ const Tileset & Area::getBorderOutside() const
 		return dBorderOutsideCache;
 		return dBorderOutsideCache;
 	
 	
 	//compute outside border cache
 	//compute outside border cache
+	dBorderOutsideCache.reserve(dBorderCache.bucket_count() * 2);
 	for(const auto & t : dTiles)
 	for(const auto & t : dTiles)
 	{
 	{
 		for(auto & i : int3::getDirs())
 		for(auto & i : int3::getDirs())
@@ -238,6 +240,7 @@ bool Area::contains(const Area & area) const
 
 
 bool Area::overlap(const std::vector<int3> & tiles) const
 bool Area::overlap(const std::vector<int3> & tiles) const
 {
 {
+	// Important: Make sure that tiles.size < area.size
 	for(const auto & t : tiles)
 	for(const auto & t : tiles)
 	{
 	{
 		if(contains(t))
 		if(contains(t))
@@ -296,15 +299,15 @@ int3 Area::nearest(const Area & area) const
 Area Area::getSubarea(const std::function<bool(const int3 &)> & filter) const
 Area Area::getSubarea(const std::function<bool(const int3 &)> & filter) const
 {
 {
 	Area subset;
 	Area subset;
-	for(const auto & t : getTilesVector())
-		if(filter(t))
-			subset.add(t);
+	subset.dTiles.reserve(getTilesVector().size());
+	vstd::copy_if(getTilesVector(), vstd::set_inserter(subset.dTiles), filter);
 	return subset;
 	return subset;
 }
 }
 
 
 void Area::clear()
 void Area::clear()
 {
 {
 	dTiles.clear();
 	dTiles.clear();
+	dTilesVectorCache.clear();
 	dTotalShiftCache = int3();
 	dTotalShiftCache = int3();
 	invalidate();
 	invalidate();
 }
 }
@@ -329,15 +332,16 @@ void Area::erase(const int3 & tile)
 void Area::unite(const Area & area)
 void Area::unite(const Area & area)
 {
 {
 	invalidate();
 	invalidate();
-	for(const auto & t : area.getTilesVector())
-	{
-		dTiles.insert(t);
-	}
+	const auto & vec = area.getTilesVector();
+	dTiles.reserve(dTiles.size() + vec.size());
+	dTiles.insert(vec.begin(), vec.end());
 }
 }
+
 void Area::intersect(const Area & area)
 void Area::intersect(const Area & area)
 {
 {
 	invalidate();
 	invalidate();
 	Tileset result;
 	Tileset result;
+	result.reserve(std::max(dTiles.size(), area.getTilesVector().size()));
 	for(const auto & t : area.getTilesVector())
 	for(const auto & t : area.getTilesVector())
 	{
 	{
 		if(dTiles.count(t))
 		if(dTiles.count(t))
@@ -359,10 +363,9 @@ void Area::translate(const int3 & shift)
 {
 {
 	dBorderCache.clear();
 	dBorderCache.clear();
 	dBorderOutsideCache.clear();
 	dBorderOutsideCache.clear();
-	
+
 	if(dTilesVectorCache.empty())
 	if(dTilesVectorCache.empty())
 	{
 	{
-		getTiles();
 		getTilesVector();
 		getTilesVector();
 	}
 	}
 	
 	
@@ -373,7 +376,6 @@ void Area::translate(const int3 & shift)
 	{
 	{
 		t += shift;
 		t += shift;
 	}
 	}
-	//toAbsolute(dTiles, shift);
 }
 }
 
 
 void Area::erase_if(std::function<bool(const int3&)> predicate)
 void Area::erase_if(std::function<bool(const int3&)> predicate)
@@ -398,8 +400,12 @@ Area operator+ (const Area & l, const int3 & r)
 
 
 Area operator+ (const Area & l, const Area & r)
 Area operator+ (const Area & l, const Area & r)
 {
 {
-	Area result(l);
-	result.unite(r);
+	Area result;
+	const auto & lTiles = l.getTilesVector();
+	const auto & rTiles = r.getTilesVector();
+	result.dTiles.reserve(lTiles.size() + rTiles.size());
+	result.dTiles.insert(lTiles.begin(), lTiles.end());
+	result.dTiles.insert(rTiles.begin(), rTiles.end());
 	return result;
 	return result;
 }
 }
 
 
@@ -419,7 +425,7 @@ Area operator* (const Area & l, const Area & r)
 
 
 bool operator== (const Area & l, const Area & r)
 bool operator== (const Area & l, const Area & r)
 {
 {
-	return l.getTiles() == r.getTiles();
+	return l.getTilesVector() == r.getTilesVector();
 }
 }
 
 
 }
 }

+ 1 - 1
lib/rmg/RmgArea.h

@@ -20,7 +20,7 @@ namespace rmg
 	static const std::array<int3, 4> dirs4 = { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0) };
 	static const std::array<int3, 4> dirs4 = { int3(0,1,0),int3(0,-1,0),int3(-1,0,0),int3(+1,0,0) };
 	static const std::array<int3, 4> dirsDiagonal= { int3(1,1,0),int3(1,-1,0),int3(-1,1,0),int3(-1,-1,0) };
 	static const std::array<int3, 4> dirsDiagonal= { int3(1,1,0),int3(1,-1,0),int3(-1,1,0),int3(-1,-1,0) };
 
 
-	using Tileset = std::set<int3>;
+	using Tileset = std::unordered_set<int3>;
 	using DistanceMap = std::map<int3, int>;
 	using DistanceMap = std::map<int3, int>;
 	void toAbsolute(Tileset & tiles, const int3 & position);
 	void toAbsolute(Tileset & tiles, const int3 & position);
 	void toRelative(Tileset & tiles, const int3 & position);
 	void toRelative(Tileset & tiles, const int3 & position);

+ 7 - 2
lib/rmg/RmgMap.cpp

@@ -45,6 +45,11 @@ RmgMap::RmgMap(const CMapGenOptions& mapGenOptions) :
 	getEditManager()->getUndoManager().setUndoRedoLimit(0);
 	getEditManager()->getUndoManager().setUndoRedoLimit(0);
 }
 }
 
 
+int RmgMap::getDecorationsPercentage() const
+{
+	return 15; // arbitrary value to generate more readable map
+}
+
 void RmgMap::foreach_neighbour(const int3 & pos, const std::function<void(int3 & pos)> & foo) const
 void RmgMap::foreach_neighbour(const int3 & pos, const std::function<void(int3 & pos)> & foo) const
 {
 {
 	for(const int3 &dir : int3::getDirs())
 	for(const int3 &dir : int3::getDirs())
@@ -90,7 +95,7 @@ void RmgMap::initTiles(CMapGenerator & generator, CRandomGenerator & rand)
 	
 	
 	getEditManager()->clearTerrain(&rand);
 	getEditManager()->clearTerrain(&rand);
 	getEditManager()->getTerrainSelection().selectRange(MapRect(int3(0, 0, 0), mapGenOptions.getWidth(), mapGenOptions.getHeight()));
 	getEditManager()->getTerrainSelection().selectRange(MapRect(int3(0, 0, 0), mapGenOptions.getWidth(), mapGenOptions.getHeight()));
-	getEditManager()->drawTerrain(ETerrainId::GRASS, &rand);
+	getEditManager()->drawTerrain(ETerrainId::GRASS, getDecorationsPercentage(), &rand);
 
 
 	const auto * tmpl = mapGenOptions.getMapTemplate();
 	const auto * tmpl = mapGenOptions.getMapTemplate();
 	zones.clear();
 	zones.clear();
@@ -309,7 +314,7 @@ void RmgMap::setZoneID(const int3& tile, TRmgTemplateZoneId zid)
 	zoneColouring[tile.x][tile.y][tile.z] = zid;
 	zoneColouring[tile.x][tile.y][tile.z] = zid;
 }
 }
 
 
-void RmgMap::setNearestObjectDistance(int3 &tile, float value)
+void RmgMap::setNearestObjectDistance(const int3 &tile, float value)
 {
 {
 	assertOnMap(tile);
 	assertOnMap(tile);
 	
 	

+ 3 - 1
lib/rmg/RmgMap.h

@@ -27,6 +27,8 @@ class playerInfo;
 class RmgMap
 class RmgMap
 {
 {
 public:
 public:
+	int getDecorationsPercentage() const;
+
 	mutable std::unique_ptr<CMap> mapInstance;
 	mutable std::unique_ptr<CMap> mapInstance;
 	std::shared_ptr<MapProxy> getMapProxy() const;
 	std::shared_ptr<MapProxy> getMapProxy() const;
 	CMap & getMap(const CMapGenerator *) const; //limited access
 	CMap & getMap(const CMapGenerator *) const; //limited access
@@ -61,7 +63,7 @@ public:
 	TerrainTile & getTile(const int3 & tile) const;
 	TerrainTile & getTile(const int3 & tile) const;
 		
 		
 	float getNearestObjectDistance(const int3 &tile) const;
 	float getNearestObjectDistance(const int3 &tile) const;
-	void setNearestObjectDistance(int3 &tile, float value);
+	void setNearestObjectDistance(const int3 &tile, float value);
 	
 	
 	TRmgTemplateZoneId getZoneID(const int3& tile) const;
 	TRmgTemplateZoneId getZoneID(const int3& tile) const;
 	void setZoneID(const int3& tile, TRmgTemplateZoneId zid);
 	void setZoneID(const int3& tile, TRmgTemplateZoneId zid);

+ 90 - 55
lib/rmg/RmgObject.cpp

@@ -38,11 +38,10 @@ const Area & Object::Instance::getBlockedArea() const
 {
 {
 	if(dBlockedAreaCache.empty())
 	if(dBlockedAreaCache.empty())
 	{
 	{
-		dBlockedAreaCache.assign(dObject.getBlockedPos());
+		std::set<int3> blockedArea = dObject.getBlockedPos();
+		dBlockedAreaCache.assign(rmg::Tileset(blockedArea.begin(), blockedArea.end()));
 		if(dObject.isVisitable() || dBlockedAreaCache.empty())
 		if(dObject.isVisitable() || dBlockedAreaCache.empty())
-			if (!dObject.isBlockedVisitable())
-				// Do no assume blocked tile is accessible
-				dBlockedAreaCache.add(dObject.visitablePos());
+			dBlockedAreaCache.add(dObject.visitablePos());
 	}
 	}
 	return dBlockedAreaCache;
 	return dBlockedAreaCache;
 }
 }
@@ -70,8 +69,10 @@ const rmg::Area & Object::Instance::getAccessibleArea() const
 	if(dAccessibleAreaCache.empty())
 	if(dAccessibleAreaCache.empty())
 	{
 	{
 		auto neighbours = rmg::Area({getVisitablePosition()}).getBorderOutside();
 		auto neighbours = rmg::Area({getVisitablePosition()}).getBorderOutside();
+		// FIXME: Blocked area of removable object is also accessible area for neighbors
 		rmg::Area visitable = rmg::Area(neighbours) - getBlockedArea();
 		rmg::Area visitable = rmg::Area(neighbours) - getBlockedArea();
-		for(const auto & from : visitable.getTiles())
+		// TODO: Add in one operation to avoid multiple invalidation
+		for(const auto & from : visitable.getTilesVector())
 		{
 		{
 			if(isVisitableFrom(from))
 			if(isVisitableFrom(from))
 				dAccessibleAreaCache.add(from);
 				dAccessibleAreaCache.add(from);
@@ -122,22 +123,13 @@ void Object::Instance::setAnyTemplate(CRandomGenerator & rng)
 
 
 void Object::Instance::setTemplate(TerrainId terrain, CRandomGenerator & rng)
 void Object::Instance::setTemplate(TerrainId terrain, CRandomGenerator & rng)
 {
 {
-	auto templates = dObject.getObjectHandler()->getTemplates(terrain);
+	auto templates = dObject.getObjectHandler()->getMostSpecificTemplates(terrain);
+
 	if (templates.empty())
 	if (templates.empty())
 	{
 	{
 		auto terrainName = VLC->terrainTypeHandler->getById(terrain)->getNameTranslated();
 		auto terrainName = VLC->terrainTypeHandler->getById(terrain)->getNameTranslated();
 		throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s") % dObject.ID % dObject.getObjTypeIndex() % terrainName));
 		throw rmgException(boost::str(boost::format("Did not find graphics for object (%d,%d) at %s") % dObject.ID % dObject.getObjTypeIndex() % terrainName));
 	}
 	}
-	//Get terrain-specific template if possible
-	int leastTerrains = (*boost::min_element(templates, [](const std::shared_ptr<const ObjectTemplate> & tmp1, const std::shared_ptr<const ObjectTemplate> & tmp2)
-	{
-		return tmp1->getAllowedTerrains().size() < tmp2->getAllowedTerrains().size();
-	}))->getAllowedTerrains().size();
-
-	vstd::erase_if(templates, [leastTerrains](const std::shared_ptr<const ObjectTemplate> & tmp)
-	{
-		return tmp->getAllowedTerrains().size() > leastTerrains;
-	});
 	
 	
 	dObject.appearance = *RandomGeneratorUtil::nextItem(templates, rng);
 	dObject.appearance = *RandomGeneratorUtil::nextItem(templates, rng);
 	dAccessibleAreaCache.clear();
 	dAccessibleAreaCache.clear();
@@ -191,7 +183,6 @@ Object::Object(CGObjectInstance & object):
 }
 }
 
 
 Object::Object(const Object & object):
 Object::Object(const Object & object):
-	dStrength(object.dStrength),
 	guarded(false)
 	guarded(false)
 {
 {
 	for(const auto & i : object.dInstances)
 	for(const auto & i : object.dInstances)
@@ -199,20 +190,24 @@ Object::Object(const Object & object):
 	setPosition(object.getPosition());
 	setPosition(object.getPosition());
 }
 }
 
 
-std::list<Object::Instance*> Object::instances()
+std::list<Object::Instance*> & Object::instances()
 {
 {
-	std::list<Object::Instance*> result;
-	for(auto & i : dInstances)
-		result.push_back(&i);
-	return result;
+	if (cachedInstanceList.empty())
+	{
+		for(auto & i : dInstances)
+			cachedInstanceList.push_back(&i);
+	}
+	return cachedInstanceList;
 }
 }
 
 
-std::list<const Object::Instance*> Object::instances() const
+std::list<const Object::Instance*> & Object::instances() const
 {
 {
-	std::list<const Object::Instance*> result;
-	for(const auto & i : dInstances)
-		result.push_back(&i);
-	return result;
+	if (cachedInstanceConstList.empty())
+	{
+		for(const auto & i : dInstances)
+			cachedInstanceConstList.push_back(&i);
+	}
+	return cachedInstanceConstList;
 }
 }
 
 
 void Object::addInstance(Instance & object)
 void Object::addInstance(Instance & object)
@@ -220,16 +215,22 @@ void Object::addInstance(Instance & object)
 	//assert(object.dParent == *this);
 	//assert(object.dParent == *this);
 	setGuardedIfMonster(object);
 	setGuardedIfMonster(object);
 	dInstances.push_back(object);
 	dInstances.push_back(object);
+	cachedInstanceList.push_back(&object);
+	cachedInstanceConstList.push_back(&object);
 
 
 	clearCachedArea();
 	clearCachedArea();
+	visibleTopOffset.reset();
 }
 }
 
 
 Object::Instance & Object::addInstance(CGObjectInstance & object)
 Object::Instance & Object::addInstance(CGObjectInstance & object)
 {
 {
 	dInstances.emplace_back(*this, object);
 	dInstances.emplace_back(*this, object);
 	setGuardedIfMonster(dInstances.back());
 	setGuardedIfMonster(dInstances.back());
+	cachedInstanceList.push_back(&dInstances.back());
+	cachedInstanceConstList.push_back(&dInstances.back());
 
 
 	clearCachedArea();
 	clearCachedArea();
+	visibleTopOffset.reset();
 	return dInstances.back();
 	return dInstances.back();
 }
 }
 
 
@@ -237,8 +238,11 @@ Object::Instance & Object::addInstance(CGObjectInstance & object, const int3 & p
 {
 {
 	dInstances.emplace_back(*this, object, position);
 	dInstances.emplace_back(*this, object, position);
 	setGuardedIfMonster(dInstances.back());
 	setGuardedIfMonster(dInstances.back());
+	cachedInstanceList.push_back(&dInstances.back());
+	cachedInstanceConstList.push_back(&dInstances.back());
 
 
 	clearCachedArea();
 	clearCachedArea();
+	visibleTopOffset.reset();
 	return dInstances.back();
 	return dInstances.back();
 }
 }
 
 
@@ -265,15 +269,16 @@ const rmg::Area & Object::getAccessibleArea(bool exceptLast) const
 		return dAccessibleAreaCache;
 		return dAccessibleAreaCache;
 	if(!exceptLast && !dAccessibleAreaFullCache.empty())
 	if(!exceptLast && !dAccessibleAreaFullCache.empty())
 		return dAccessibleAreaFullCache;
 		return dAccessibleAreaFullCache;
-	
+
+	// FIXME: This clears tiles for every consecutive object
 	for(auto i = dInstances.begin(); i != std::prev(dInstances.end()); ++i)
 	for(auto i = dInstances.begin(); i != std::prev(dInstances.end()); ++i)
 		dAccessibleAreaCache.unite(i->getAccessibleArea());
 		dAccessibleAreaCache.unite(i->getAccessibleArea());
-	
+
 	dAccessibleAreaFullCache = dAccessibleAreaCache;
 	dAccessibleAreaFullCache = dAccessibleAreaCache;
 	dAccessibleAreaFullCache.unite(dInstances.back().getAccessibleArea());
 	dAccessibleAreaFullCache.unite(dInstances.back().getAccessibleArea());
 	dAccessibleAreaCache.subtract(getArea());
 	dAccessibleAreaCache.subtract(getArea());
 	dAccessibleAreaFullCache.subtract(getArea());
 	dAccessibleAreaFullCache.subtract(getArea());
-	
+
 	if(exceptLast)
 	if(exceptLast)
 		return dAccessibleAreaCache;
 		return dAccessibleAreaCache;
 	else
 	else
@@ -282,33 +287,45 @@ const rmg::Area & Object::getAccessibleArea(bool exceptLast) const
 
 
 const rmg::Area & Object::getBlockVisitableArea() const
 const rmg::Area & Object::getBlockVisitableArea() const
 {
 {
-	if(dInstances.empty())
-		return dBlockVisitableCache;
-
-	for(const auto & i : dInstances)
+	if(dBlockVisitableCache.empty())
 	{
 	{
-		// FIXME: Account for blockvis objects with multiple visitable tiles
-		if (i.isBlockedVisitable())
-			dBlockVisitableCache.add(i.getVisitablePosition());
+		for(const auto & i : dInstances)
+		{
+			// FIXME: Account for blockvis objects with multiple visitable tiles
+			if (i.isBlockedVisitable())
+				dBlockVisitableCache.add(i.getVisitablePosition());
+		}
 	}
 	}
-
 	return dBlockVisitableCache;
 	return dBlockVisitableCache;
 }
 }
 
 
 const rmg::Area & Object::getRemovableArea() const
 const rmg::Area & Object::getRemovableArea() const
 {
 {
-	if(dInstances.empty())
-		return dRemovableAreaCache;
-
-	for(const auto & i : dInstances)
+	if(dRemovableAreaCache.empty())
 	{
 	{
-		if (i.isRemovable())
-			dRemovableAreaCache.unite(i.getBlockedArea());
+		for(const auto & i : dInstances)
+		{
+			if (i.isRemovable())
+				dRemovableAreaCache.unite(i.getBlockedArea());
+		}
 	}
 	}
 
 
 	return dRemovableAreaCache;
 	return dRemovableAreaCache;
 }
 }
 
 
+const rmg::Area & Object::getVisitableArea() const
+{
+	if(dVisitableCache.empty())
+	{
+		for(const auto & i : dInstances)
+		{
+			// FIXME: Account for bjects with multiple visitable tiles
+			dVisitableCache.add(i.getVisitablePosition());
+		}
+	}
+	return dVisitableCache;
+}
+
 const rmg::Area Object::getEntrableArea() const
 const rmg::Area Object::getEntrableArea() const
 {
 {
 	// Calculate Area that hero can freely pass
 	// Calculate Area that hero can freely pass
@@ -316,7 +333,8 @@ const rmg::Area Object::getEntrableArea() const
 	// Do not use blockVisitTiles, unless they belong to removable objects (resources etc.)
 	// Do not use blockVisitTiles, unless they belong to removable objects (resources etc.)
 	// area = accessibleArea - (blockVisitableArea - removableArea)
 	// area = accessibleArea - (blockVisitableArea - removableArea)
 
 
-	rmg::Area entrableArea = getAccessibleArea();
+	// FIXME: What does it do? AccessibleArea means area AROUND the object 
+	rmg::Area entrableArea = getVisitableArea();
 	rmg::Area blockVisitableArea = getBlockVisitableArea();
 	rmg::Area blockVisitableArea = getBlockVisitableArea();
 	blockVisitableArea.subtract(getRemovableArea());
 	blockVisitableArea.subtract(getRemovableArea());
 	entrableArea.subtract(blockVisitableArea);
 	entrableArea.subtract(blockVisitableArea);
@@ -326,11 +344,14 @@ const rmg::Area Object::getEntrableArea() const
 
 
 void Object::setPosition(const int3 & position)
 void Object::setPosition(const int3 & position)
 {
 {
-	dAccessibleAreaCache.translate(position - dPosition);
-	dAccessibleAreaFullCache.translate(position - dPosition);
-	dBlockVisitableCache.translate(position - dPosition);
-	dRemovableAreaCache.translate(position - dPosition);
-	dFullAreaCache.translate(position - dPosition);
+	auto shift = position - dPosition;
+
+	dAccessibleAreaCache.translate(shift);
+	dAccessibleAreaFullCache.translate(shift);
+	dBlockVisitableCache.translate(shift);
+	dVisitableCache.translate(shift);
+	dRemovableAreaCache.translate(shift);
+	dFullAreaCache.translate(shift);
 	
 	
 	dPosition = position;
 	dPosition = position;
 	for(auto& i : dInstances)
 	for(auto& i : dInstances)
@@ -341,6 +362,8 @@ void Object::setTemplate(const TerrainId & terrain, CRandomGenerator & rng)
 {
 {
 	for(auto& i : dInstances)
 	for(auto& i : dInstances)
 		i.setTemplate(terrain, rng);
 		i.setTemplate(terrain, rng);
+
+	visibleTopOffset.reset();
 }
 }
 
 
 const Area & Object::getArea() const
 const Area & Object::getArea() const
@@ -358,15 +381,23 @@ const Area & Object::getArea() const
 
 
 const int3 Object::getVisibleTop() const
 const int3 Object::getVisibleTop() const
 {
 {
-	int3 topTile(-1, 10000, -1); //Start at the bottom
-	for (const auto& i : dInstances)
+	if (visibleTopOffset)
+	{
+		return dPosition + visibleTopOffset.value();
+	}
+	else
 	{
 	{
-		if (i.getTopTile().y < topTile.y)
+		int3 topTile(-1, 10000, -1); //Start at the bottom
+		for (const auto& i : dInstances)
 		{
 		{
-			topTile = i.getTopTile();
+			if (i.getTopTile().y < topTile.y)
+			{
+				topTile = i.getTopTile();
+			}
 		}
 		}
+		visibleTopOffset = topTile - dPosition;
+		return topTile;
 	}
 	}
-	return topTile;
 }
 }
 
 
 bool rmg::Object::isGuarded() const
 bool rmg::Object::isGuarded() const
@@ -436,6 +467,7 @@ void Object::clearCachedArea() const
 	dAccessibleAreaCache.clear();
 	dAccessibleAreaCache.clear();
 	dAccessibleAreaFullCache.clear();
 	dAccessibleAreaFullCache.clear();
 	dBlockVisitableCache.clear();
 	dBlockVisitableCache.clear();
+	dVisitableCache.clear();
 	dRemovableAreaCache.clear();
 	dRemovableAreaCache.clear();
 }
 }
 
 
@@ -444,6 +476,9 @@ void Object::clear()
 	for(auto & instance : dInstances)
 	for(auto & instance : dInstances)
 		instance.clear();
 		instance.clear();
 	dInstances.clear();
 	dInstances.clear();
+	cachedInstanceList.clear();
+	cachedInstanceConstList.clear();
+	visibleTopOffset.reset();
 
 
 	clearCachedArea();
 	clearCachedArea();
 }
 }

+ 7 - 3
lib/rmg/RmgObject.h

@@ -68,12 +68,13 @@ public:
 	Instance & addInstance(CGObjectInstance & object);
 	Instance & addInstance(CGObjectInstance & object);
 	Instance & addInstance(CGObjectInstance & object, const int3 & position);
 	Instance & addInstance(CGObjectInstance & object, const int3 & position);
 	
 	
-	std::list<Instance*> instances();
-	std::list<const Instance*> instances() const;
+	std::list<Instance*> & instances();
+	std::list<const Instance*> & instances() const;
 	
 	
 	int3 getVisitablePosition() const;
 	int3 getVisitablePosition() const;
 	const Area & getAccessibleArea(bool exceptLast = false) const;
 	const Area & getAccessibleArea(bool exceptLast = false) const;
 	const Area & getBlockVisitableArea() const;
 	const Area & getBlockVisitableArea() const;
+	const Area & getVisitableArea() const;
 	const Area & getRemovableArea() const;
 	const Area & getRemovableArea() const;
 	const Area getEntrableArea() const;
 	const Area getEntrableArea() const;
 	
 	
@@ -96,9 +97,12 @@ private:
 	mutable Area dFullAreaCache;
 	mutable Area dFullAreaCache;
 	mutable Area dAccessibleAreaCache, dAccessibleAreaFullCache;
 	mutable Area dAccessibleAreaCache, dAccessibleAreaFullCache;
 	mutable Area dBlockVisitableCache;
 	mutable Area dBlockVisitableCache;
+	mutable Area dVisitableCache;
 	mutable Area dRemovableAreaCache;
 	mutable Area dRemovableAreaCache;
 	int3 dPosition;
 	int3 dPosition;
-	ui32 dStrength;
+	mutable std::optional<int3> visibleTopOffset;
+	mutable std::list<Object::Instance*> cachedInstanceList;
+	mutable std::list<const Object::Instance*> cachedInstanceConstList;
 	bool guarded;
 	bool guarded;
 };
 };
 }
 }

+ 4 - 3
lib/rmg/RmgPath.cpp

@@ -68,11 +68,12 @@ Path Path::search(const Tileset & dst, bool straight, std::function<float(const
 	if(!dArea)
 	if(!dArea)
 		return Path::invalid();
 		return Path::invalid();
 	
 	
+	if(dst.empty()) // Skip construction of same area
+		return Path(*dArea);
+
 	auto resultArea = *dArea + dst;
 	auto resultArea = *dArea + dst;
 	Path result(resultArea);
 	Path result(resultArea);
-	if(dst.empty())
-		return result;
-	
+
 	int3 src = rmg::Area(dst).nearest(dPath);
 	int3 src = rmg::Area(dst).nearest(dPath);
 	result.connect(src);
 	result.connect(src);
 	
 	

+ 68 - 10
lib/rmg/Zone.cpp

@@ -15,6 +15,7 @@
 #include "TileInfo.h"
 #include "TileInfo.h"
 #include "CMapGenerator.h"
 #include "CMapGenerator.h"
 #include "RmgPath.h"
 #include "RmgPath.h"
+#include "modificators/ObjectManager.h"
 
 
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
 
 
@@ -177,6 +178,38 @@ rmg::Path Zone::searchPath(const rmg::Area & src, bool onlyStraight, const std::
 	return resultPath;
 	return resultPath;
 }
 }
 
 
+rmg::Path Zone::searchPath(const rmg::Area & src, bool onlyStraight, const rmg::Area & searchArea) const
+///connect current tile to any other free tile within searchArea
+{
+	auto movementCost = [this](const int3 & s, const int3 & d)
+	{
+		if(map.isFree(d))
+			return 1;
+		else if (map.isPossible(d))
+			return 2;
+		return 3;
+	};
+
+	rmg::Path freePath(searchArea);
+	rmg::Path resultPath(searchArea);
+	freePath.connect(dAreaFree);
+
+	//connect to all pieces
+	auto goals = connectedAreas(src, onlyStraight);
+	for(auto & goal : goals)
+	{
+		auto path = freePath.search(goal, onlyStraight, movementCost);
+		if(path.getPathArea().empty())
+			return rmg::Path::invalid();
+
+		freePath.connect(path.getPathArea());
+		resultPath.connect(path.getPathArea());
+	}
+
+	return resultPath;
+}
+
+
 rmg::Path Zone::searchPath(const int3 & src, bool onlyStraight, const std::function<bool(const int3 &)> & areafilter) const
 rmg::Path Zone::searchPath(const int3 & src, bool onlyStraight, const std::function<bool(const int3 &)> & areafilter) const
 ///connect current tile to any other free tile within zone
 ///connect current tile to any other free tile within zone
 {
 {
@@ -204,33 +237,38 @@ void Zone::fractalize()
 	rmg::Area tilesToIgnore; //will be erased in this iteration
 	rmg::Area tilesToIgnore; //will be erased in this iteration
 
 
 	//Squared
 	//Squared
-	float minDistance = 10 * 10;
+	float minDistance = 9 * 9;
+	float freeDistance = pos.z ? (10 * 10) : 6 * 6;
 	float spanFactor = (pos.z ? 0.25 : 0.5f); //Narrower passages in the Underground
 	float spanFactor = (pos.z ? 0.25 : 0.5f); //Narrower passages in the Underground
+	float marginFactor = 1.0f;
 
 
 	int treasureValue = 0;
 	int treasureValue = 0;
 	int treasureDensity = 0;
 	int treasureDensity = 0;
-	for (auto t : treasureInfo)
+	for (const auto & t : treasureInfo)
 	{
 	{
 		treasureValue += ((t.min + t.max) / 2) * t.density / 1000.f; //Thousands
 		treasureValue += ((t.min + t.max) / 2) * t.density / 1000.f; //Thousands
 		treasureDensity += t.density;
 		treasureDensity += t.density;
 	}
 	}
 
 
-	if (treasureValue > 200)
+	if (treasureValue > 400)
 	{
 	{
-		//Less obstacles - max span is 1 (no obstacles)
-		spanFactor = 1.0f - ((std::max(0, (1000 - treasureValue)) / (1000.f - 200)) * (1 - spanFactor));
+		// A quater at max density
+		marginFactor = (0.25f + ((std::max(0, (600 - treasureValue))) / (600.f - 400)) * 0.75f);
 	}
 	}
-	else if (treasureValue < 100)
+	else if (treasureValue < 125)
 	{
 	{
 		//Dense obstacles
 		//Dense obstacles
-		spanFactor *= (treasureValue / 100.f);
-		vstd::amax(spanFactor, 0.2f);
+		spanFactor *= (treasureValue / 125.f);
+		vstd::amax(spanFactor, 0.15f);
 	}
 	}
 	if (treasureDensity <= 10)
 	if (treasureDensity <= 10)
 	{
 	{
-		vstd::amin(spanFactor, 0.25f); //Add extra obstacles to fill up space
+		vstd::amin(spanFactor, 0.1f + 0.01f * treasureDensity); //Add extra obstacles to fill up space
 	}
 	}
 	float blockDistance = minDistance * spanFactor; //More obstacles in the Underground
 	float blockDistance = minDistance * spanFactor; //More obstacles in the Underground
+	freeDistance = freeDistance * marginFactor;
+	vstd::amax(freeDistance, 4 * 4);
+	logGlobal->info("Zone %d: treasureValue %d blockDistance: %2.f, freeDistance: %2.f", getId(), treasureValue, blockDistance, freeDistance);
 	
 	
 	if(type != ETemplateZoneType::JUNCTION)
 	if(type != ETemplateZoneType::JUNCTION)
 	{
 	{
@@ -240,6 +278,16 @@ void Zone::fractalize()
 		{
 		{
 			//link tiles in random order
 			//link tiles in random order
 			std::vector<int3> tilesToMakePath = possibleTiles.getTilesVector();
 			std::vector<int3> tilesToMakePath = possibleTiles.getTilesVector();
+
+			// Do not fractalize tiles near the edge of the map to avoid paths adjacent to map edge
+			const auto h = map.height();
+			const auto w = map.width();
+			const size_t MARGIN = 3;
+			vstd::erase_if(tilesToMakePath, [&, h, w](const int3 & tile)
+			{
+				return tile.x < MARGIN || tile.x > (w - MARGIN) ||
+					tile.y < MARGIN || tile.y > (h - MARGIN);
+			});
 			RandomGeneratorUtil::randomShuffle(tilesToMakePath, getRand());
 			RandomGeneratorUtil::randomShuffle(tilesToMakePath, getRand());
 			
 			
 			int3 nodeFound(-1, -1, -1);
 			int3 nodeFound(-1, -1, -1);
@@ -248,7 +296,7 @@ void Zone::fractalize()
 			{
 			{
 				//find closest free tile
 				//find closest free tile
 				int3 closestTile = clearedTiles.nearest(tileToMakePath);
 				int3 closestTile = clearedTiles.nearest(tileToMakePath);
-				if(closestTile.dist2dSQ(tileToMakePath) <= minDistance)
+				if(closestTile.dist2dSQ(tileToMakePath) <= freeDistance)
 					tilesToIgnore.add(tileToMakePath);
 					tilesToIgnore.add(tileToMakePath);
 				else
 				else
 				{
 				{
@@ -265,6 +313,16 @@ void Zone::fractalize()
 			tilesToIgnore.clear();
 			tilesToIgnore.clear();
 		}
 		}
 	}
 	}
+	else
+	{
+		// Handle special case - place Monoliths at the edge of a zone
+		auto objectManager = getModificator<ObjectManager>();
+		if (objectManager)
+		{
+			objectManager->createMonoliths();
+		}
+	}
+
 	Lock lock(areaMutex);
 	Lock lock(areaMutex);
 	//cut straight paths towards the center. A* is too slow for that.
 	//cut straight paths towards the center. A* is too slow for that.
 	auto areas = connectedAreas(clearedTiles, false);
 	auto areas = connectedAreas(clearedTiles, false);

+ 1 - 0
lib/rmg/Zone.h

@@ -66,6 +66,7 @@ public:
 	void connectPath(const rmg::Path & path);
 	void connectPath(const rmg::Path & path);
 	rmg::Path searchPath(const rmg::Area & src, bool onlyStraight, const std::function<bool(const int3 &)> & areafilter = AREA_NO_FILTER) const;
 	rmg::Path searchPath(const rmg::Area & src, bool onlyStraight, const std::function<bool(const int3 &)> & areafilter = AREA_NO_FILTER) const;
 	rmg::Path searchPath(const int3 & src, bool onlyStraight, const std::function<bool(const int3 &)> & areafilter = AREA_NO_FILTER) const;
 	rmg::Path searchPath(const int3 & src, bool onlyStraight, const std::function<bool(const int3 &)> & areafilter = AREA_NO_FILTER) const;
+	rmg::Path searchPath(const rmg::Area & src, bool onlyStraight, const rmg::Area & searchArea) const;
 
 
 	TModificators getModificators();
 	TModificators getModificators();
 
 

+ 2 - 1
lib/rmg/modificators/ConnectionsPlacer.cpp

@@ -302,6 +302,8 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c
 	if(zone.isUnderground() != otherZone->isUnderground())
 	if(zone.isUnderground() != otherZone->isUnderground())
 	{
 	{
 		int3 zShift(0, 0, zone.getPos().z - otherZone->getPos().z);
 		int3 zShift(0, 0, zone.getPos().z - otherZone->getPos().z);
+
+		std::scoped_lock doubleLock(zone.areaMutex, otherZone->areaMutex);
 		auto commonArea = zone.areaPossible() * (otherZone->areaPossible() + zShift);
 		auto commonArea = zone.areaPossible() * (otherZone->areaPossible() + zShift);
 		if(!commonArea.empty())
 		if(!commonArea.empty())
 		{
 		{
@@ -322,7 +324,6 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c
 			bool guarded2 = managerOther.addGuard(rmgGate2, connection.getGuardStrength(), true);
 			bool guarded2 = managerOther.addGuard(rmgGate2, connection.getGuardStrength(), true);
 			int minDist = 3;
 			int minDist = 3;
 			
 			
-			std::scoped_lock doubleLock(zone.areaMutex, otherZone->areaMutex);
 			rmg::Path path2(otherZone->area());
 			rmg::Path path2(otherZone->area());
 			rmg::Path path1 = manager.placeAndConnectObject(commonArea, rmgGate1, [this, minDist, &path2, &rmgGate1, &zShift, guarded2, &managerOther, &rmgGate2	](const int3 & tile)
 			rmg::Path path1 = manager.placeAndConnectObject(commonArea, rmgGate1, [this, minDist, &path2, &rmgGate1, &zShift, guarded2, &managerOther, &rmgGate2	](const int3 & tile)
 			{
 			{

+ 91 - 31
lib/rmg/modificators/ObjectManager.cpp

@@ -95,7 +95,7 @@ void ObjectManager::updateDistances(std::function<ui32(const int3 & tile)> dista
 {
 {
 	RecursiveLock lock(externalAccessMutex);
 	RecursiveLock lock(externalAccessMutex);
 	tilesByDistance.clear();
 	tilesByDistance.clear();
-	for (auto tile : zone.areaPossible().getTiles()) //don't need to mark distance for not possible tiles
+	for (const auto & tile : zone.areaPossible().getTilesVector()) //don't need to mark distance for not possible tiles
 	{
 	{
 		ui32 d = distanceFunction(tile);
 		ui32 d = distanceFunction(tile);
 		map.setNearestObjectDistance(tile, std::min(static_cast<float>(d), map.getNearestObjectDistance(tile)));
 		map.setNearestObjectDistance(tile, std::min(static_cast<float>(d), map.getNearestObjectDistance(tile)));
@@ -178,7 +178,7 @@ int3 ObjectManager::findPlaceForObject(const rmg::Area & searchArea, rmg::Object
 	}
 	}
 	else
 	else
 	{
 	{
-		for(const auto & tile : searchArea.getTiles())
+		for(const auto & tile : searchArea.getTilesVector())
 		{
 		{
 			obj.setPosition(tile);
 			obj.setPosition(tile);
 
 
@@ -238,15 +238,14 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg
 	RecursiveLock lock(externalAccessMutex);
 	RecursiveLock lock(externalAccessMutex);
 	return placeAndConnectObject(searchArea, obj, [this, min_dist, &obj](const int3 & tile)
 	return placeAndConnectObject(searchArea, obj, [this, min_dist, &obj](const int3 & tile)
 	{
 	{
-		auto ti = map.getTileInfo(tile);
-		float dist = ti.getNearestObjectDistance();
-		if(dist < min_dist)
-			return -1.f;
-
+		float bestDistance = 10e9;
 		for(const auto & t : obj.getArea().getTilesVector())
 		for(const auto & t : obj.getArea().getTilesVector())
 		{
 		{
-			if(map.getTileInfo(t).getNearestObjectDistance() < min_dist)
+			float distance = map.getTileInfo(t).getNearestObjectDistance();
+			if(distance < min_dist)
 				return -1.f;
 				return -1.f;
+			else
+				vstd::amin(bestDistance, distance);
 		}
 		}
 		
 		
 		rmg::Area perimeter;
 		rmg::Area perimeter;
@@ -298,7 +297,7 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg
 			}
 			}
 		}
 		}
 		
 		
-		return dist;
+		return bestDistance;
 	}, isGuarded, onlyStraight, optimizer);
 	}, isGuarded, onlyStraight, optimizer);
 }
 }
 
 
@@ -306,6 +305,7 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg
 {
 {
 	int3 pos;
 	int3 pos;
 	auto possibleArea = searchArea;
 	auto possibleArea = searchArea;
+	auto cachedArea = zone.areaPossible() + zone.freePaths();
 	while(true)
 	while(true)
 	{
 	{
 		pos = findPlaceForObject(possibleArea, obj, weightFunction, optimizer);
 		pos = findPlaceForObject(possibleArea, obj, weightFunction, optimizer);
@@ -314,7 +314,7 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg
 			return rmg::Path::invalid();
 			return rmg::Path::invalid();
 		}
 		}
 		possibleArea.erase(pos); //do not place again at this point
 		possibleArea.erase(pos); //do not place again at this point
-		auto accessibleArea = obj.getAccessibleArea(isGuarded) * (zone.areaPossible() + zone.freePaths());
+		auto accessibleArea = obj.getAccessibleArea(isGuarded) * cachedArea;
 		//we should exclude tiles which will be covered
 		//we should exclude tiles which will be covered
 		if(isGuarded)
 		if(isGuarded)
 		{
 		{
@@ -323,21 +323,31 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg
 			accessibleArea.add(obj.instances().back()->getPosition(true));
 			accessibleArea.add(obj.instances().back()->getPosition(true));
 		}
 		}
 
 
-		auto path = zone.searchPath(accessibleArea, onlyStraight, [&obj, isGuarded](const int3 & t)
+		rmg::Area subArea;
+		if (isGuarded)
 		{
 		{
-			if(isGuarded)
+			const auto & guardedArea = obj.instances().back()->getAccessibleArea();
+			const auto & unguardedArea = obj.getAccessibleArea(isGuarded);
+			subArea = cachedArea.getSubarea([guardedArea, unguardedArea, obj](const int3 & t)
 			{
 			{
-				const auto & guardedArea = obj.instances().back()->getAccessibleArea();
-				const auto & unguardedArea = obj.getAccessibleArea(isGuarded);
 				if(unguardedArea.contains(t) && !guardedArea.contains(t))
 				if(unguardedArea.contains(t) && !guardedArea.contains(t))
 					return false;
 					return false;
 				
 				
 				//guard position is always target
 				//guard position is always target
 				if(obj.instances().back()->getPosition(true) == t)
 				if(obj.instances().back()->getPosition(true) == t)
 					return true;
 					return true;
-			}
-			return !obj.getArea().contains(t);
-		});
+
+				return !obj.getArea().contains(t);
+			});
+		}
+		else
+		{
+			subArea = cachedArea.getSubarea([obj](const int3 & t)
+			{
+				return !obj.getArea().contains(t);
+			});
+		}
+		auto path = zone.searchPath(accessibleArea, onlyStraight, subArea);
 		
 		
 		if(path.valid())
 		if(path.valid())
 		{
 		{
@@ -346,6 +356,41 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg
 	}
 	}
 }
 }
 
 
+bool ObjectManager::createMonoliths()
+{
+	// Special case for Junction zone only
+	logGlobal->trace("Creating Monoliths");
+	for(const auto & objInfo : requiredObjects)
+	{
+		if (objInfo.obj->ID != Obj::MONOLITH_TWO_WAY)
+		{
+			continue;
+		}
+
+		rmg::Object rmgObject(*objInfo.obj);
+		rmgObject.setTemplate(zone.getTerrainType(), zone.getRand());
+		bool guarded = addGuard(rmgObject, objInfo.guardStrength, true);
+
+		Zone::Lock lock(zone.areaMutex);
+		auto path = placeAndConnectObject(zone.areaPossible(), rmgObject, 3, guarded, false, OptimizeType::DISTANCE);
+		
+		if(!path.valid())
+		{
+			logGlobal->error("Failed to fill zone %d due to lack of space", zone.getId());
+			return false;
+		}
+		
+		zone.connectPath(path);
+		placeObject(rmgObject, guarded, true, objInfo.createRoad);
+	}
+
+	vstd::erase_if(requiredObjects, [](const auto & objInfo)
+	{
+		return  objInfo.obj->ID == Obj::MONOLITH_TWO_WAY;
+	});
+	return true;
+}
+
 bool ObjectManager::createRequiredObjects()
 bool ObjectManager::createRequiredObjects()
 {
 {
 	logGlobal->trace("Creating required objects");
 	logGlobal->trace("Creating required objects");
@@ -424,7 +469,8 @@ bool ObjectManager::createRequiredObjects()
 		}
 		}
 
 
 		rmg::Object rmgNearObject(*nearby.obj);
 		rmg::Object rmgNearObject(*nearby.obj);
-		rmg::Area possibleArea(rmg::Area(targetObject->getBlockedPos()).getBorderOutside());
+		std::set<int3> blockedArea = targetObject->getBlockedPos();
+		rmg::Area possibleArea(rmg::Area(rmg::Tileset(blockedArea.begin(), blockedArea.end())).getBorderOutside());
 		possibleArea.intersect(zone.areaPossible());
 		possibleArea.intersect(zone.areaPossible());
 		if(possibleArea.empty())
 		if(possibleArea.empty())
 		{
 		{
@@ -513,6 +559,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD
 			if(map.isOnMap(i) && map.isPossible(i))
 			if(map.isOnMap(i) && map.isPossible(i))
 				map.setOccupied(i, ETileType::BLOCKED);
 				map.setOccupied(i, ETileType::BLOCKED);
 	}
 	}
+	lock.unlock();
 	
 	
 	if (updateDistance)
 	if (updateDistance)
 	{
 	{
@@ -535,11 +582,13 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD
 			auto manager = map.getZones().at(id)->getModificator<ObjectManager>();
 			auto manager = map.getZones().at(id)->getModificator<ObjectManager>();
 			if (manager)
 			if (manager)
 			{
 			{
+				// TODO: Update distances for perimeter of guarded object, not just treasures
 				manager->updateDistances(object);
 				manager->updateDistances(object);
 			}
 			}
 		}
 		}
 	}
 	}
 	
 	
+	// TODO: Add multiple tiles in one operation to avoid multiple invalidation
 	for(auto * instance : object.instances())
 	for(auto * instance : object.instances())
 	{
 	{
 		objectsVisitableArea.add(instance->getVisitablePosition());
 		objectsVisitableArea.add(instance->getVisitablePosition());
@@ -552,10 +601,23 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD
 				continue;
 				continue;
 			}
 			}
 			else if(instance->object().appearance->isVisitableFromTop())
 			else if(instance->object().appearance->isVisitableFromTop())
+			{
+				//Passable objects
 				m->areaForRoads().add(instance->getVisitablePosition());
 				m->areaForRoads().add(instance->getVisitablePosition());
-			else
+			}
+			else if(!instance->object().appearance->isVisitableFromTop())
 			{
 			{
-				m->areaIsolated().add(instance->getVisitablePosition() + int3(0, -1, 0));
+				// Do not route road behind visitable tile
+				int3 visitablePos = instance->getVisitablePosition();
+				auto areaVisitable = rmg::Area({visitablePos});
+				auto borderAbove = areaVisitable.getBorderOutside();
+				vstd::erase_if(borderAbove, [&](const int3 & tile)
+				{
+					return tile.y >= visitablePos.y ||
+					(!instance->object().blockingAt(tile + int3(0, 1, 0)) && 
+					instance->object().blockingAt(tile));
+				});				
+				m->areaIsolated().unite(borderAbove);
 			}
 			}
 		}
 		}
 
 
@@ -669,22 +731,20 @@ bool ObjectManager::addGuard(rmg::Object & object, si32 strength, bool zoneGuard
 		return false;
 		return false;
 	
 	
 	// Prefer non-blocking tiles, if any
 	// Prefer non-blocking tiles, if any
-	auto entrableTiles = object.getEntrableArea().getTiles();
-	int3 entrableTile(-1, -1, -1);
-	if (entrableTiles.empty())
+	auto entrableArea = object.getEntrableArea();
+	if (entrableArea.empty())
 	{
 	{
-		entrableTile = object.getVisitablePosition();
-	}
-	else
-	{
-		entrableTile = *RandomGeneratorUtil::nextItem(entrableTiles, zone.getRand());
+		entrableArea.add(object.getVisitablePosition());
 	}
 	}
 
 
-	rmg::Area visitablePos({entrableTile});
-	visitablePos.unite(visitablePos.getBorderOutside());
+	rmg::Area entrableBorder = entrableArea.getBorderOutside();
 	
 	
 	auto accessibleArea = object.getAccessibleArea();
 	auto accessibleArea = object.getAccessibleArea();
-	accessibleArea.intersect(visitablePos);
+	accessibleArea.erase_if([&](const int3 & tile)
+	{
+		return !entrableBorder.contains(tile);
+	});
+	
 	if(accessibleArea.empty())
 	if(accessibleArea.empty())
 	{
 	{
 		delete guard;
 		delete guard;

+ 3 - 1
lib/rmg/modificators/ObjectManager.h

@@ -48,7 +48,8 @@ public:
 	{
 	{
 		NONE = 0x00000000,
 		NONE = 0x00000000,
 		WEIGHT = 0x00000001,
 		WEIGHT = 0x00000001,
-		DISTANCE = 0x00000010
+		DISTANCE = 0x00000010,
+		BOTH = 0x00000011
 	};
 	};
 
 
 public:
 public:
@@ -61,6 +62,7 @@ public:
 	void addCloseObject(const RequiredObjectInfo & info);
 	void addCloseObject(const RequiredObjectInfo & info);
 	void addNearbyObject(const RequiredObjectInfo & info);
 	void addNearbyObject(const RequiredObjectInfo & info);
 
 
+	bool createMonoliths();
 	bool createRequiredObjects();
 	bool createRequiredObjects();
 
 
 	int3 findPlaceForObject(const rmg::Area & searchArea, rmg::Object & obj, si32 min_dist, OptimizeType optimizer) const;
 	int3 findPlaceForObject(const rmg::Area & searchArea, rmg::Object & obj, si32 min_dist, OptimizeType optimizer) const;

+ 2 - 2
lib/rmg/modificators/ObstaclePlacer.cpp

@@ -51,7 +51,7 @@ void ObstaclePlacer::process()
 		do
 		do
 		{
 		{
 			toBlock.clear();
 			toBlock.clear();
-			for (const auto& tile : zone.areaPossible().getTiles())
+			for (const auto& tile : zone.areaPossible().getTilesVector())
 			{
 			{
 				rmg::Area neighbors;
 				rmg::Area neighbors;
 				rmg::Area t;
 				rmg::Area t;
@@ -76,7 +76,7 @@ void ObstaclePlacer::process()
 				}
 				}
 			}
 			}
 			zone.areaPossible().subtract(toBlock);
 			zone.areaPossible().subtract(toBlock);
-			for (const auto& tile : toBlock.getTiles())
+			for (const auto& tile : toBlock.getTilesVector())
 			{
 			{
 				map.setOccupied(tile, ETileType::BLOCKED);
 				map.setOccupied(tile, ETileType::BLOCKED);
 			}
 			}

+ 92 - 68
lib/rmg/modificators/TreasurePlacer.cpp

@@ -584,7 +584,7 @@ std::vector<ObjectInfo*> TreasurePlacer::prepareTreasurePile(const CTreasureInfo
 	int maxValue = treasureInfo.max;
 	int maxValue = treasureInfo.max;
 	int minValue = treasureInfo.min;
 	int minValue = treasureInfo.min;
 	
 	
-	const ui32 desiredValue =zone.getRand().nextInt(minValue, maxValue);
+	const ui32 desiredValue = zone.getRand().nextInt(minValue, maxValue);
 	
 	
 	int currentValue = 0;
 	int currentValue = 0;
 	bool hasLargeObject = false;
 	bool hasLargeObject = false;
@@ -614,6 +614,13 @@ std::vector<ObjectInfo*> TreasurePlacer::prepareTreasurePile(const CTreasureInfo
 		oi->maxPerZone--;
 		oi->maxPerZone--;
 		
 		
 		currentValue += oi->value;
 		currentValue += oi->value;
+
+		if (currentValue >= minValue)
+		{
+			// 50% chance to end right here
+			if (zone.getRand().nextInt() & 1)
+				break;
+		}
 	}
 	}
 	
 	
 	return objectInfos;
 	return objectInfos;
@@ -626,30 +633,41 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector<ObjectInfo*>
 	{
 	{
 		auto blockedArea = rmgObject.getArea();
 		auto blockedArea = rmgObject.getArea();
 		auto entrableArea = rmgObject.getEntrableArea();
 		auto entrableArea = rmgObject.getEntrableArea();
+		auto accessibleArea = rmgObject.getAccessibleArea();
 		
 		
 		if(rmgObject.instances().empty())
 		if(rmgObject.instances().empty())
-			entrableArea.add(int3());
+		{
+			accessibleArea.add(int3());
+		}
 		
 		
 		auto * object = oi->generateObject();
 		auto * object = oi->generateObject();
 		if(oi->templates.empty())
 		if(oi->templates.empty())
 			continue;
 			continue;
 		
 		
-		object->appearance = *RandomGeneratorUtil::nextItem(oi->templates, zone.getRand());
+		auto templates = object->getObjectHandler()->getMostSpecificTemplates(zone.getTerrainType());
+
+		if (templates.empty())
+		{
+			throw rmgException(boost::str(boost::format("Did not find template for object (%d,%d) at %s") % object->ID % object->subID % zone.getTerrainType().encode(zone.getTerrainType())));
+		}
 
 
-		auto blockingIssue = object->isBlockedVisitable() && !object->isRemovable();
-		if (blockingIssue)
+		object->appearance = *RandomGeneratorUtil::nextItem(templates, zone.getRand());
+
+		//Put object in accessible area next to entrable area (excluding blockvis tiles)
+		if (!entrableArea.empty())
 		{
 		{
-			// Do not place next to another such object (Corpse issue)
-			// Calculate this before instance is added to rmgObject
-			auto blockVisitProximity = rmgObject.getBlockVisitableArea().getBorderOutside();
-			entrableArea.subtract(blockVisitProximity);
+			auto entrableBorder = entrableArea.getBorderOutside();
+			accessibleArea.erase_if([&](const int3 & tile)
+			{
+				return !entrableBorder.count(tile);
+			});
 		}
 		}
 
 
 		auto & instance = rmgObject.addInstance(*object);
 		auto & instance = rmgObject.addInstance(*object);
 
 
 		do
 		do
 		{
 		{
-			if(entrableArea.empty())
+			if(accessibleArea.empty())
 			{
 			{
 				//fail - fallback
 				//fail - fallback
 				rmgObject.clear();
 				rmgObject.clear();
@@ -657,15 +675,24 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector<ObjectInfo*>
 			}
 			}
 			
 			
 			std::vector<int3> bestPositions;
 			std::vector<int3> bestPositions;
-			if(densePlacement)
+			if(densePlacement && !entrableArea.empty())
 			{
 			{
+				// Choose positon which has access to as many entrable tiles as possible
 				int bestPositionsWeight = std::numeric_limits<int>::max();
 				int bestPositionsWeight = std::numeric_limits<int>::max();
-				for(const auto & t : entrableArea.getTilesVector())
+				for(const auto & t : accessibleArea.getTilesVector())
 				{
 				{
 					instance.setPosition(t);
 					instance.setPosition(t);
-					int w = rmgObject.getEntrableArea().getTilesVector().size();
 
 
-					if(w && w < bestPositionsWeight)
+					auto currentAccessibleArea = rmgObject.getAccessibleArea();
+					auto currentEntrableBorder = rmgObject.getEntrableArea().getBorderOutside();
+					currentAccessibleArea.erase_if([&](const int3 & tile)
+					{
+						return !currentEntrableBorder.count(tile);
+					});
+
+					size_t w = currentAccessibleArea.getTilesVector().size();
+
+					if(w > bestPositionsWeight)
 					{
 					{
 						// Minimum 1 position must be entrable
 						// Minimum 1 position must be entrable
 						bestPositions.clear();
 						bestPositions.clear();
@@ -677,12 +704,11 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector<ObjectInfo*>
 						bestPositions.push_back(t);
 						bestPositions.push_back(t);
 					}
 					}
 				}
 				}
-
 			}
 			}
 
 
 			if (bestPositions.empty())
 			if (bestPositions.empty())
 			{
 			{
-				bestPositions = entrableArea.getTilesVector();
+				bestPositions = accessibleArea.getTilesVector();
 			}
 			}
 			
 			
 			int3 nextPos = *RandomGeneratorUtil::nextItem(bestPositions, zone.getRand());
 			int3 nextPos = *RandomGeneratorUtil::nextItem(bestPositions, zone.getRand());
@@ -699,11 +725,11 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector<ObjectInfo*>
 			if(rmgObject.instances().size() == 1)
 			if(rmgObject.instances().size() == 1)
 				break;
 				break;
 
 
-			if(!blockedArea.overlap(instance.getBlockedArea()) && entrableArea.overlap(instanceAccessibleArea))
+			if(!blockedArea.overlap(instance.getBlockedArea()) && accessibleArea.overlap(instanceAccessibleArea))
 				break;
 				break;
 
 
 			//fail - new position
 			//fail - new position
-			entrableArea.erase(nextPos);
+			accessibleArea.erase(nextPos);
 		} while(true);
 		} while(true);
 	}
 	}
 	return rmgObject;
 	return rmgObject;
@@ -791,7 +817,7 @@ void TreasurePlacer::createTreasures(ObjectManager& manager)
 	size_t size = 0;
 	size_t size = 0;
 	{
 	{
 		Zone::Lock lock(zone.areaMutex);
 		Zone::Lock lock(zone.areaMutex);
-		size = zone.getArea().getTiles().size();
+		size = zone.getArea().getTilesVector().size();
 	}
 	}
 
 
 	int totalDensity = 0;
 	int totalDensity = 0;
@@ -808,16 +834,17 @@ void TreasurePlacer::createTreasures(ObjectManager& manager)
 
 
 		totalDensity += t->density;
 		totalDensity += t->density;
 
 
-		size_t count = size * t->density / 500;
+		const int DENSITY_CONSTANT = 300;
+		size_t count = (size * t->density) / DENSITY_CONSTANT;
 
 
 		//Assure space for lesser treasures, if there are any left
 		//Assure space for lesser treasures, if there are any left
+		const int averageValue = (t->min + t->max) / 2;
 		if (t != (treasureInfo.end() - 1))
 		if (t != (treasureInfo.end() - 1))
 		{
 		{
-			const int averageValue = (t->min + t->max) / 2;
 			if (averageValue > 10000)
 			if (averageValue > 10000)
 			{
 			{
 				//Will surely be guarded => larger piles => less space inbetween
 				//Will surely be guarded => larger piles => less space inbetween
-				vstd::amin(count, size * (10.f / 500) / (std::sqrt((float)averageValue / 10000)));
+				vstd::amin(count, size * (10.f / DENSITY_CONSTANT) / (std::sqrt((float)averageValue / 10000)));
 			}
 			}
 		}
 		}
 		
 		
@@ -837,7 +864,7 @@ void TreasurePlacer::createTreasures(ObjectManager& manager)
 			int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0, [](int v, const ObjectInfo* oi) {return v + oi->value; });
 			int value = std::accumulate(treasurePileInfos.begin(), treasurePileInfos.end(), 0, [](int v, const ObjectInfo* oi) {return v + oi->value; });
 
 
 			const ui32 maxPileGenerationAttemps = 2;
 			const ui32 maxPileGenerationAttemps = 2;
-			for (ui32 attempt = 0; attempt <= maxPileGenerationAttemps; attempt++)
+			for (ui32 attempt = 0; attempt < maxPileGenerationAttemps; attempt++)
 			{
 			{
 				auto rmgObject = constructTreasurePile(treasurePileInfos, attempt == maxAttempts);
 				auto rmgObject = constructTreasurePile(treasurePileInfos, attempt == maxAttempts);
 
 
@@ -865,61 +892,58 @@ void TreasurePlacer::createTreasures(ObjectManager& manager)
 		{
 		{
 			const bool guarded = rmgObject.isGuarded();
 			const bool guarded = rmgObject.isGuarded();
 
 
-			for (int attempt = 0; attempt <= maxAttempts;)
-			{
-				auto path = rmg::Path::invalid();
+			auto path = rmg::Path::invalid();
 
 
-				Zone::Lock lock(zone.areaMutex); //We are going to subtract this area
-				auto possibleArea = zone.areaPossible();
+			Zone::Lock lock(zone.areaMutex); //We are going to subtract this area
+			auto possibleArea = zone.areaPossible();
+			possibleArea.erase_if([this, &minDistance](const int3& tile) -> bool
+			{
+				auto ti = map.getTileInfo(tile);
+				return (ti.getNearestObjectDistance() < minDistance);
+			});
 
 
-				if (guarded)
-				{
-					path = manager.placeAndConnectObject(possibleArea, rmgObject, [this, &rmgObject, &minDistance, &manager](const int3& tile)
+			if (guarded)
+			{
+				path = manager.placeAndConnectObject(possibleArea, rmgObject, [this, &rmgObject, &minDistance, &manager](const int3& tile)
+					{
+						float bestDistance = 10e9;
+						for (const auto& t : rmgObject.getArea().getTilesVector())
 						{
 						{
-							auto ti = map.getTileInfo(tile);
-							if (ti.getNearestObjectDistance() < minDistance)
+							float distance = map.getTileInfo(t).getNearestObjectDistance();
+							if (distance < minDistance)
 								return -1.f;
 								return -1.f;
+							else
+								vstd::amin(bestDistance, distance);
+						}
 
 
-							for (const auto& t : rmgObject.getArea().getTilesVector())
-							{
-								if (map.getTileInfo(t).getNearestObjectDistance() < minDistance)
-									return -1.f;
-							}
+						const auto & guardedArea = rmgObject.instances().back()->getAccessibleArea();
+						const auto areaToBlock = rmgObject.getAccessibleArea(true) - guardedArea;
 
 
-							auto guardedArea = rmgObject.instances().back()->getAccessibleArea();
-							auto areaToBlock = rmgObject.getAccessibleArea(true);
-							areaToBlock.subtract(guardedArea);
-							if (areaToBlock.overlap(zone.freePaths()) || areaToBlock.overlap(manager.getVisitableArea()))
-								return -1.f;
+						if (zone.freePaths().overlap(areaToBlock) || manager.getVisitableArea().overlap(areaToBlock))
+							return -1.f;
 
 
-							return ti.getNearestObjectDistance();
-						}, guarded, false, ObjectManager::OptimizeType::DISTANCE);
-				}
-				else
-				{
-					path = manager.placeAndConnectObject(possibleArea, rmgObject, minDistance, guarded, false, ObjectManager::OptimizeType::DISTANCE);
-				}
+						return bestDistance;
+					}, guarded, false, ObjectManager::OptimizeType::BOTH);
+			}
+			else
+			{
+				path = manager.placeAndConnectObject(possibleArea, rmgObject, minDistance, guarded, false, ObjectManager::OptimizeType::DISTANCE);
+			}
+			lock.unlock();
 
 
-				if (path.valid())
-				{
-					//debug purposes
-					treasureArea.unite(rmgObject.getArea());
-					if (guarded)
-					{
-						guards.unite(rmgObject.instances().back()->getBlockedArea());
-						auto guardedArea = rmgObject.instances().back()->getAccessibleArea();
-						auto areaToBlock = rmgObject.getAccessibleArea(true);
-						areaToBlock.subtract(guardedArea);
-						treasureBlockArea.unite(areaToBlock);
-					}
-					zone.connectPath(path);
-					manager.placeObject(rmgObject, guarded, true);
-					break;
-				}
-				else
+			if (path.valid())
+			{
+				//debug purposes
+				treasureArea.unite(rmgObject.getArea());
+				if (guarded)
 				{
 				{
-					++attempt;
+					guards.unite(rmgObject.instances().back()->getBlockedArea());
+					auto guardedArea = rmgObject.instances().back()->getAccessibleArea();
+					auto areaToBlock = rmgObject.getAccessibleArea(true) - guardedArea;
+					treasureBlockArea.unite(areaToBlock);
 				}
 				}
+				zone.connectPath(path);
+				manager.placeObject(rmgObject, guarded, true);
 			}
 			}
 		}
 		}
 	}
 	}

+ 5 - 5
lib/rmg/modificators/WaterProxy.cpp

@@ -112,7 +112,7 @@ void WaterProxy::collectLakes()
 		for(const auto & t : lake.getBorderOutside())
 		for(const auto & t : lake.getBorderOutside())
 			if(map.isOnMap(t))
 			if(map.isOnMap(t))
 				lakes.back().neighbourZones[map.getZoneID(t)].add(t);
 				lakes.back().neighbourZones[map.getZoneID(t)].add(t);
-		for(const auto & t : lake.getTiles())
+		for(const auto & t : lake.getTilesVector())
 			lakeMap[t] = lakeId;
 			lakeMap[t] = lakeId;
 		
 		
 		//each lake must have at least one free tile
 		//each lake must have at least one free tile
@@ -143,7 +143,7 @@ RouteInfo WaterProxy::waterRoute(Zone & dst)
 		{
 		{
 			if(!lake.keepConnections.count(dst.getId()))
 			if(!lake.keepConnections.count(dst.getId()))
 			{
 			{
-				for(const auto & ct : lake.neighbourZones[dst.getId()].getTiles())
+				for(const auto & ct : lake.neighbourZones[dst.getId()].getTilesVector())
 				{
 				{
 					if(map.isPossible(ct))
 					if(map.isPossible(ct))
 						map.setOccupied(ct, ETileType::BLOCKED);
 						map.setOccupied(ct, ETileType::BLOCKED);
@@ -155,7 +155,7 @@ RouteInfo WaterProxy::waterRoute(Zone & dst)
 			}
 			}
 
 
 			//Don't place shipyard or boats on the very small lake
 			//Don't place shipyard or boats on the very small lake
-			if (lake.area.getTiles().size() < 25)
+			if (lake.area.getTilesVector().size() < 25)
 			{
 			{
 				logGlobal->info("Skipping very small lake at zone %d", dst.getId());
 				logGlobal->info("Skipping very small lake at zone %d", dst.getId());
 				continue;
 				continue;
@@ -273,7 +273,7 @@ bool WaterProxy::placeBoat(Zone & land, const Lake & lake, bool createRoad, Rout
 
 
 	while(!boardingPositions.empty())
 	while(!boardingPositions.empty())
 	{
 	{
-		auto boardingPosition = *boardingPositions.getTiles().begin();
+		auto boardingPosition = *boardingPositions.getTilesVector().begin();
 		rmg::Area shipPositions({boardingPosition});
 		rmg::Area shipPositions({boardingPosition});
 		auto boutside = shipPositions.getBorderOutside();
 		auto boutside = shipPositions.getBorderOutside();
 		shipPositions.assign(boutside);
 		shipPositions.assign(boutside);
@@ -336,7 +336,7 @@ bool WaterProxy::placeShipyard(Zone & land, const Lake & lake, si32 guard, bool
 	
 	
 	while(!boardingPositions.empty())
 	while(!boardingPositions.empty())
 	{
 	{
-		auto boardingPosition = *boardingPositions.getTiles().begin();
+		auto boardingPosition = *boardingPositions.getTilesVector().begin();
 		rmg::Area shipPositions({boardingPosition});
 		rmg::Area shipPositions({boardingPosition});
 		auto boutside = shipPositions.getBorderOutside();
 		auto boutside = shipPositions.getBorderOutside();
 		shipPositions.assign(boutside);
 		shipPositions.assign(boutside);

+ 13 - 13
lib/rmg/threadpool/MapProxy.cpp

@@ -14,47 +14,47 @@
 VCMI_LIB_NAMESPACE_BEGIN
 VCMI_LIB_NAMESPACE_BEGIN
 
 
 MapProxy::MapProxy(RmgMap & map):
 MapProxy::MapProxy(RmgMap & map):
-    map(map)
+	map(map)
 {
 {
 }
 }
 
 
 void MapProxy::insertObject(CGObjectInstance * obj)
 void MapProxy::insertObject(CGObjectInstance * obj)
 {
 {
-    Lock lock(mx);
-    map.getEditManager()->insertObject(obj);
+	Lock lock(mx);
+	map.getEditManager()->insertObject(obj);
 }
 }
 
 
 void MapProxy::insertObjects(std::set<CGObjectInstance*>& objects)
 void MapProxy::insertObjects(std::set<CGObjectInstance*>& objects)
 {
 {
-    Lock lock(mx);
-    map.getEditManager()->insertObjects(objects);
+	Lock lock(mx);
+	map.getEditManager()->insertObjects(objects);
 }
 }
 
 
 void MapProxy::removeObject(CGObjectInstance * obj)
 void MapProxy::removeObject(CGObjectInstance * obj)
 {
 {
-    Lock lock(mx);
-    map.getEditManager()->removeObject(obj);
+	Lock lock(mx);
+	map.getEditManager()->removeObject(obj);
 }
 }
 
 
 void MapProxy::drawTerrain(CRandomGenerator & generator, std::vector<int3> & tiles, TerrainId terrain)
 void MapProxy::drawTerrain(CRandomGenerator & generator, std::vector<int3> & tiles, TerrainId terrain)
 {
 {
-    Lock lock(mx);
+	Lock lock(mx);
 	map.getEditManager()->getTerrainSelection().setSelection(tiles);
 	map.getEditManager()->getTerrainSelection().setSelection(tiles);
-	map.getEditManager()->drawTerrain(terrain, &generator);
+	map.getEditManager()->drawTerrain(terrain, map.getDecorationsPercentage(), &generator);
 }
 }
 
 
 void MapProxy::drawRivers(CRandomGenerator & generator, std::vector<int3> & tiles, TerrainId terrain)
 void MapProxy::drawRivers(CRandomGenerator & generator, std::vector<int3> & tiles, TerrainId terrain)
 {
 {
-    Lock lock(mx);
+	Lock lock(mx);
 	map.getEditManager()->getTerrainSelection().setSelection(tiles);
 	map.getEditManager()->getTerrainSelection().setSelection(tiles);
 	map.getEditManager()->drawRiver(VLC->terrainTypeHandler->getById(terrain)->river, &generator);
 	map.getEditManager()->drawRiver(VLC->terrainTypeHandler->getById(terrain)->river, &generator);
 }
 }
 
 
 void MapProxy::drawRoads(CRandomGenerator & generator, std::vector<int3> & tiles, RoadId roadType)
 void MapProxy::drawRoads(CRandomGenerator & generator, std::vector<int3> & tiles, RoadId roadType)
 {
 {
-    Lock lock(mx);
-    map.getEditManager()->getTerrainSelection().setSelection(tiles);
+	Lock lock(mx);
+	map.getEditManager()->getTerrainSelection().setSelection(tiles);
 	map.getEditManager()->drawRoad(roadType, &generator);
 	map.getEditManager()->drawRoad(roadType, &generator);
 }
 }
 
 
-VCMI_LIB_NAMESPACE_END
+VCMI_LIB_NAMESPACE_END

+ 11 - 11
lib/rmg/threadpool/MapProxy.h

@@ -22,21 +22,21 @@ class RmgMap;
 class MapProxy
 class MapProxy
 {
 {
 public:
 public:
-    MapProxy(RmgMap & map);
+	MapProxy(RmgMap & map);
 
 
-    void insertObject(CGObjectInstance * obj);
-    void insertObjects(std::set<CGObjectInstance*>& objects);
-    void removeObject(CGObjectInstance* obj);
+	void insertObject(CGObjectInstance * obj);
+	void insertObjects(std::set<CGObjectInstance*>& objects);
+	void removeObject(CGObjectInstance* obj);
 
 
-    void drawTerrain(CRandomGenerator & generator, std::vector<int3> & tiles, TerrainId terrain);
-    void drawRivers(CRandomGenerator & generator, std::vector<int3> & tiles, TerrainId terrain);
-    void drawRoads(CRandomGenerator & generator, std::vector<int3> & tiles, RoadId roadType);
+	void drawTerrain(CRandomGenerator & generator, std::vector<int3> & tiles, TerrainId terrain);
+	void drawRivers(CRandomGenerator & generator, std::vector<int3> & tiles, TerrainId terrain);
+	void drawRoads(CRandomGenerator & generator, std::vector<int3> & tiles, RoadId roadType);
 
 
 private:
 private:
-    mutable boost::shared_mutex mx;
-    using Lock = boost::unique_lock<boost::shared_mutex>;
+	mutable boost::shared_mutex mx;
+	using Lock = boost::unique_lock<boost::shared_mutex>;
 
 
-    RmgMap & map;
+	RmgMap & map;
 };
 };
 
 
-VCMI_LIB_NAMESPACE_END
+VCMI_LIB_NAMESPACE_END

+ 3 - 1
mapeditor/mapcontroller.cpp

@@ -272,6 +272,8 @@ void MapController::resetMapHandler()
 
 
 void MapController::commitTerrainChange(int level, const TerrainId & terrain)
 void MapController::commitTerrainChange(int level, const TerrainId & terrain)
 {
 {
+	static const int terrainDecorationPercentageLevel = 10;
+
 	std::vector<int3> v(_scenes[level]->selectionTerrainView.selection().begin(),
 	std::vector<int3> v(_scenes[level]->selectionTerrainView.selection().begin(),
 						_scenes[level]->selectionTerrainView.selection().end());
 						_scenes[level]->selectionTerrainView.selection().end());
 	if(v.empty())
 	if(v.empty())
@@ -281,7 +283,7 @@ void MapController::commitTerrainChange(int level, const TerrainId & terrain)
 	_scenes[level]->selectionTerrainView.draw();
 	_scenes[level]->selectionTerrainView.draw();
 	
 	
 	_map->getEditManager()->getTerrainSelection().setSelection(v);
 	_map->getEditManager()->getTerrainSelection().setSelection(v);
-	_map->getEditManager()->drawTerrain(terrain, &CRandomGenerator::getDefault());
+	_map->getEditManager()->drawTerrain(terrain, terrainDecorationPercentageLevel, &CRandomGenerator::getDefault());
 	
 	
 	for(auto & t : v)
 	for(auto & t : v)
 		_scenes[level]->terrainView.setDirty(t);
 		_scenes[level]->terrainView.setDirty(t);

+ 32 - 32
mapeditor/translation/czech.ts

@@ -217,7 +217,7 @@
     <message>
     <message>
         <location filename="../mainwindow.ui" line="256"/>
         <location filename="../mainwindow.ui" line="256"/>
         <source>Map Objects View</source>
         <source>Map Objects View</source>
-        <translation type="unfinished"></translation>
+        <translation>Zobrazení objektů mapy</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.ui" line="300"/>
         <location filename="../mainwindow.ui" line="300"/>
@@ -227,7 +227,7 @@
     <message>
     <message>
         <location filename="../mainwindow.ui" line="378"/>
         <location filename="../mainwindow.ui" line="378"/>
         <source>Inspector</source>
         <source>Inspector</source>
-        <translation type="unfinished"></translation>
+        <translation>Inspektor</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.ui" line="420"/>
         <location filename="../mainwindow.ui" line="420"/>
@@ -348,7 +348,7 @@
     <message>
     <message>
         <location filename="../mainwindow.ui" line="1194"/>
         <location filename="../mainwindow.ui" line="1194"/>
         <source>Map title and description</source>
         <source>Map title and description</source>
-        <translation type="unfinished"></translation>
+        <translation>Název a popis mapy</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.ui" line="1205"/>
         <location filename="../mainwindow.ui" line="1205"/>
@@ -387,7 +387,7 @@
         <location filename="../mainwindow.cpp" line="1056"/>
         <location filename="../mainwindow.cpp" line="1056"/>
         <location filename="../mainwindow.cpp" line="1113"/>
         <location filename="../mainwindow.cpp" line="1113"/>
         <source>Update appearance</source>
         <source>Update appearance</source>
-        <translation type="unfinished"></translation>
+        <translation>Aktualizovat vzhled</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.ui" line="1300"/>
         <location filename="../mainwindow.ui" line="1300"/>
@@ -651,7 +651,7 @@
     <message>
     <message>
         <location filename="../mapsettings/mapsettings.ui" line="179"/>
         <location filename="../mapsettings/mapsettings.ui" line="179"/>
         <source>Abilities</source>
         <source>Abilities</source>
-        <translation type="unfinished"></translation>
+        <translation>Schopnosti</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mapsettings/mapsettings.ui" line="214"/>
         <location filename="../mapsettings/mapsettings.ui" line="214"/>
@@ -838,53 +838,53 @@
     <message>
     <message>
         <location filename="../inspector/heroskillswidget.cpp" line="19"/>
         <location filename="../inspector/heroskillswidget.cpp" line="19"/>
         <source>Beginner</source>
         <source>Beginner</source>
-        <translation type="unfinished"></translation>
+        <translation>Začátečník</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../inspector/heroskillswidget.cpp" line="20"/>
         <location filename="../inspector/heroskillswidget.cpp" line="20"/>
         <source>Advanced</source>
         <source>Advanced</source>
-        <translation type="unfinished"></translation>
+        <translation>Pokročilý</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../inspector/heroskillswidget.cpp" line="21"/>
         <location filename="../inspector/heroskillswidget.cpp" line="21"/>
         <source>Expert</source>
         <source>Expert</source>
-        <translation type="unfinished"></translation>
+        <translation>Expert</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../inspector/inspector.cpp" line="35"/>
         <location filename="../inspector/inspector.cpp" line="35"/>
         <source>Compliant</source>
         <source>Compliant</source>
-        <translation type="unfinished"></translation>
+        <translation>Ochotná</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../inspector/inspector.cpp" line="36"/>
         <location filename="../inspector/inspector.cpp" line="36"/>
         <source>Friendly</source>
         <source>Friendly</source>
-        <translation type="unfinished"></translation>
+        <translation>Přátelská</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../inspector/inspector.cpp" line="37"/>
         <location filename="../inspector/inspector.cpp" line="37"/>
         <source>Aggressive</source>
         <source>Aggressive</source>
-        <translation type="unfinished"></translation>
+        <translation>Agresivní</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../inspector/inspector.cpp" line="38"/>
         <location filename="../inspector/inspector.cpp" line="38"/>
         <source>Hostile</source>
         <source>Hostile</source>
-        <translation type="unfinished"></translation>
+        <translation>Nepřátelská</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../inspector/inspector.cpp" line="39"/>
         <location filename="../inspector/inspector.cpp" line="39"/>
         <source>Savage</source>
         <source>Savage</source>
-        <translation type="unfinished"></translation>
+        <translation>Brutální</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../inspector/inspector.cpp" line="478"/>
         <location filename="../inspector/inspector.cpp" line="478"/>
         <location filename="../inspector/inspector.cpp" line="845"/>
         <location filename="../inspector/inspector.cpp" line="845"/>
         <source>neutral</source>
         <source>neutral</source>
-        <translation type="unfinished"></translation>
+        <translation>neutrální</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../inspector/inspector.cpp" line="843"/>
         <location filename="../inspector/inspector.cpp" line="843"/>
         <source>UNFLAGGABLE</source>
         <source>UNFLAGGABLE</source>
-        <translation type="unfinished"></translation>
+        <translation>NEOZNAČITELNÝ</translation>
     </message>
     </message>
 </context>
 </context>
 <context>
 <context>
@@ -1101,7 +1101,7 @@
     <message>
     <message>
         <location filename="../inspector/rewardswidget.ui" line="214"/>
         <location filename="../inspector/rewardswidget.ui" line="214"/>
         <source>Message to be displayed on granting of this reward</source>
         <source>Message to be displayed on granting of this reward</source>
-        <translation type="unfinished"></translation>
+        <translation>Zobrazená zpráva při udělení odměny</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../inspector/rewardswidget.ui" line="225"/>
         <location filename="../inspector/rewardswidget.ui" line="225"/>
@@ -1482,32 +1482,32 @@
     <message>
     <message>
         <location filename="../validator.cpp" line="101"/>
         <location filename="../validator.cpp" line="101"/>
         <source>Object %1 is assigned to non-playable player %2</source>
         <source>Object %1 is assigned to non-playable player %2</source>
-        <translation type="unfinished"></translation>
+        <translation>Objekt %1 je přiřazen nehratelnému hráči %2</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="108"/>
         <location filename="../validator.cpp" line="108"/>
         <source>Town %1 has undefined owner %2</source>
         <source>Town %1 has undefined owner %2</source>
-        <translation type="unfinished"></translation>
+        <translation>Město %1 nemá definovaného vlastníka %2</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="118"/>
         <location filename="../validator.cpp" line="118"/>
         <source>Prison %1 must be a NEUTRAL</source>
         <source>Prison %1 must be a NEUTRAL</source>
-        <translation type="unfinished"></translation>
+        <translation>Vězení %1 musí být NEUTRÁLNÍ</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="124"/>
         <location filename="../validator.cpp" line="124"/>
         <source>Hero %1 must have an owner</source>
         <source>Hero %1 must have an owner</source>
-        <translation type="unfinished"></translation>
+        <translation>Hrdina %1 musí mít vlastníka</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="129"/>
         <location filename="../validator.cpp" line="129"/>
         <source>Hero %1 is prohibited by map settings</source>
         <source>Hero %1 is prohibited by map settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Hrdina %1 je zakázaný nastavením mapy</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="132"/>
         <location filename="../validator.cpp" line="132"/>
         <source>Hero %1 has duplicate on map</source>
         <source>Hero %1 has duplicate on map</source>
-        <translation type="unfinished"></translation>
+        <translation>Hrdina %1 má na mapě dvojníka</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="135"/>
         <location filename="../validator.cpp" line="135"/>
@@ -1517,7 +1517,7 @@
     <message>
     <message>
         <location filename="../validator.cpp" line="146"/>
         <location filename="../validator.cpp" line="146"/>
         <source>Spell scroll %1 is prohibited by map settings</source>
         <source>Spell scroll %1 is prohibited by map settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Kouzlo %1 je zakázáno nastavením mapy</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="149"/>
         <location filename="../validator.cpp" line="149"/>
@@ -1527,32 +1527,32 @@
     <message>
     <message>
         <location filename="../validator.cpp" line="155"/>
         <location filename="../validator.cpp" line="155"/>
         <source>Artifact %1 is prohibited by map settings</source>
         <source>Artifact %1 is prohibited by map settings</source>
-        <translation type="unfinished"></translation>
+        <translation>Artefakt %1 je zakázán nastavením mapy</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="164"/>
         <location filename="../validator.cpp" line="164"/>
         <source>Player %1 doesn&apos;t have any starting town</source>
         <source>Player %1 doesn&apos;t have any starting town</source>
-        <translation type="unfinished"></translation>
+        <translation>Hráč %1 nemá žádné počáteční město</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="168"/>
         <location filename="../validator.cpp" line="168"/>
         <source>Map name is not specified</source>
         <source>Map name is not specified</source>
-        <translation type="unfinished"></translation>
+        <translation>Mapa nemá název</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="170"/>
         <location filename="../validator.cpp" line="170"/>
         <source>Map description is not specified</source>
         <source>Map description is not specified</source>
-        <translation type="unfinished"></translation>
+        <translation>Mapa nemá popis</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="177"/>
         <location filename="../validator.cpp" line="177"/>
         <source>Map contains object from mod &quot;%1&quot;, but doesn&apos;t require it</source>
         <source>Map contains object from mod &quot;%1&quot;, but doesn&apos;t require it</source>
-        <translation type="unfinished"></translation>
+        <translation>Mapa obsahuje objekt z modifikace &quot;%1&quot;. ale nevyžaduje ji</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="183"/>
         <location filename="../validator.cpp" line="183"/>
         <source>Exception occurs during validation: %1</source>
         <source>Exception occurs during validation: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Při posudku nastala výjimka: %1</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../validator.cpp" line="187"/>
         <location filename="../validator.cpp" line="187"/>
@@ -1797,7 +1797,7 @@
     <message>
     <message>
         <location filename="../windownewmap.cpp" line="296"/>
         <location filename="../windownewmap.cpp" line="296"/>
         <source>RMG failure</source>
         <source>RMG failure</source>
-        <translation type="unfinished"></translation>
+        <translation>Chyba RMG</translation>
     </message>
     </message>
 </context>
 </context>
 <context>
 <context>
@@ -1815,12 +1815,12 @@
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="110"/>
         <location filename="../mainwindow.cpp" line="110"/>
         <source>From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG&apos;s.</source>
         <source>From an extracted archive, it Splits TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 and Un44 into individual PNG&apos;s.</source>
-        <translation type="unfinished"></translation>
+        <translation>Z rozbaleného archivu rozdělí TwCrPort, CPRSMALL, FlagPort, ITPA, ITPt, Un32 a Un44 do jednotlivých PNG.</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="111"/>
         <location filename="../mainwindow.cpp" line="111"/>
         <source>From an extracted archive, Converts single Images (found in Images folder) from .pcx to png.</source>
         <source>From an extracted archive, Converts single Images (found in Images folder) from .pcx to png.</source>
-        <translation type="unfinished"></translation>
+        <translation>Z rozbaleného archivu převede jednoduché obrázky (nalezené ve složce Images) z .pcx do png.</translation>
     </message>
     </message>
     <message>
     <message>
         <location filename="../mainwindow.cpp" line="112"/>
         <location filename="../mainwindow.cpp" line="112"/>

+ 8 - 5
server/CGameHandler.cpp

@@ -253,11 +253,12 @@ void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill)
 				break;
 				break;
 			case ECommander::HEALTH:
 			case ECommander::HEALTH:
 				scp.accumulatedBonus.type = BonusType::STACK_HEALTH;
 				scp.accumulatedBonus.type = BonusType::STACK_HEALTH;
-				scp.accumulatedBonus.valType = BonusValueType::PERCENT_TO_BASE;
+				scp.accumulatedBonus.valType = BonusValueType::PERCENT_TO_ALL; //TODO: check how it accumulates in original WoG with artifacts such as vial of life blood, elixir of life etc.
 				break;
 				break;
 			case ECommander::DAMAGE:
 			case ECommander::DAMAGE:
 				scp.accumulatedBonus.type = BonusType::CREATURE_DAMAGE;
 				scp.accumulatedBonus.type = BonusType::CREATURE_DAMAGE;
-				scp.accumulatedBonus.valType = BonusValueType::PERCENT_TO_BASE;
+				scp.accumulatedBonus.subtype = BonusCustomSubtype::creatureDamageBoth;
+				scp.accumulatedBonus.valType = BonusValueType::PERCENT_TO_ALL;
 				break;
 				break;
 			case ECommander::SPEED:
 			case ECommander::SPEED:
 				scp.accumulatedBonus.type = BonusType::STACKS_SPEED;
 				scp.accumulatedBonus.type = BonusType::STACKS_SPEED;
@@ -1973,7 +1974,9 @@ bool CGameHandler::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destA
 		{
 		{
 			const bool needsLastStack = armySrc->needsLastStack();
 			const bool needsLastStack = armySrc->needsLastStack();
 			const auto quantity = setSrc.getStackCount(srcSlot) - (needsLastStack ? 1 : 0);
 			const auto quantity = setSrc.getStackCount(srcSlot) - (needsLastStack ? 1 : 0);
-			moves.insert(std::make_pair(srcSlot, std::make_pair(slotToMove, quantity)));
+
+			if(quantity > 0) //0 may happen when we need last creature and we have exactly 1 amount of that creature - amount of "rest we can transfer" becomes 0
+				moves.insert(std::make_pair(srcSlot, std::make_pair(slotToMove, quantity)));
 		}
 		}
 	}
 	}
 	BulkRebalanceStacks bulkRS;
 	BulkRebalanceStacks bulkRS;
@@ -2223,12 +2226,12 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8
 
 
 bool CGameHandler::hasPlayerAt(PlayerColor player, std::shared_ptr<CConnection> c) const
 bool CGameHandler::hasPlayerAt(PlayerColor player, std::shared_ptr<CConnection> c) const
 {
 {
-	return connections.at(player).count(c);
+	return connections.count(player) && connections.at(player).count(c);
 }
 }
 
 
 bool CGameHandler::hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const
 bool CGameHandler::hasBothPlayersAtSameConnection(PlayerColor left, PlayerColor right) const
 {
 {
-	return connections.at(left) == connections.at(right);
+	return connections.count(left) && connections.count(right) && connections.at(left) == connections.at(right);
 }
 }
 
 
 bool CGameHandler::disbandCreature(ObjectInstanceID id, SlotID pos)
 bool CGameHandler::disbandCreature(ObjectInstanceID id, SlotID pos)

+ 5 - 5
server/NetPacksLobbyServer.cpp

@@ -189,11 +189,11 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyClientDisconnected(Lobb
 	}
 	}
 	srv.updateAndPropagateLobbyState();
 	srv.updateAndPropagateLobbyState();
 	
 	
-	if(srv.getState() != EServerState::SHUTDOWN && srv.remoteConnections.count(pack.c))
-	{
-		srv.remoteConnections -= pack.c;
-		srv.connectToRemote();
-	}
+//	if(srv.getState() != EServerState::SHUTDOWN && srv.remoteConnections.count(pack.c))
+//	{
+//		srv.remoteConnections -= pack.c;
+//		srv.connectToRemote();
+//	}
 }
 }
 
 
 void ClientPermissionsCheckerNetPackVisitor::visitLobbyChatMessage(LobbyChatMessage & pack)
 void ClientPermissionsCheckerNetPackVisitor::visitLobbyChatMessage(LobbyChatMessage & pack)

+ 18 - 11
server/TurnTimerHandler.cpp

@@ -34,6 +34,8 @@ void TurnTimerHandler::onGameplayStart(PlayerColor player)
 	{
 	{
 		timers[player] = si->turnTimerInfo;
 		timers[player] = si->turnTimerInfo;
 		timers[player].turnTimer = 0;
 		timers[player].turnTimer = 0;
+		timers[player].battleTimer = 0;
+		timers[player].unitTimer = 0;
 		timers[player].isActive = true;
 		timers[player].isActive = true;
 		timers[player].isBattle = false;
 		timers[player].isBattle = false;
 		lastUpdate[player] = std::numeric_limits<int>::max();
 		lastUpdate[player] = std::numeric_limits<int>::max();
@@ -103,11 +105,8 @@ bool TurnTimerHandler::timerCountDown(int & timer, int initialTimer, PlayerColor
 	{
 	{
 		timer -= waitTime;
 		timer -= waitTime;
 		lastUpdate[player] += waitTime;
 		lastUpdate[player] += waitTime;
-		int frequency = (timer > turnTimePropagateThreshold
-						 && initialTimer - timer > turnTimePropagateThreshold)
-		? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit;
 		
 		
-		if(lastUpdate[player] >= frequency)
+		if(lastUpdate[player] >= turnTimePropagateFrequency)
 			sendTimerUpdate(player);
 			sendTimerUpdate(player);
 
 
 		return true;
 		return true;
@@ -127,6 +126,10 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime)
 	const auto * state = gameHandler.getPlayerState(player);
 	const auto * state = gameHandler.getPlayerState(player);
 	if(state && state->human && timer.isActive && !timer.isBattle && state->status == EPlayerStatus::INGAME)
 	if(state && state->human && timer.isActive && !timer.isBattle && state->status == EPlayerStatus::INGAME)
 	{
 	{
+		// turn timers are only used if turn timer is non-zero
+		if (si->turnTimerInfo.turnTimer == 0)
+			return;
+
 		if(timerCountDown(timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime))
 		if(timerCountDown(timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime))
 			return;
 			return;
 
 
@@ -277,17 +280,21 @@ void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime)
 	auto & timer = timers[player];
 	auto & timer = timers[player];
 	if(timer.isActive && timer.isBattle)
 	if(timer.isActive && timer.isBattle)
 	{
 	{
-		 if (timerCountDown(timer.unitTimer, si->turnTimerInfo.unitTimer, player, waitTime))
+		// in pvp battles, timers are only used if unit timer is non-zero
+		if(isPvpBattle(battleID) && si->turnTimerInfo.unitTimer == 0)
 			return;
 			return;
 
 
-		 if (timerCountDown(timer.battleTimer, si->turnTimerInfo.battleTimer, player, waitTime))
-			 return;
+		if (timerCountDown(timer.unitTimer, si->turnTimerInfo.unitTimer, player, waitTime))
+			return;
 
 
-		 if (timerCountDown(timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime))
-			 return;
+		if (timerCountDown(timer.battleTimer, si->turnTimerInfo.battleTimer, player, waitTime))
+			return;
 
 
-		 if (timerCountDown(timer.baseTimer, si->turnTimerInfo.baseTimer, player, waitTime))
-			 return;
+		if (timerCountDown(timer.turnTimer, si->turnTimerInfo.turnTimer, player, waitTime))
+			return;
+
+		if (timerCountDown(timer.baseTimer, si->turnTimerInfo.baseTimer, player, waitTime))
+			return;
 
 
 		if(isPvpBattle(battleID))
 		if(isPvpBattle(battleID))
 		{
 		{

+ 1 - 3
server/TurnTimerHandler.h

@@ -25,9 +25,7 @@ class CGameHandler;
 class TurnTimerHandler
 class TurnTimerHandler
 {	
 {	
 	CGameHandler & gameHandler;
 	CGameHandler & gameHandler;
-	const int turnTimePropagateFrequency = 5000;
-	const int turnTimePropagateFrequencyCrit = 1000;
-	const int turnTimePropagateThreshold = 3000;
+	const int turnTimePropagateFrequency = 1000;
 	std::map<PlayerColor, TurnTimerInfo> timers;
 	std::map<PlayerColor, TurnTimerInfo> timers;
 	std::map<PlayerColor, int> lastUpdate;
 	std::map<PlayerColor, int> lastUpdate;
 	std::map<PlayerColor, bool> endTurnAllowed;
 	std::map<PlayerColor, bool> endTurnAllowed;

+ 39 - 15
server/battles/BattleActionProcessor.cpp

@@ -411,24 +411,48 @@ bool BattleActionProcessor::doUnitSpellAction(const CBattleInfoCallback & battle
 	std::shared_ptr<const Bonus> randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER));
 	std::shared_ptr<const Bonus> randSpellcaster = stack->getBonus(Selector::type()(BonusType::RANDOM_SPELLCASTER));
 	std::shared_ptr<const Bonus> spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, BonusSubtypeID(spellID)));
 	std::shared_ptr<const Bonus> spellcaster = stack->getBonus(Selector::typeSubtype(BonusType::SPELLCASTER, BonusSubtypeID(spellID)));
 
 
-	//TODO special bonus for genies ability
-	if (randSpellcaster && battle.battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_AIMED) == SpellID::NONE)
-		spellID = battle.battleGetRandomStackSpell(gameHandler->getRandomGenerator(), stack, CBattleInfoCallback::RANDOM_GENIE);
-
-	if (spellID == SpellID::NONE)
+	if (!spellcaster && !randSpellcaster)
+	{
 		gameHandler->complain("That stack can't cast spells!");
 		gameHandler->complain("That stack can't cast spells!");
-	else
+		return false;
+	}
+
+	if (randSpellcaster)
 	{
 	{
-		const CSpell * spell = SpellID(spellID).toSpell();
-		spells::BattleCast parameters(&battle, stack, spells::Mode::CREATURE_ACTIVE, spell);
-		int32_t spellLvl = 0;
-		if(spellcaster)
-			vstd::amax(spellLvl, spellcaster->val);
-		if(randSpellcaster)
-			vstd::amax(spellLvl, randSpellcaster->val);
-		parameters.setSpellLevel(spellLvl);
-		parameters.cast(gameHandler->spellEnv, target);
+		if (target.size() != 1)
+		{
+			gameHandler->complain("Invalid target for random spellcaster!");
+			return false;
+		}
+
+		const battle::Unit * subject = target[0].unitValue;
+		if (target[0].unitValue == nullptr)
+			subject = battle.battleGetStackByPos(target[0].hexValue, true);
+
+		if (subject == nullptr)
+		{
+			gameHandler->complain("Invalid target for random spellcaster!");
+			return false;
+		}
+
+		spellID = battle.getRandomBeneficialSpell(gameHandler->getRandomGenerator(), stack, subject);
+
+		if (spellID == SpellID::NONE)
+		{
+			gameHandler->complain("That stack can't cast spells!");
+			return false;
+		}
 	}
 	}
+
+	const CSpell * spell = SpellID(spellID).toSpell();
+	spells::BattleCast parameters(&battle, stack, spells::Mode::CREATURE_ACTIVE, spell);
+	int32_t spellLvl = 0;
+	if(spellcaster)
+		vstd::amax(spellLvl, spellcaster->val);
+	if(randSpellcaster)
+		vstd::amax(spellLvl, randSpellcaster->val);
+	parameters.setSpellLevel(spellLvl);
+	parameters.cast(gameHandler->spellEnv, target);
 	return true;
 	return true;
 }
 }
 
 

+ 1 - 1
server/processors/PlayerMessageProcessor.cpp

@@ -384,7 +384,7 @@ void PlayerMessageProcessor::cheatPuzzleReveal(PlayerColor player)
 
 
 	for(auto & obj : gameHandler->gameState()->map->objects)
 	for(auto & obj : gameHandler->gameState()->map->objects)
 	{
 	{
-		if(obj->ID == Obj::OBELISK)
+		if(obj && obj->ID == Obj::OBELISK)
 		{
 		{
 			gameHandler->setObjPropertyID(obj->id, ObjProperty::OBELISK_VISITED, t->id);
 			gameHandler->setObjPropertyID(obj->id, ObjProperty::OBELISK_VISITED, t->id);
 			for(const auto & color : t->players)
 			for(const auto & color : t->players)

+ 37 - 5
server/processors/TurnOrderProcessor.cpp

@@ -9,6 +9,7 @@
  */
  */
 #include "StdInc.h"
 #include "StdInc.h"
 #include "TurnOrderProcessor.h"
 #include "TurnOrderProcessor.h"
+#include "PlayerMessageProcessor.h"
 
 
 #include "../queries/QueriesProcessor.h"
 #include "../queries/QueriesProcessor.h"
 #include "../queries/MapQueries.h"
 #include "../queries/MapQueries.h"
@@ -35,9 +36,9 @@ int TurnOrderProcessor::simturnsTurnsMinLimit() const
 	return gameHandler->getStartInfo()->simturnsInfo.requiredTurns;
 	return gameHandler->getStartInfo()->simturnsInfo.requiredTurns;
 }
 }
 
 
-void TurnOrderProcessor::updateContactStatus()
+std::vector<TurnOrderProcessor::PlayerPair> TurnOrderProcessor::computeContactStatus() const
 {
 {
-	blockedContacts.clear();
+	std::vector<PlayerPair> result;
 
 
 	assert(actedPlayers.empty());
 	assert(actedPlayers.empty());
 	assert(actingPlayers.empty());
 	assert(actingPlayers.empty());
@@ -50,9 +51,40 @@ void TurnOrderProcessor::updateContactStatus()
 				continue;
 				continue;
 
 
 			if (computeCanActSimultaneously(left, right))
 			if (computeCanActSimultaneously(left, right))
-				blockedContacts.push_back({left, right});
+				result.push_back({left, right});
 		}
 		}
 	}
 	}
+	return result;
+}
+
+void TurnOrderProcessor::updateAndNotifyContactStatus()
+{
+	auto newBlockedContacts = computeContactStatus();
+
+	if (newBlockedContacts.empty())
+	{
+		// Simturns between all players have ended - send single global notification
+		if (!blockedContacts.empty())
+			gameHandler->playerMessages->broadcastSystemMessage("Simultaneous turns have ended");
+	}
+	else
+	{
+		// Simturns between some players have ended - notify each pair
+		for (auto const & contact : blockedContacts)
+		{
+			if (vstd::contains(newBlockedContacts, contact))
+				continue;
+
+			MetaString message;
+			message.appendRawString("Simultaneous turns between players %s and %s have ended"); // FIXME: we should send MetaString itself and localize it on client side
+			message.replaceName(contact.a);
+			message.replaceName(contact.b);
+
+			gameHandler->playerMessages->broadcastSystemMessage(message.toString());
+		}
+	}
+
+	blockedContacts = newBlockedContacts;
 }
 }
 
 
 bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) const
 bool TurnOrderProcessor::playersInContact(PlayerColor left, PlayerColor right) const
@@ -204,7 +236,7 @@ void TurnOrderProcessor::doStartNewDay()
 	std::swap(actedPlayers, awaitingPlayers);
 	std::swap(actedPlayers, awaitingPlayers);
 
 
 	gameHandler->onNewTurn();
 	gameHandler->onNewTurn();
-	updateContactStatus();
+	updateAndNotifyContactStatus();
 	tryStartTurnsForPlayers();
 	tryStartTurnsForPlayers();
 }
 }
 
 
@@ -301,7 +333,7 @@ bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which)
 void TurnOrderProcessor::onGameStarted()
 void TurnOrderProcessor::onGameStarted()
 {
 {
 	if (actingPlayers.empty())
 	if (actingPlayers.empty())
-		updateContactStatus();
+		blockedContacts = computeContactStatus();
 
 
 	// this may be game load - send notification to players that they can act
 	// this may be game load - send notification to players that they can act
 	auto actingPlayersCopy = actingPlayers;
 	auto actingPlayersCopy = actingPlayers;

部分文件因为文件数量过多而无法显示