浏览代码

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)
 {
 	//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();
 

+ 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
 
 ### General

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

@@ -54,6 +54,8 @@
 	"vcmi.radialWheel.moveDown" : "Move down",
 	"vcmi.radialWheel.moveBottom" : "Move to bottom",
 
+	"vcmi.spellBook.search" : "hledat...",
+
 	"vcmi.mainMenu.serverConnecting" : "Připojování...",
 	"vcmi.mainMenu.serverAddressEnter" : "Zadejte adresu:",
 	"vcmi.mainMenu.serverConnectionFailed" : "Připojování selhalo",
@@ -69,6 +71,7 @@
 	"vcmi.lobby.noPreview" : "bez náhledu",
 	"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.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}",
@@ -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.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.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.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.audioGroup" : "Nastavení zvuku",
@@ -107,28 +110,32 @@
 	"vcmi.systemOptions.longTouchMenu.help"      : "Změnit dobu dlouhého podržení.",
 	"vcmi.systemOptions.longTouchMenu.entry"     : "%d milisekund",
 	"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.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.help"   : "{Vylepšení rozhraní}\n\nZapne různá vylepšení rozhraní, jako je tlačítko batohu atd. Zakažte pro zážitek klasické hry.",
 	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "Velká kniha kouzel",
 	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Velká kniha kouzel}\n\nPovolí větší knihu kouzel, do které se jich více vleze na jednu stranu. Animace změny stránek s tímto nastavením nefunguje.",
+	"vcmi.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.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.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.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.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.help" : "{Posouvání okraji}\n\nPosouvat mapu světa, když je kurzor na okraji obrazovky. Může být zakázáno držením klávesy CTRL.",
 	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info Panel Creature Management", //TODO
-	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info Panel Creature Management}\n\nAllows rearranging creatures in info panel instead of cycling between default components",
+	"vcmi.adventureOptions.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.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.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
@@ -141,16 +148,16 @@
 	"vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO",
 	"vcmi.battleOptions.queueSizeSmallButton.hover": "MALÁ",
 	"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.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.animationsSpeed5.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.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ů",
@@ -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.skipBattleIntroMusic.hover": "Přeskočit úvodní hudbu",
 	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Přeskočit úvodní hudbu}\n\nPovolí akce při úvodní hudbě přehrávané při začátku každé bitvy.",
+
 	"vcmi.adventureMap.revisitObject.hover" : "Znovu navštívit místo",
 	"vcmi.adventureMap.revisitObject.help" : "{Znovu navštívit místo}\n\nPokud se hrdina nachází na nějakém místě mapy, může jej znovu navštívit.",
 
@@ -211,26 +219,26 @@
 	"vcmi.logicalExpressions.noneOf" : "Žádné z následujících:",
 
 	"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.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.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.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.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.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.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.optionsTab.turnOptions.hover" : "Možnosti tahu",
@@ -251,37 +259,37 @@
 	"vcmi.optionsTab.accumulate" : "Accumulate",
 
 	"vcmi.optionsTab.simturnsTitle" : "Souběžné tahy",
-	"vcmi.optionsTab.simturnsMin.hover" : "At least for",
-	"vcmi.optionsTab.simturnsMax.hover" : "At most for",
-	"vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Simultaneous AI Turns",
-	"vcmi.optionsTab.simturnsMin.help" : "Play simultaneously for specified number of days. Contacts between players during this period are blocked",
-	"vcmi.optionsTab.simturnsMax.help" : "Play simultaneously for specified number of days or until contact with another player",
-	"vcmi.optionsTab.simturnsAI.help" : "{Simultaneous AI Turns}\nExperimental option. Allows AI players to act at the same time as human player when simultaneous turns are enabled.",
-
-	"vcmi.optionsTab.turnTime.select"     : "Select turn timer preset",
-	"vcmi.optionsTab.turnTime.unlimited"  : "Unlimited turn time",
-	"vcmi.optionsTab.turnTime.classic.1"  : "Classic timer: 1 minute",
-	"vcmi.optionsTab.turnTime.classic.2"  : "Classic timer: 2 minutes",
-	"vcmi.optionsTab.turnTime.classic.5"  : "Classic timer: 5 minutes",
-	"vcmi.optionsTab.turnTime.classic.10" : "Classic timer: 10 minutes",
-	"vcmi.optionsTab.turnTime.classic.20" : "Classic timer: 20 minutes",
-	"vcmi.optionsTab.turnTime.classic.30" : "Classic timer: 30 minutes",
-	"vcmi.optionsTab.turnTime.chess.20"   : "Chess: 20:00 + 10:00 + 02:00 + 00:00",
-	"vcmi.optionsTab.turnTime.chess.16"   : "Chess: 16:00 + 08:00 + 01:30 + 00:00",
-	"vcmi.optionsTab.turnTime.chess.8"    : "Chess: 08:00 + 04:00 + 01:00 + 00:00",
-	"vcmi.optionsTab.turnTime.chess.4"    : "Chess: 04:00 + 02:00 + 00:30 + 00:00",
-	"vcmi.optionsTab.turnTime.chess.2"    : "Chess: 02:00 + 01:00 + 00:15 + 00:00",
-	"vcmi.optionsTab.turnTime.chess.1"    : "Chess: 01:00 + 01:00 + 00:00 + 00:00",
-
-	"vcmi.optionsTab.simturns.select"         : "Select simultaneous turns preset",
-	"vcmi.optionsTab.simturns.none"           : "No simultaneous turns",
-	"vcmi.optionsTab.simturns.tillContactMax" : "Simturns: Until contact",
-	"vcmi.optionsTab.simturns.tillContact1"   : "Simturns: 1 week, break on contact",
-	"vcmi.optionsTab.simturns.tillContact2"   : "Simturns: 2 weeks, break on contact",
-	"vcmi.optionsTab.simturns.tillContact4"   : "Simturns: 1 month, break on contact",
-	"vcmi.optionsTab.simturns.blocked1"       : "Simturns: 1 week, contacts blocked",
-	"vcmi.optionsTab.simturns.blocked2"       : "Simturns: 2 weeks, contacts blocked",
-	"vcmi.optionsTab.simturns.blocked4"       : "Simturns: 1 month, contacts blocked",
+	"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
 	// 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",
 
 	// 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.1" : "Učeň",
 	"vcmi.stackExperience.rank.2" : "Trénovaný",
@@ -315,7 +323,7 @@
 	"vcmi.stackExperience.rank.6" : "Adept",
 	"vcmi.stackExperience.rank.7" : "Expert",
 	"vcmi.stackExperience.rank.8" : "Elitní",
-	"vcmi.stackExperience.rank.9" : "Master",
+	"vcmi.stackExperience.rank.9" : "Mistr",
 	"vcmi.stackExperience.rank.10" : "Eso",
 	
 	"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.LIFE_DRAIN.name": "Vysátí životů (${val}%)",
 	"core.bonus.LIFE_DRAIN.description": "Vysaje ${val}% uděleného poškození",
-	"core.bonus.MANA_CHANNELING.name": "Magic Channel ${val}%", // 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_DRAIN.name": "Vysátí 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.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.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.mapScrollSpeed5.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.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.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.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.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.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.mapScrollSpeed5.hover": "",
 	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",

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

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

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

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

+ 4 - 0
client/NetPacksLobbyClient.cpp

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

+ 8 - 6
client/PlayerLocalState.cpp

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

+ 2 - 2
client/PlayerLocalState.h

@@ -66,14 +66,14 @@ public:
 	const CGTownInstance * getOwnedTown(size_t index);
 	void addOwnedTown(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 CGHeroInstance * getWanderingHero(size_t index);
 	const CGHeroInstance * getNextWanderingHero(const CGHeroInstance * hero);
 	void addWanderingHero(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);
 	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);
 	widget->getMapView()->onViewMapActivated();
 
-	if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.isEnabled() || LOCPLINT->cb->getStartInfo()->turnTimerInfo.isBattleEnabled())
-		watches = std::make_shared<TurnTimerWidget>();
+	if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.turnTimer != 0)
+		watches = std::make_shared<TurnTimerWidget>(Point(24, 24));
 	
 	addUsedEvents(KEYBOARD | TIME);
 }
@@ -331,6 +331,11 @@ void AdventureMapInterface::onTownOrderChanged()
 	widget->getTownList()->updateWidget();
 }
 
+void AdventureMapInterface::onHeroOrderChanged()
+{
+	widget->getHeroList()->updateWidget();
+}
+
 void AdventureMapInterface::onMapTilesChanged(boost::optional<std::unordered_set<int3>> positions)
 {
 	if (positions)

+ 3 - 0
client/adventureMap/AdventureMapInterface.h

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

+ 27 - 21
client/adventureMap/CList.cpp

@@ -279,14 +279,14 @@ void CHeroList::CHeroItem::gesture(bool on, const Point & initialPosition, const
 	if(heroes.size() < 2)
 		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 = {
 		{ 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);
 		} },
 		{ 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()
 {
-	auto & heroes = LOCPLINT->localState->getWanderingHeroes();
+	const auto & heroes = LOCPLINT->localState->getWanderingHeroes();
 
 	listBox->resize(heroes.size());
 
@@ -337,7 +337,7 @@ void CHeroList::updateWidget()
 		if (!item)
 			continue;
 
-		if (item->hero == heroes[i])
+		if (item->hero == heroes.at(i))
 		{
 			item->update();
 		}
@@ -362,11 +362,9 @@ std::shared_ptr<CIntObject> CTownList::createItem(size_t index)
 }
 
 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);
 	picture = std::make_shared<CAnimImage>(AnimationPath::builtin("ITPA"), 0);
 	pos = picture->pos;
@@ -382,7 +380,6 @@ std::shared_ptr<CIntObject> CTownList::CTownItem::genSelection()
 
 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)];
 
 	picture->setFrame(iconIndex + 2);
@@ -392,17 +389,17 @@ void CTownList::CTownItem::update()
 void CTownList::CTownItem::select(bool on)
 {
 	if(on)
-		LOCPLINT->localState->setSelection(LOCPLINT->localState->getOwnedTowns()[townIndex]);
+		LOCPLINT->localState->setSelection(town);
 }
 
 void CTownList::CTownItem::open()
 {
-	LOCPLINT->openTownWindow(LOCPLINT->localState->getOwnedTowns()[townIndex]);
+	LOCPLINT->openTownWindow(town);
 }
 
 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)
@@ -411,8 +408,9 @@ void CTownList::CTownItem::gesture(bool on, const Point & initialPosition, const
 		return;
 
 	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;
 
 	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;
 
 	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--)
 				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++)
 				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()
 {
-	return LOCPLINT->localState->getOwnedTowns()[townIndex]->getObjectName();
+	return town->getObjectName();
 }
 
 CTownList::CTownList(int visibleItemsCount, Rect widgetPosition, Point firstItemOffset, Point itemOffsetDelta, size_t initialItemsCount)
@@ -473,7 +471,15 @@ void CTownList::updateWidget()
 		if (!item)
 			continue;
 
-		listBox->reset();
+		if (item->town == towns[i])
+		{
+			item->update();
+		}
+		else
+		{
+			listBox->reset();
+			break;
+		}
 	}
 
 	if (LOCPLINT->localState->getCurrentTown())

+ 1 - 1
client/adventureMap/CList.h

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

+ 140 - 111
client/adventureMap/TurnTimerWidget.cpp

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

+ 23 - 30
client/adventureMap/TurnTimerWidget.h

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

+ 3 - 3
client/battle/BattleActionsController.cpp

@@ -616,8 +616,8 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
 			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;
 
@@ -887,7 +887,7 @@ void BattleActionsController::tryActivateStackSpellcasting(const CStack *casterS
 	{
 		// faerie dragon can cast only one, randomly selected spell until their next move
 		//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)
 			creatureSpells.push_back(spellToCast);

+ 2 - 0
client/battle/BattleInterface.cpp

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

+ 1 - 0
client/battle/BattleInterface.h

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

+ 22 - 2
client/battle/BattleInterfaceClasses.cpp

@@ -34,6 +34,7 @@
 #include "../widgets/Buttons.h"
 #include "../widgets/Images.h"
 #include "../widgets/TextControls.h"
+#include "../widgets/MiscWidgets.h"
 #include "../windows/CMessage.h"
 #include "../windows/CSpellWindow.h"
 #include "../render/CAnimation.h"
@@ -787,11 +788,16 @@ void StackQueue::update()
 	owner.getBattle()->battleGetTurnOrder(queueData, stackBoxes.size(), 0);
 
 	size_t boxIndex = 0;
+	ui32 tmpTurn = -1;
 
 	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++)
-			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++)
@@ -829,11 +835,14 @@ StackQueue::StackBox::StackBox(StackQueue * owner):
 	{
 		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);
+		roundRect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, 2, 48), ColorRGBA(0, 0, 0, 255), ColorRGBA(0, 255, 0, 255));
 	}
 	else
 	{
 		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);
+		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_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->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)
 	{
@@ -861,6 +871,16 @@ void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn)
 			icon->setFrame(owner->getSiegeShooterIconID(), 1);
 
 		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)
 		{

+ 4 - 2
client/battle/BattleInterfaceClasses.h

@@ -39,6 +39,7 @@ class CToggleButton;
 class CLabel;
 class CTextBox;
 class CAnimImage;
+class TransparentFilledRectangle;
 class CPlayerInterface;
 class BattleRenderer;
 
@@ -206,6 +207,8 @@ class StackQueue : public CIntObject
 		std::shared_ptr<CAnimImage> icon;
 		std::shared_ptr<CLabel> amount;
 		std::shared_ptr<CAnimImage> stateIcon;
+		std::shared_ptr<CLabel> round;
+		std::shared_ptr<TransparentFilledRectangle> roundRect;
 
 		void show(Canvas & to) override;
 		void showAll(Canvas & to) override;
@@ -213,9 +216,8 @@ class StackQueue : public CIntObject
 		bool isBoundUnitHighlighted() const;
 	public:
 		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;
-
 	};
 
 	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)
 {
+	// if timer run out and we did not act in time - deactivate current stack
+	setActiveStack(nullptr);
 	removeExpiredColorFilters();
 }
 

+ 43 - 4
client/battle/BattleWindow.cpp

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

+ 5 - 0
client/battle/BattleWindow.h

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

+ 2 - 4
client/lobby/CBonusSelection.cpp

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

+ 2 - 0
client/lobby/CLobbyScreen.cpp

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

+ 6 - 3
client/lobby/CSelectionBase.cpp

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

+ 2 - 0
client/lobby/CSelectionBase.h

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

+ 5 - 1
client/lobby/RandomMapTab.cpp

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

+ 3 - 0
client/mapView/MapViewController.cpp

@@ -269,6 +269,9 @@ void MapViewController::afterRender()
 
 bool MapViewController::isEventInstant(const CGObjectInstance * obj, const PlayerColor & initiator)
 {
+	if(settings["gameTweaks"]["skipAdventureMapAnimations"].Bool())
+		return true;
+
 	if (!isEventVisible(obj, initiator))
 		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)
-	: CIntObject(LCLICK | HOVER),
+	: CIntObject(LCLICK | SHOW_POPUP | HOVER),
 	hero(hero),
-	clickFunctor(nullptr)
+	clickFunctor(nullptr),
+	clickRFunctor(nullptr)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
@@ -147,12 +148,23 @@ void CHeroArea::addClickCallback(ClickFunctor callback)
 	clickFunctor = callback;
 }
 
+void CHeroArea::addRClickCallback(ClickFunctor callback)
+{
+	clickRFunctor = callback;
+}
+
 void CHeroArea::clickPressed(const Point & cursorPosition)
 {
 	if(clickFunctor)
 		clickFunctor();
 }
 
+void CHeroArea::showPopupWindow(const Point & cursorPosition)
+{
+	if(clickRFunctor)
+		clickRFunctor();
+}
+
 void CHeroArea::hover(bool on)
 {
 	if (on && hero)

+ 3 - 0
client/widgets/MiscWidgets.h

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

+ 6 - 3
client/widgets/TextControls.cpp

@@ -375,7 +375,7 @@ void CTextBox::setText(const std::string & text)
 	else if(slider)
 	{
 		// decrease width again if slider still used
-		label->pos.w = pos.w - 32;
+		label->pos.w = pos.w - 16;
 		assert(label->pos.w > 0);
 		label->setText(text);
 		slider->setAmount(label->textSize.y);
@@ -383,12 +383,12 @@ void CTextBox::setText(const std::string & text)
 	else if(label->textSize.y > label->pos.h)
 	{
 		// create slider and update widget
-		label->pos.w = pos.w - 32;
+		label->pos.w = pos.w - 16;
 		assert(label->pos.w > 0);
 		label->setText(text);
 
 		OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
-		slider = std::make_shared<CSlider>(Point(pos.w - 32, 0), pos.h, std::bind(&CTextBox::sliderMoved, this, _1),
+		slider = std::make_shared<CSlider>(Point(pos.w - 16, 0), pos.h, std::bind(&CTextBox::sliderMoved, this, _1),
 			label->pos.h, label->textSize.y, 0, Orientation::VERTICAL, CSlider::EStyle(sliderStyle));
 		slider->setScrollStep((int)graphics->fonts[label->font]->getLineHeight());
 		slider->setPanningStep(1);
@@ -801,6 +801,9 @@ void CFocusable::moveFocus()
 		if(i == focusables.end())
 			i = focusables.begin();
 
+		if (*i == this)
+			return;
+
 		if((*i)->isActive())
 		{
 			(*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));
 	}
 
-	portrait = std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsLarge"), hero->getIconIndex(), 0, 170, 66);
+	portrait = std::make_shared<CHeroArea>(170, 66, hero);
+	portrait->addClickCallback(nullptr);
+	portrait->addRClickCallback([hero](){ GH.windows().createAndPushWindow<CRClickPopupInt>(std::make_shared<CHeroWindow>(hero)); });
 	ok = std::make_shared<CButton>(Point(297, 413), AnimationPath::builtin("IOKAY"), CButton::tooltip(), std::bind(&CLevelWindow::close, this), EShortcut::GLOBAL_ACCEPT);
 
 	//%s has gained a level.

+ 2 - 1
client/windows/GUIClasses.h

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

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

@@ -130,6 +130,10 @@ AdventureOptionsTab::AdventureOptionsTab()
 	{
 		return setBoolSetting("adventure", "smoothDragging", value);
 	});
+	addCallback("skipAdventureMapAnimationsChanged", [](bool value)
+	{
+		return setBoolSetting("gameTweaks", "skipAdventureMapAnimations", value);
+	});
 	build(config);
 
 	std::shared_ptr<CToggleGroup> playerHeroSpeedToggle = widget<CToggleGroup>("heroMovementSpeedPicker");
@@ -172,4 +176,7 @@ AdventureOptionsTab::AdventureOptionsTab()
 	std::shared_ptr<CToggleButton> smoothDraggingCheckbox = widget<CToggleButton>("smoothDraggingCheckbox");
 	if (smoothDraggingCheckbox)
 		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);
 	});
 
-	addCallback("enableLargeSpellbookChanged", [](bool value)
+	addCallback("enableLargeSpellbookChanged", [this](bool 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)
@@ -196,6 +202,10 @@ GeneralOptionsTab::GeneralOptionsTab()
 
 	std::shared_ptr<CToggleButton> spellbookAnimationCheckbox = widget<CToggleButton>("spellbookAnimationCheckbox");
 	spellbookAnimationCheckbox->setSelected(settings["video"]["spellbookAnimation"].Bool());
+	if(settings["gameTweaks"]["enableLargeSpellbook"].Bool())
+		spellbookAnimationCheckbox->disable();
+	else
+		spellbookAnimationCheckbox->enable();
 
 	std::shared_ptr<CToggleButton> fullscreenBorderlessCheckbox = widget<CToggleButton>("fullscreenBorderlessCheckbox");
 	if (fullscreenBorderlessCheckbox)

+ 2 - 0
config/artifacts.json

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

+ 6 - 1
config/schemas/settings.json

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

+ 4 - 1
config/spells/timed.json

@@ -1153,7 +1153,10 @@
 		},
 		"targetCondition" : {
 			"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"
 			],
-			"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
 		{

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

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

+ 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 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.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)
 
 # VCMI Project

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

@@ -76,7 +76,7 @@
 		</screenshot>
 	</screenshots>
 	<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.0" date="2023-12-08" 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;
 bool JsonNode::Bool() const
 {
-	if (getType() == JsonType::DATA_NULL)
-		return boolDefault;
-	assert(getType() == JsonType::DATA_BOOL);
-	return std::get<bool>(data);
+	assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_BOOL);
+
+	if (getType() == JsonType::DATA_BOOL)
+		return std::get<bool>(data);
+
+	return boolDefault;
 }
 
 const double floatDefault = 0;
 double JsonNode::Float() const
 {
-	if(getType() == JsonType::DATA_NULL)
-		return floatDefault;
+	assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_INTEGER || getType() == JsonType::DATA_FLOAT);
+
+	if(getType() == JsonType::DATA_FLOAT)
+		return std::get<double>(data);
 
 	if(getType() == JsonType::DATA_INTEGER)
 		return static_cast<double>(std::get<si64>(data));
 
-	assert(getType() == JsonType::DATA_FLOAT);
-	return std::get<double>(data);
+	return floatDefault;
 }
 
-const si64 integetDefault = 0;
+const si64 integerDefault = 0;
 si64 JsonNode::Integer() const
 {
-	if(getType() == JsonType::DATA_NULL)
-		return integetDefault;
+	assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_INTEGER || getType() == JsonType::DATA_FLOAT);
+
+	if(getType() == JsonType::DATA_INTEGER)
+		return std::get<si64>(data);
 
 	if(getType() == JsonType::DATA_FLOAT)
 		return static_cast<si64>(std::get<double>(data));
 
-	assert(getType() == JsonType::DATA_INTEGER);
-	return std::get<si64>(data);
+	return integerDefault;
 }
 
 const std::string stringDefault = std::string();
 const std::string & JsonNode::String() const
 {
-	if (getType() == JsonType::DATA_NULL)
-		return stringDefault;
-	assert(getType() == JsonType::DATA_STRING);
-	return std::get<std::string>(data);
+	assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_STRING);
+
+	if (getType() == JsonType::DATA_STRING)
+		return std::get<std::string>(data);
+
+	return stringDefault;
 }
 
 const JsonVector vectorDefault = JsonVector();
 const JsonVector & JsonNode::Vector() const
 {
-	if (getType() == JsonType::DATA_NULL)
-		return vectorDefault;
-	assert(getType() == JsonType::DATA_VECTOR);
-	return std::get<JsonVector>(data);
+	assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_VECTOR);
+
+	if (getType() == JsonType::DATA_VECTOR)
+		return std::get<JsonVector>(data);
+
+	return vectorDefault;
 }
 
 const JsonMap mapDefault = JsonMap();
 const JsonMap & JsonNode::Struct() const
 {
-	if (getType() == JsonType::DATA_NULL)
-		return mapDefault;
-	assert(getType() == JsonType::DATA_STRUCT);
-	return std::get<JsonMap>(data);
+	assert(getType() == JsonType::DATA_NULL || getType() == JsonType::DATA_STRUCT);
+
+	if (getType() == JsonType::DATA_STRUCT)
+		return std::get<JsonMap>(data);
+
+	return mapDefault;
 }
 
 JsonNode & JsonNode::operator[](const std::string & child)

+ 9 - 7
lib/LoadProgress.cpp

@@ -13,14 +13,16 @@
 
 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

+ 37 - 0
lib/TurnTimerInfo.cpp

@@ -22,4 +22,41 @@ bool TurnTimerInfo::isBattleEnabled() const
 	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

+ 5 - 10
lib/TurnTimerInfo.h

@@ -28,16 +28,11 @@ struct DLL_LINKAGE TurnTimerInfo
 	bool isEnabled() 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>
 	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;
 }
 
-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
 {
 	RETURN_IF_NOT_BATTLE(nullptr);
@@ -1610,7 +1594,7 @@ std::set<const battle::Unit *> CBattleInfoCallback::battleAdjacentUnits(const ba
 	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);
 	//This is complete list. No spells from mods.
@@ -1658,9 +1642,19 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c
 		std::stringstream cachingStr;
 		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;
 
 		switch (spellID.toEnum())
@@ -1703,7 +1697,7 @@ SpellID CBattleInfoCallback::getRandomBeneficialSpell(CRandomGenerator & rand, c
 		case SpellID::CURE: //only damaged units
 		{
 			//do not cast on affected by debuffs
-			if(!subject->canBeHealed())
+			if(subject->getFirstHPleft() == subject->getMaxHealth())
 				continue;
 		}
 			break;

+ 1 - 7
lib/battle/CBattleInfoCallback.h

@@ -52,11 +52,6 @@ struct DLL_LINKAGE BattleClientInterfaceData
 class DLL_LINKAGE CBattleInfoCallback : public virtual CBattleInfoEssentials
 {
 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::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
 	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
 
 	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);
 	}
 
+	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 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 sourceTypeSel(BonusSource source);
 	CSelector DLL_LINKAGE valueType(BonusValueType valType);
+	CSelector DLL_LINKAGE typeSubtypeValueType(BonusType Type, BonusSubtypeID Subtype, BonusValueType valType);
 
 	/**
 	 * 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)
 {
-	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)
 		bonus->val += b->val;
 	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),
 			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>
@@ -204,14 +217,9 @@ int3 findClosestTile (Container & container, int3 dest)
 
 VCMI_LIB_NAMESPACE_END
 
-
 template<>
 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;
 }
 
+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::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
 	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>> 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)
 	/// 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)
 			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)
 			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),
 	battle(nullptr)
 {

+ 1 - 1
lib/mapObjects/CArmedInstance.h

@@ -43,7 +43,7 @@ public:
 	//////////////////////////////////////////////////////////////////////////
 
 	CArmedInstance();
-	CArmedInstance(bool isHypotetic);
+	CArmedInstance(bool isHypothetic);
 
 	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);
 						break;
 					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;
 					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;
 			}

+ 27 - 1
lib/mapObjects/MiscObjects.cpp

@@ -12,6 +12,7 @@
 #include "MiscObjects.h"
 
 #include "../ArtifactUtils.h"
+#include "../bonuses/Propagators.h"
 #include "../constants/StringConstants.h"
 #include "../CConfigHandler.h"
 #include "../CGeneralTextHandler.h"
@@ -116,7 +117,15 @@ void CGMine::initObj(CRandomGenerator & rand)
 		putStack(SlotID(0), troglodytes);
 
 		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
 	{
@@ -1005,6 +1014,23 @@ void CGGarrison::serializeJsonOptions(JsonSerializeFormat& 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)
 {
 	if (ID == Obj::EYE_OF_MAGI)

+ 2 - 0
lib/mapObjects/MiscObjects.h

@@ -60,6 +60,7 @@ class DLL_LINKAGE CGGarrison : public CArmedInstance
 public:
 	bool removableUnits;
 
+	void initObj(CRandomGenerator &rand) override;
 	bool passableFor(PlayerColor color) const override;
 	void onHeroVisit(const CGHeroInstance * h) const override;
 	void battleFinished(const CGHeroInstance *hero, const BattleResult &result) const override;
@@ -71,6 +72,7 @@ public:
 	}
 protected:
 	void serializeJsonOptions(JsonSerializeFormat & handler) override;
+	void addAntimagicGarrisonBonus();
 };
 
 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)));
 }
 
-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();
 }
 
@@ -143,8 +143,6 @@ void CMapEditManager::drawRiver(RiverId riverType, CRandomGenerator* gen)
 	terrainSel.clearSelection();
 }
 
-
-
 void CMapEditManager::insertObject(CGObjectInstance * obj)
 {
 	execute(std::make_unique<CInsertObjectOperation>(map, obj));

+ 1 - 1
lib/mapping/CMapEditManager.h

@@ -70,7 +70,7 @@ public:
 	void clearTerrain(CRandomGenerator * gen = nullptr);
 
 	/// 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.
 	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));
 }
 
-CDrawTerrainOperation::CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, CRandomGenerator * gen):
+CDrawTerrainOperation::CDrawTerrainOperation(CMap * map, CTerrainSelection terrainSel, TerrainId terType, int decorationsPercentage, CRandomGenerator * gen):
 	CMapOperation(map),
 	terrainSel(std::move(terrainSel)),
 	terType(terType),
+	decorationsPercentage(decorationsPercentage),
 	gen(gen)
 {
 
@@ -286,14 +287,19 @@ void CDrawTerrainOperation::updateTerrainViews()
 		// Get mapping
 		const TerrainViewPattern& pattern = patterns[bestPattern][valRslt.flip];
 		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];
-		}
 
 		// Set terrain view
 		auto & tile = map->getTile(pos);
@@ -555,12 +561,12 @@ CClearTerrainOperation::CClearTerrainOperation(CMap* map, CRandomGenerator* gen)
 {
 	CTerrainSelection terrainSel(map);
 	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)
 	{
 		terrainSel.clearSelection();
 		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
 {
 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 undo() override;
@@ -101,6 +101,7 @@ private:
 
 	CTerrainSelection terrainSel;
 	TerrainId terType;
+	int decorationsPercentage;
 	CRandomGenerator* gen;
 	std::set<int3> invalidatedTerViews;
 };

+ 2 - 0
lib/mapping/MapEditUtils.cpp

@@ -145,6 +145,7 @@ const std::string TerrainViewPattern::RULE_ANY = "?";
 TerrainViewPattern::TerrainViewPattern()
 	: diffImages(false)
 	, rotationTypesCount(0)
+	, decoration(false)
 	, minPoints(0)
 	, maxPoints(std::numeric_limits<int>::max())
 {
@@ -209,6 +210,7 @@ CTerrainViewPatternConfig::CTerrainViewPatternConfig()
 			// Read various properties
 			pattern.id = ptrnNode["id"].String();
 			assert(!pattern.id.empty());
+			pattern.decoration = ptrnNode["decoration"].Bool();
 			pattern.minPoints = static_cast<int>(ptrnNode["minPoints"].Float());
 			pattern.maxPoints = static_cast<int>(ptrnNode["maxPoints"].Float());
 			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
 	/// the same frame will be used and rotated.
 	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...)
 	/// are supported.
 	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();
 			for(const auto & offset : obj->getBlockedOffsets())
 			{
-				rmgObject->setPosition(tile - offset);
+				auto newPos = tile - offset;
 
-				if(!isInTheMap(rmgObject->getPosition()))
+				if(!isInTheMap(newPos))
 					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;
+				}
 
 				if(isProhibited(rmgObject->getArea()))
 					continue;

+ 1 - 1
lib/networkPacks/PacksForLobby.h

@@ -86,7 +86,7 @@ struct DLL_LINKAGE LobbyChatMessage : public CLobbyPackToPropagate
 struct DLL_LINKAGE LobbyGuiAction : public CLobbyPackToPropagate
 {
 	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;
 
 

+ 1 - 0
lib/rewardable/Reward.cpp

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

+ 24 - 18
lib/rmg/RmgArea.cpp

@@ -19,12 +19,12 @@ namespace rmg
 
 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)
@@ -161,6 +161,7 @@ const Tileset & Area::getBorder() const
 		return dBorderCache;
 	
 	//compute border cache
+	dBorderCache.reserve(dTiles.bucket_count());
 	for(const auto & t : dTiles)
 	{
 		for(auto & i : int3::getDirs())
@@ -182,6 +183,7 @@ const Tileset & Area::getBorderOutside() const
 		return dBorderOutsideCache;
 	
 	//compute outside border cache
+	dBorderOutsideCache.reserve(dBorderCache.bucket_count() * 2);
 	for(const auto & t : dTiles)
 	{
 		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
 {
+	// Important: Make sure that tiles.size < area.size
 	for(const auto & t : tiles)
 	{
 		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 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;
 }
 
 void Area::clear()
 {
 	dTiles.clear();
+	dTilesVectorCache.clear();
 	dTotalShiftCache = int3();
 	invalidate();
 }
@@ -329,15 +332,16 @@ void Area::erase(const int3 & tile)
 void Area::unite(const Area & area)
 {
 	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)
 {
 	invalidate();
 	Tileset result;
+	result.reserve(std::max(dTiles.size(), area.getTilesVector().size()));
 	for(const auto & t : area.getTilesVector())
 	{
 		if(dTiles.count(t))
@@ -359,10 +363,9 @@ void Area::translate(const int3 & shift)
 {
 	dBorderCache.clear();
 	dBorderOutsideCache.clear();
-	
+
 	if(dTilesVectorCache.empty())
 	{
-		getTiles();
 		getTilesVector();
 	}
 	
@@ -373,7 +376,6 @@ void Area::translate(const int3 & shift)
 	{
 		t += shift;
 	}
-	//toAbsolute(dTiles, shift);
 }
 
 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 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;
 }
 
@@ -419,7 +425,7 @@ Area 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> 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>;
 	void toAbsolute(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);
 }
 
+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
 {
 	for(const int3 &dir : int3::getDirs())
@@ -90,7 +95,7 @@ void RmgMap::initTiles(CMapGenerator & generator, CRandomGenerator & rand)
 	
 	getEditManager()->clearTerrain(&rand);
 	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();
 	zones.clear();
@@ -309,7 +314,7 @@ void RmgMap::setZoneID(const int3& tile, TRmgTemplateZoneId 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);
 	

+ 3 - 1
lib/rmg/RmgMap.h

@@ -27,6 +27,8 @@ class playerInfo;
 class RmgMap
 {
 public:
+	int getDecorationsPercentage() const;
+
 	mutable std::unique_ptr<CMap> mapInstance;
 	std::shared_ptr<MapProxy> getMapProxy() const;
 	CMap & getMap(const CMapGenerator *) const; //limited access
@@ -61,7 +63,7 @@ public:
 	TerrainTile & getTile(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;
 	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())
 	{
-		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.isBlockedVisitable())
-				// Do no assume blocked tile is accessible
-				dBlockedAreaCache.add(dObject.visitablePos());
+			dBlockedAreaCache.add(dObject.visitablePos());
 	}
 	return dBlockedAreaCache;
 }
@@ -70,8 +69,10 @@ const rmg::Area & Object::Instance::getAccessibleArea() const
 	if(dAccessibleAreaCache.empty())
 	{
 		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();
-		for(const auto & from : visitable.getTiles())
+		// TODO: Add in one operation to avoid multiple invalidation
+		for(const auto & from : visitable.getTilesVector())
 		{
 			if(isVisitableFrom(from))
 				dAccessibleAreaCache.add(from);
@@ -122,22 +123,13 @@ void Object::Instance::setAnyTemplate(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())
 	{
 		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));
 	}
-	//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);
 	dAccessibleAreaCache.clear();
@@ -191,7 +183,6 @@ Object::Object(CGObjectInstance & object):
 }
 
 Object::Object(const Object & object):
-	dStrength(object.dStrength),
 	guarded(false)
 {
 	for(const auto & i : object.dInstances)
@@ -199,20 +190,24 @@ Object::Object(const Object & object):
 	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)
@@ -220,16 +215,22 @@ void Object::addInstance(Instance & object)
 	//assert(object.dParent == *this);
 	setGuardedIfMonster(object);
 	dInstances.push_back(object);
+	cachedInstanceList.push_back(&object);
+	cachedInstanceConstList.push_back(&object);
 
 	clearCachedArea();
+	visibleTopOffset.reset();
 }
 
 Object::Instance & Object::addInstance(CGObjectInstance & object)
 {
 	dInstances.emplace_back(*this, object);
 	setGuardedIfMonster(dInstances.back());
+	cachedInstanceList.push_back(&dInstances.back());
+	cachedInstanceConstList.push_back(&dInstances.back());
 
 	clearCachedArea();
+	visibleTopOffset.reset();
 	return dInstances.back();
 }
 
@@ -237,8 +238,11 @@ Object::Instance & Object::addInstance(CGObjectInstance & object, const int3 & p
 {
 	dInstances.emplace_back(*this, object, position);
 	setGuardedIfMonster(dInstances.back());
+	cachedInstanceList.push_back(&dInstances.back());
+	cachedInstanceConstList.push_back(&dInstances.back());
 
 	clearCachedArea();
+	visibleTopOffset.reset();
 	return dInstances.back();
 }
 
@@ -265,15 +269,16 @@ const rmg::Area & Object::getAccessibleArea(bool exceptLast) const
 		return dAccessibleAreaCache;
 	if(!exceptLast && !dAccessibleAreaFullCache.empty())
 		return dAccessibleAreaFullCache;
-	
+
+	// FIXME: This clears tiles for every consecutive object
 	for(auto i = dInstances.begin(); i != std::prev(dInstances.end()); ++i)
 		dAccessibleAreaCache.unite(i->getAccessibleArea());
-	
+
 	dAccessibleAreaFullCache = dAccessibleAreaCache;
 	dAccessibleAreaFullCache.unite(dInstances.back().getAccessibleArea());
 	dAccessibleAreaCache.subtract(getArea());
 	dAccessibleAreaFullCache.subtract(getArea());
-	
+
 	if(exceptLast)
 		return dAccessibleAreaCache;
 	else
@@ -282,33 +287,45 @@ const rmg::Area & Object::getAccessibleArea(bool exceptLast) 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;
 }
 
 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;
 }
 
+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
 {
 	// 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.)
 	// 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();
 	blockVisitableArea.subtract(getRemovableArea());
 	entrableArea.subtract(blockVisitableArea);
@@ -326,11 +344,14 @@ const rmg::Area Object::getEntrableArea() const
 
 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;
 	for(auto& i : dInstances)
@@ -341,6 +362,8 @@ void Object::setTemplate(const TerrainId & terrain, CRandomGenerator & rng)
 {
 	for(auto& i : dInstances)
 		i.setTemplate(terrain, rng);
+
+	visibleTopOffset.reset();
 }
 
 const Area & Object::getArea() const
@@ -358,15 +381,23 @@ const Area & Object::getArea() 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
@@ -436,6 +467,7 @@ void Object::clearCachedArea() const
 	dAccessibleAreaCache.clear();
 	dAccessibleAreaFullCache.clear();
 	dBlockVisitableCache.clear();
+	dVisitableCache.clear();
 	dRemovableAreaCache.clear();
 }
 
@@ -444,6 +476,9 @@ void Object::clear()
 	for(auto & instance : dInstances)
 		instance.clear();
 	dInstances.clear();
+	cachedInstanceList.clear();
+	cachedInstanceConstList.clear();
+	visibleTopOffset.reset();
 
 	clearCachedArea();
 }

+ 7 - 3
lib/rmg/RmgObject.h

@@ -68,12 +68,13 @@ public:
 	Instance & addInstance(CGObjectInstance & object);
 	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;
 	const Area & getAccessibleArea(bool exceptLast = false) const;
 	const Area & getBlockVisitableArea() const;
+	const Area & getVisitableArea() const;
 	const Area & getRemovableArea() const;
 	const Area getEntrableArea() const;
 	
@@ -96,9 +97,12 @@ private:
 	mutable Area dFullAreaCache;
 	mutable Area dAccessibleAreaCache, dAccessibleAreaFullCache;
 	mutable Area dBlockVisitableCache;
+	mutable Area dVisitableCache;
 	mutable Area dRemovableAreaCache;
 	int3 dPosition;
-	ui32 dStrength;
+	mutable std::optional<int3> visibleTopOffset;
+	mutable std::list<Object::Instance*> cachedInstanceList;
+	mutable std::list<const Object::Instance*> cachedInstanceConstList;
 	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)
 		return Path::invalid();
 	
+	if(dst.empty()) // Skip construction of same area
+		return Path(*dArea);
+
 	auto resultArea = *dArea + dst;
 	Path result(resultArea);
-	if(dst.empty())
-		return result;
-	
+
 	int3 src = rmg::Area(dst).nearest(dPath);
 	result.connect(src);
 	

+ 68 - 10
lib/rmg/Zone.cpp

@@ -15,6 +15,7 @@
 #include "TileInfo.h"
 #include "CMapGenerator.h"
 #include "RmgPath.h"
+#include "modificators/ObjectManager.h"
 
 VCMI_LIB_NAMESPACE_BEGIN
 
@@ -177,6 +178,38 @@ rmg::Path Zone::searchPath(const rmg::Area & src, bool onlyStraight, const std::
 	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
 ///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
 
 	//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 marginFactor = 1.0f;
 
 	int treasureValue = 0;
 	int treasureDensity = 0;
-	for (auto t : treasureInfo)
+	for (const auto & t : treasureInfo)
 	{
 		treasureValue += ((t.min + t.max) / 2) * t.density / 1000.f; //Thousands
 		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
-		spanFactor *= (treasureValue / 100.f);
-		vstd::amax(spanFactor, 0.2f);
+		spanFactor *= (treasureValue / 125.f);
+		vstd::amax(spanFactor, 0.15f);
 	}
 	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
+	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)
 	{
@@ -240,6 +278,16 @@ void Zone::fractalize()
 		{
 			//link tiles in random order
 			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());
 			
 			int3 nodeFound(-1, -1, -1);
@@ -248,7 +296,7 @@ void Zone::fractalize()
 			{
 				//find closest free tile
 				int3 closestTile = clearedTiles.nearest(tileToMakePath);
-				if(closestTile.dist2dSQ(tileToMakePath) <= minDistance)
+				if(closestTile.dist2dSQ(tileToMakePath) <= freeDistance)
 					tilesToIgnore.add(tileToMakePath);
 				else
 				{
@@ -265,6 +313,16 @@ void Zone::fractalize()
 			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);
 	//cut straight paths towards the center. A* is too slow for that.
 	auto areas = connectedAreas(clearedTiles, false);

+ 1 - 0
lib/rmg/Zone.h

@@ -66,6 +66,7 @@ public:
 	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 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();
 

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

@@ -302,6 +302,8 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c
 	if(zone.isUnderground() != otherZone->isUnderground())
 	{
 		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);
 		if(!commonArea.empty())
 		{
@@ -322,7 +324,6 @@ void ConnectionsPlacer::selfSideIndirectConnection(const rmg::ZoneConnection & c
 			bool guarded2 = managerOther.addGuard(rmgGate2, connection.getGuardStrength(), true);
 			int minDist = 3;
 			
-			std::scoped_lock doubleLock(zone.areaMutex, otherZone->areaMutex);
 			rmg::Path path2(otherZone->area());
 			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);
 	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);
 		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
 	{
-		for(const auto & tile : searchArea.getTiles())
+		for(const auto & tile : searchArea.getTilesVector())
 		{
 			obj.setPosition(tile);
 
@@ -238,15 +238,14 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg
 	RecursiveLock lock(externalAccessMutex);
 	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())
 		{
-			if(map.getTileInfo(t).getNearestObjectDistance() < min_dist)
+			float distance = map.getTileInfo(t).getNearestObjectDistance();
+			if(distance < min_dist)
 				return -1.f;
+			else
+				vstd::amin(bestDistance, distance);
 		}
 		
 		rmg::Area perimeter;
@@ -298,7 +297,7 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg
 			}
 		}
 		
-		return dist;
+		return bestDistance;
 	}, isGuarded, onlyStraight, optimizer);
 }
 
@@ -306,6 +305,7 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg
 {
 	int3 pos;
 	auto possibleArea = searchArea;
+	auto cachedArea = zone.areaPossible() + zone.freePaths();
 	while(true)
 	{
 		pos = findPlaceForObject(possibleArea, obj, weightFunction, optimizer);
@@ -314,7 +314,7 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg
 			return rmg::Path::invalid();
 		}
 		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
 		if(isGuarded)
 		{
@@ -323,21 +323,31 @@ rmg::Path ObjectManager::placeAndConnectObject(const rmg::Area & searchArea, rmg
 			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))
 					return false;
 				
 				//guard position is always target
 				if(obj.instances().back()->getPosition(true) == t)
 					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())
 		{
@@ -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()
 {
 	logGlobal->trace("Creating required objects");
@@ -424,7 +469,8 @@ bool ObjectManager::createRequiredObjects()
 		}
 
 		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());
 		if(possibleArea.empty())
 		{
@@ -513,6 +559,7 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD
 			if(map.isOnMap(i) && map.isPossible(i))
 				map.setOccupied(i, ETileType::BLOCKED);
 	}
+	lock.unlock();
 	
 	if (updateDistance)
 	{
@@ -535,11 +582,13 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD
 			auto manager = map.getZones().at(id)->getModificator<ObjectManager>();
 			if (manager)
 			{
+				// TODO: Update distances for perimeter of guarded object, not just treasures
 				manager->updateDistances(object);
 			}
 		}
 	}
 	
+	// TODO: Add multiple tiles in one operation to avoid multiple invalidation
 	for(auto * instance : object.instances())
 	{
 		objectsVisitableArea.add(instance->getVisitablePosition());
@@ -552,10 +601,23 @@ void ObjectManager::placeObject(rmg::Object & object, bool guarded, bool updateD
 				continue;
 			}
 			else if(instance->object().appearance->isVisitableFromTop())
+			{
+				//Passable objects
 				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;
 	
 	// 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();
-	accessibleArea.intersect(visitablePos);
+	accessibleArea.erase_if([&](const int3 & tile)
+	{
+		return !entrableBorder.contains(tile);
+	});
+	
 	if(accessibleArea.empty())
 	{
 		delete guard;

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

@@ -48,7 +48,8 @@ public:
 	{
 		NONE = 0x00000000,
 		WEIGHT = 0x00000001,
-		DISTANCE = 0x00000010
+		DISTANCE = 0x00000010,
+		BOTH = 0x00000011
 	};
 
 public:
@@ -61,6 +62,7 @@ public:
 	void addCloseObject(const RequiredObjectInfo & info);
 	void addNearbyObject(const RequiredObjectInfo & info);
 
+	bool createMonoliths();
 	bool createRequiredObjects();
 
 	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
 		{
 			toBlock.clear();
-			for (const auto& tile : zone.areaPossible().getTiles())
+			for (const auto& tile : zone.areaPossible().getTilesVector())
 			{
 				rmg::Area neighbors;
 				rmg::Area t;
@@ -76,7 +76,7 @@ void ObstaclePlacer::process()
 				}
 			}
 			zone.areaPossible().subtract(toBlock);
-			for (const auto& tile : toBlock.getTiles())
+			for (const auto& tile : toBlock.getTilesVector())
 			{
 				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 minValue = treasureInfo.min;
 	
-	const ui32 desiredValue =zone.getRand().nextInt(minValue, maxValue);
+	const ui32 desiredValue = zone.getRand().nextInt(minValue, maxValue);
 	
 	int currentValue = 0;
 	bool hasLargeObject = false;
@@ -614,6 +614,13 @@ std::vector<ObjectInfo*> TreasurePlacer::prepareTreasurePile(const CTreasureInfo
 		oi->maxPerZone--;
 		
 		currentValue += oi->value;
+
+		if (currentValue >= minValue)
+		{
+			// 50% chance to end right here
+			if (zone.getRand().nextInt() & 1)
+				break;
+		}
 	}
 	
 	return objectInfos;
@@ -626,30 +633,41 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector<ObjectInfo*>
 	{
 		auto blockedArea = rmgObject.getArea();
 		auto entrableArea = rmgObject.getEntrableArea();
+		auto accessibleArea = rmgObject.getAccessibleArea();
 		
 		if(rmgObject.instances().empty())
-			entrableArea.add(int3());
+		{
+			accessibleArea.add(int3());
+		}
 		
 		auto * object = oi->generateObject();
 		if(oi->templates.empty())
 			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);
 
 		do
 		{
-			if(entrableArea.empty())
+			if(accessibleArea.empty())
 			{
 				//fail - fallback
 				rmgObject.clear();
@@ -657,15 +675,24 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector<ObjectInfo*>
 			}
 			
 			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();
-				for(const auto & t : entrableArea.getTilesVector())
+				for(const auto & t : accessibleArea.getTilesVector())
 				{
 					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
 						bestPositions.clear();
@@ -677,12 +704,11 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector<ObjectInfo*>
 						bestPositions.push_back(t);
 					}
 				}
-
 			}
 
 			if (bestPositions.empty())
 			{
-				bestPositions = entrableArea.getTilesVector();
+				bestPositions = accessibleArea.getTilesVector();
 			}
 			
 			int3 nextPos = *RandomGeneratorUtil::nextItem(bestPositions, zone.getRand());
@@ -699,11 +725,11 @@ rmg::Object TreasurePlacer::constructTreasurePile(const std::vector<ObjectInfo*>
 			if(rmgObject.instances().size() == 1)
 				break;
 
-			if(!blockedArea.overlap(instance.getBlockedArea()) && entrableArea.overlap(instanceAccessibleArea))
+			if(!blockedArea.overlap(instance.getBlockedArea()) && accessibleArea.overlap(instanceAccessibleArea))
 				break;
 
 			//fail - new position
-			entrableArea.erase(nextPos);
+			accessibleArea.erase(nextPos);
 		} while(true);
 	}
 	return rmgObject;
@@ -791,7 +817,7 @@ void TreasurePlacer::createTreasures(ObjectManager& manager)
 	size_t size = 0;
 	{
 		Zone::Lock lock(zone.areaMutex);
-		size = zone.getArea().getTiles().size();
+		size = zone.getArea().getTilesVector().size();
 	}
 
 	int totalDensity = 0;
@@ -808,16 +834,17 @@ void TreasurePlacer::createTreasures(ObjectManager& manager)
 
 		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
+		const int averageValue = (t->min + t->max) / 2;
 		if (t != (treasureInfo.end() - 1))
 		{
-			const int averageValue = (t->min + t->max) / 2;
 			if (averageValue > 10000)
 			{
 				//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; });
 
 			const ui32 maxPileGenerationAttemps = 2;
-			for (ui32 attempt = 0; attempt <= maxPileGenerationAttemps; attempt++)
+			for (ui32 attempt = 0; attempt < maxPileGenerationAttemps; attempt++)
 			{
 				auto rmgObject = constructTreasurePile(treasurePileInfos, attempt == maxAttempts);
 
@@ -865,61 +892,58 @@ void TreasurePlacer::createTreasures(ObjectManager& manager)
 		{
 			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;
+							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())
 			if(map.isOnMap(t))
 				lakes.back().neighbourZones[map.getZoneID(t)].add(t);
-		for(const auto & t : lake.getTiles())
+		for(const auto & t : lake.getTilesVector())
 			lakeMap[t] = lakeId;
 		
 		//each lake must have at least one free tile
@@ -143,7 +143,7 @@ RouteInfo WaterProxy::waterRoute(Zone & dst)
 		{
 			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))
 						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
-			if (lake.area.getTiles().size() < 25)
+			if (lake.area.getTilesVector().size() < 25)
 			{
 				logGlobal->info("Skipping very small lake at zone %d", dst.getId());
 				continue;
@@ -273,7 +273,7 @@ bool WaterProxy::placeBoat(Zone & land, const Lake & lake, bool createRoad, Rout
 
 	while(!boardingPositions.empty())
 	{
-		auto boardingPosition = *boardingPositions.getTiles().begin();
+		auto boardingPosition = *boardingPositions.getTilesVector().begin();
 		rmg::Area shipPositions({boardingPosition});
 		auto boutside = shipPositions.getBorderOutside();
 		shipPositions.assign(boutside);
@@ -336,7 +336,7 @@ bool WaterProxy::placeShipyard(Zone & land, const Lake & lake, si32 guard, bool
 	
 	while(!boardingPositions.empty())
 	{
-		auto boardingPosition = *boardingPositions.getTiles().begin();
+		auto boardingPosition = *boardingPositions.getTilesVector().begin();
 		rmg::Area shipPositions({boardingPosition});
 		auto boutside = shipPositions.getBorderOutside();
 		shipPositions.assign(boutside);

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

@@ -14,47 +14,47 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 MapProxy::MapProxy(RmgMap & map):
-    map(map)
+	map(map)
 {
 }
 
 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)
 {
-    Lock lock(mx);
-    map.getEditManager()->insertObjects(objects);
+	Lock lock(mx);
+	map.getEditManager()->insertObjects(objects);
 }
 
 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)
 {
-    Lock lock(mx);
+	Lock lock(mx);
 	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)
 {
-    Lock lock(mx);
+	Lock lock(mx);
 	map.getEditManager()->getTerrainSelection().setSelection(tiles);
 	map.getEditManager()->drawRiver(VLC->terrainTypeHandler->getById(terrain)->river, &generator);
 }
 
 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);
 }
 
-VCMI_LIB_NAMESPACE_END
+VCMI_LIB_NAMESPACE_END

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

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

+ 32 - 32
mapeditor/translation/czech.ts

@@ -217,7 +217,7 @@
     <message>
         <location filename="../mainwindow.ui" line="256"/>
         <source>Map Objects View</source>
-        <translation type="unfinished"></translation>
+        <translation>Zobrazení objektů mapy</translation>
     </message>
     <message>
         <location filename="../mainwindow.ui" line="300"/>
@@ -227,7 +227,7 @@
     <message>
         <location filename="../mainwindow.ui" line="378"/>
         <source>Inspector</source>
-        <translation type="unfinished"></translation>
+        <translation>Inspektor</translation>
     </message>
     <message>
         <location filename="../mainwindow.ui" line="420"/>
@@ -348,7 +348,7 @@
     <message>
         <location filename="../mainwindow.ui" line="1194"/>
         <source>Map title and description</source>
-        <translation type="unfinished"></translation>
+        <translation>Název a popis mapy</translation>
     </message>
     <message>
         <location filename="../mainwindow.ui" line="1205"/>
@@ -387,7 +387,7 @@
         <location filename="../mainwindow.cpp" line="1056"/>
         <location filename="../mainwindow.cpp" line="1113"/>
         <source>Update appearance</source>
-        <translation type="unfinished"></translation>
+        <translation>Aktualizovat vzhled</translation>
     </message>
     <message>
         <location filename="../mainwindow.ui" line="1300"/>
@@ -651,7 +651,7 @@
     <message>
         <location filename="../mapsettings/mapsettings.ui" line="179"/>
         <source>Abilities</source>
-        <translation type="unfinished"></translation>
+        <translation>Schopnosti</translation>
     </message>
     <message>
         <location filename="../mapsettings/mapsettings.ui" line="214"/>
@@ -838,53 +838,53 @@
     <message>
         <location filename="../inspector/heroskillswidget.cpp" line="19"/>
         <source>Beginner</source>
-        <translation type="unfinished"></translation>
+        <translation>Začátečník</translation>
     </message>
     <message>
         <location filename="../inspector/heroskillswidget.cpp" line="20"/>
         <source>Advanced</source>
-        <translation type="unfinished"></translation>
+        <translation>Pokročilý</translation>
     </message>
     <message>
         <location filename="../inspector/heroskillswidget.cpp" line="21"/>
         <source>Expert</source>
-        <translation type="unfinished"></translation>
+        <translation>Expert</translation>
     </message>
     <message>
         <location filename="../inspector/inspector.cpp" line="35"/>
         <source>Compliant</source>
-        <translation type="unfinished"></translation>
+        <translation>Ochotná</translation>
     </message>
     <message>
         <location filename="../inspector/inspector.cpp" line="36"/>
         <source>Friendly</source>
-        <translation type="unfinished"></translation>
+        <translation>Přátelská</translation>
     </message>
     <message>
         <location filename="../inspector/inspector.cpp" line="37"/>
         <source>Aggressive</source>
-        <translation type="unfinished"></translation>
+        <translation>Agresivní</translation>
     </message>
     <message>
         <location filename="../inspector/inspector.cpp" line="38"/>
         <source>Hostile</source>
-        <translation type="unfinished"></translation>
+        <translation>Nepřátelská</translation>
     </message>
     <message>
         <location filename="../inspector/inspector.cpp" line="39"/>
         <source>Savage</source>
-        <translation type="unfinished"></translation>
+        <translation>Brutální</translation>
     </message>
     <message>
         <location filename="../inspector/inspector.cpp" line="478"/>
         <location filename="../inspector/inspector.cpp" line="845"/>
         <source>neutral</source>
-        <translation type="unfinished"></translation>
+        <translation>neutrální</translation>
     </message>
     <message>
         <location filename="../inspector/inspector.cpp" line="843"/>
         <source>UNFLAGGABLE</source>
-        <translation type="unfinished"></translation>
+        <translation>NEOZNAČITELNÝ</translation>
     </message>
 </context>
 <context>
@@ -1101,7 +1101,7 @@
     <message>
         <location filename="../inspector/rewardswidget.ui" line="214"/>
         <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>
         <location filename="../inspector/rewardswidget.ui" line="225"/>
@@ -1482,32 +1482,32 @@
     <message>
         <location filename="../validator.cpp" line="101"/>
         <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>
         <location filename="../validator.cpp" line="108"/>
         <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>
         <location filename="../validator.cpp" line="118"/>
         <source>Prison %1 must be a NEUTRAL</source>
-        <translation type="unfinished"></translation>
+        <translation>Vězení %1 musí být NEUTRÁLNÍ</translation>
     </message>
     <message>
         <location filename="../validator.cpp" line="124"/>
         <source>Hero %1 must have an owner</source>
-        <translation type="unfinished"></translation>
+        <translation>Hrdina %1 musí mít vlastníka</translation>
     </message>
     <message>
         <location filename="../validator.cpp" line="129"/>
         <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>
         <location filename="../validator.cpp" line="132"/>
         <source>Hero %1 has duplicate on map</source>
-        <translation type="unfinished"></translation>
+        <translation>Hrdina %1 má na mapě dvojníka</translation>
     </message>
     <message>
         <location filename="../validator.cpp" line="135"/>
@@ -1517,7 +1517,7 @@
     <message>
         <location filename="../validator.cpp" line="146"/>
         <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>
         <location filename="../validator.cpp" line="149"/>
@@ -1527,32 +1527,32 @@
     <message>
         <location filename="../validator.cpp" line="155"/>
         <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>
         <location filename="../validator.cpp" line="164"/>
         <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>
         <location filename="../validator.cpp" line="168"/>
         <source>Map name is not specified</source>
-        <translation type="unfinished"></translation>
+        <translation>Mapa nemá název</translation>
     </message>
     <message>
         <location filename="../validator.cpp" line="170"/>
         <source>Map description is not specified</source>
-        <translation type="unfinished"></translation>
+        <translation>Mapa nemá popis</translation>
     </message>
     <message>
         <location filename="../validator.cpp" line="177"/>
         <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>
         <location filename="../validator.cpp" line="183"/>
         <source>Exception occurs during validation: %1</source>
-        <translation type="unfinished"></translation>
+        <translation>Při posudku nastala výjimka: %1</translation>
     </message>
     <message>
         <location filename="../validator.cpp" line="187"/>
@@ -1797,7 +1797,7 @@
     <message>
         <location filename="../windownewmap.cpp" line="296"/>
         <source>RMG failure</source>
-        <translation type="unfinished"></translation>
+        <translation>Chyba RMG</translation>
     </message>
 </context>
 <context>
@@ -1815,12 +1815,12 @@
     <message>
         <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>
-        <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>
         <location filename="../mainwindow.cpp" line="111"/>
         <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>
         <location filename="../mainwindow.cpp" line="112"/>

+ 8 - 5
server/CGameHandler.cpp

@@ -253,11 +253,12 @@ void CGameHandler::levelUpCommander (const CCommanderInstance * c, int skill)
 				break;
 			case ECommander::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;
 			case ECommander::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;
 			case ECommander::SPEED:
 				scp.accumulatedBonus.type = BonusType::STACKS_SPEED;
@@ -1973,7 +1974,9 @@ bool CGameHandler::bulkMoveArmy(ObjectInstanceID srcArmy, ObjectInstanceID destA
 		{
 			const bool needsLastStack = armySrc->needsLastStack();
 			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;
@@ -2223,12 +2226,12 @@ bool CGameHandler::arrangeStacks(ObjectInstanceID id1, ObjectInstanceID id2, ui8
 
 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
 {
-	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)

+ 5 - 5
server/NetPacksLobbyServer.cpp

@@ -189,11 +189,11 @@ void ApplyOnServerAfterAnnounceNetPackVisitor::visitLobbyClientDisconnected(Lobb
 	}
 	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)

+ 18 - 11
server/TurnTimerHandler.cpp

@@ -34,6 +34,8 @@ void TurnTimerHandler::onGameplayStart(PlayerColor player)
 	{
 		timers[player] = si->turnTimerInfo;
 		timers[player].turnTimer = 0;
+		timers[player].battleTimer = 0;
+		timers[player].unitTimer = 0;
 		timers[player].isActive = true;
 		timers[player].isBattle = false;
 		lastUpdate[player] = std::numeric_limits<int>::max();
@@ -103,11 +105,8 @@ bool TurnTimerHandler::timerCountDown(int & timer, int initialTimer, PlayerColor
 	{
 		timer -= waitTime;
 		lastUpdate[player] += waitTime;
-		int frequency = (timer > turnTimePropagateThreshold
-						 && initialTimer - timer > turnTimePropagateThreshold)
-		? turnTimePropagateFrequency : turnTimePropagateFrequencyCrit;
 		
-		if(lastUpdate[player] >= frequency)
+		if(lastUpdate[player] >= turnTimePropagateFrequency)
 			sendTimerUpdate(player);
 
 		return true;
@@ -127,6 +126,10 @@ void TurnTimerHandler::onPlayerMakingTurn(PlayerColor player, int waitTime)
 	const auto * state = gameHandler.getPlayerState(player);
 	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))
 			return;
 
@@ -277,17 +280,21 @@ void TurnTimerHandler::onBattleLoop(const BattleID & battleID, int waitTime)
 	auto & timer = timers[player];
 	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;
 
-		 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))
 		{

+ 1 - 3
server/TurnTimerHandler.h

@@ -25,9 +25,7 @@ class CGameHandler;
 class TurnTimerHandler
 {	
 	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, int> lastUpdate;
 	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> 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!");
-	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;
 }
 

+ 1 - 1
server/processors/PlayerMessageProcessor.cpp

@@ -384,7 +384,7 @@ void PlayerMessageProcessor::cheatPuzzleReveal(PlayerColor player)
 
 	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);
 			for(const auto & color : t->players)

+ 37 - 5
server/processors/TurnOrderProcessor.cpp

@@ -9,6 +9,7 @@
  */
 #include "StdInc.h"
 #include "TurnOrderProcessor.h"
+#include "PlayerMessageProcessor.h"
 
 #include "../queries/QueriesProcessor.h"
 #include "../queries/MapQueries.h"
@@ -35,9 +36,9 @@ int TurnOrderProcessor::simturnsTurnsMinLimit() const
 	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(actingPlayers.empty());
@@ -50,9 +51,40 @@ void TurnOrderProcessor::updateContactStatus()
 				continue;
 
 			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
@@ -204,7 +236,7 @@ void TurnOrderProcessor::doStartNewDay()
 	std::swap(actedPlayers, awaitingPlayers);
 
 	gameHandler->onNewTurn();
-	updateContactStatus();
+	updateAndNotifyContactStatus();
 	tryStartTurnsForPlayers();
 }
 
@@ -301,7 +333,7 @@ bool TurnOrderProcessor::onPlayerEndsTurn(PlayerColor which)
 void TurnOrderProcessor::onGameStarted()
 {
 	if (actingPlayers.empty())
-		updateContactStatus();
+		blockedContacts = computeContactStatus();
 
 	// this may be game load - send notification to players that they can act
 	auto actingPlayersCopy = actingPlayers;

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