Jelajahi Sumber

Merge branch 'develop' into joystick_support

kdmcser 1 tahun lalu
induk
melakukan
cee8d34fc5
100 mengubah file dengan 3792 tambahan dan 1926 penghapusan
  1. 3 3
      .github/workflows/github.yml
  2. 1 2
      AI/BattleAI/StackWithBonuses.cpp
  3. 3 0
      AI/VCAI/VCAI.cpp
  4. 5 0
      CMakeLists.txt
  5. 80 0
      ChangeLog.md
  6. 3 6
      Global.h
  7. 2 0
      Mods/vcmi/config/vcmi/english.json
  8. 537 0
      Mods/vcmi/config/vcmi/portuguese.json
  9. 2 0
      Mods/vcmi/config/vcmi/ukrainian.json
  10. 11 0
      Mods/vcmi/mod.json
  11. 1 0
      client/CGameInfo.cpp
  12. 0 2
      client/CMT.cpp
  13. 14 6
      client/CMakeLists.txt
  14. 7 0
      client/CMusicHandler.cpp
  15. 1 0
      client/CMusicHandler.h
  16. 15 80
      client/CPlayerInterface.cpp
  17. 97 34
      client/CServerHandler.cpp
  18. 5 1
      client/CServerHandler.h
  19. 0 1
      client/Client.h
  20. 84 17
      client/ClientCommandManager.cpp
  21. 5 2
      client/ClientCommandManager.h
  22. 2 1
      client/HeroMovementController.cpp
  23. 15 0
      client/NetPacksClient.cpp
  24. 60 61
      client/adventureMap/AdventureMapInterface.cpp
  25. 5 2
      client/adventureMap/AdventureMapInterface.h
  26. 2 2
      client/adventureMap/AdventureMapShortcuts.cpp
  27. 7 3
      client/eventsSDL/InputSourceKeyboard.cpp
  28. 7 0
      client/eventsSDL/InputSourceText.cpp
  29. 2 0
      client/eventsSDL/InputSourceText.h
  30. 1 1
      client/globalLobby/GlobalLobbyClient.cpp
  31. 2 1
      client/globalLobby/GlobalLobbyLoginWindow.cpp
  32. 1 0
      client/globalLobby/GlobalLobbyWindow.cpp
  33. 1 1
      client/gui/CGuiHandler.cpp
  34. 13 4
      client/gui/EventDispatcher.cpp
  35. 31 140
      client/gui/ShortcutHandler.cpp
  36. 4 2
      client/gui/ShortcutHandler.h
  37. 8 3
      client/gui/WindowHandler.cpp
  38. 1 1
      client/lobby/CBonusSelection.cpp
  39. 1 1
      client/lobby/SelectionTab.cpp
  40. 5 0
      client/mainmenu/CMainMenu.cpp
  41. 1 0
      client/mainmenu/CPrologEpilogVideo.cpp
  42. 7 1
      client/mapView/MapRenderer.cpp
  43. 1 1
      client/mapView/mapHandler.h
  44. 1 1
      client/renderSDL/ScreenHandler.cpp
  45. 6 0
      client/widgets/Buttons.cpp
  46. 1 0
      client/widgets/Buttons.h
  47. 2 1
      client/widgets/CArtifactsOfHeroAltar.cpp
  48. 1 1
      client/widgets/CArtifactsOfHeroAltar.h
  49. 7 9
      client/widgets/CArtifactsOfHeroBackpack.cpp
  50. 2 1
      client/widgets/CArtifactsOfHeroKingdom.cpp
  51. 2 1
      client/widgets/CArtifactsOfHeroKingdom.h
  52. 2 1
      client/widgets/CArtifactsOfHeroMain.cpp
  53. 1 1
      client/widgets/CArtifactsOfHeroMain.h
  54. 3 21
      client/widgets/CArtifactsOfHeroMarket.cpp
  55. 2 3
      client/widgets/CArtifactsOfHeroMarket.h
  56. 0 6
      client/widgets/CWindowWithArtifacts.cpp
  57. 2 2
      client/widgets/MiscWidgets.cpp
  58. 1 1
      client/widgets/Slider.cpp
  59. 3 1
      client/widgets/Slider.h
  60. 13 0
      client/widgets/TextControls.cpp
  61. 1 0
      client/widgets/TextControls.h
  62. 91 72
      client/widgets/markets/CAltarArtifacts.cpp
  63. 8 21
      client/widgets/markets/CAltarArtifacts.h
  64. 103 86
      client/widgets/markets/CAltarCreatures.cpp
  65. 10 11
      client/widgets/markets/CAltarCreatures.h
  66. 126 0
      client/widgets/markets/CArtifactsBuying.cpp
  67. 25 0
      client/widgets/markets/CArtifactsBuying.h
  68. 174 0
      client/widgets/markets/CArtifactsSelling.cpp
  69. 35 0
      client/widgets/markets/CArtifactsSelling.h
  70. 124 0
      client/widgets/markets/CFreelancerGuild.cpp
  71. 26 0
      client/widgets/markets/CFreelancerGuild.h
  72. 244 0
      client/widgets/markets/CMarketBase.cpp
  73. 128 0
      client/widgets/markets/CMarketBase.h
  74. 123 0
      client/widgets/markets/CMarketResources.cpp
  75. 27 0
      client/widgets/markets/CMarketResources.h
  76. 0 105
      client/widgets/markets/CTradeBase.cpp
  77. 0 74
      client/widgets/markets/CTradeBase.h
  78. 111 0
      client/widgets/markets/CTransferResources.cpp
  79. 25 0
      client/widgets/markets/CTransferResources.h
  80. 115 124
      client/widgets/markets/TradePanels.cpp
  81. 44 26
      client/widgets/markets/TradePanels.h
  82. 0 147
      client/windows/CAltarWindow.cpp
  83. 0 40
      client/windows/CAltarWindow.h
  84. 7 7
      client/windows/CCastleInterface.cpp
  85. 4 4
      client/windows/CKingdomInterface.cpp
  86. 263 0
      client/windows/CMarketWindow.cpp
  87. 50 0
      client/windows/CMarketWindow.h
  88. 19 5
      client/windows/CSpellWindow.cpp
  89. 0 679
      client/windows/CTradeWindow.cpp
  90. 0 80
      client/windows/CTradeWindow.h
  91. 4 3
      client/windows/InfoWindows.cpp
  92. 7 7
      config/battlefields.json
  93. 689 0
      config/biomes.json
  94. 20 1
      config/gameConfig.json
  95. 1 1
      config/heroes/conflux.json
  96. 3 3
      config/mainmenu.json
  97. 67 0
      config/schemas/biome.json
  98. 4 0
      config/schemas/faction.json
  99. 5 0
      config/schemas/mod.json
  100. 7 1
      config/schemas/settings.json

+ 3 - 3
.github/workflows/github.yml

@@ -32,7 +32,7 @@ jobs:
             test: 0
             preset: linux-gcc-debug
           - platform: mac-intel
-            os: macos-12
+            os: macos-13
             test: 0
             pack: 1
             pack_type: Release
@@ -42,7 +42,7 @@ jobs:
             conan_options: --options with_apple_system_libs=True
             artifact_platform: intel
           - platform: mac-arm
-            os: macos-12
+            os: macos-13
             test: 0
             pack: 1
             pack_type: Release
@@ -52,7 +52,7 @@ jobs:
             conan_options: --options with_apple_system_libs=True
             artifact_platform: arm
           - platform: ios
-            os: macos-12
+            os: macos-13
             test: 0
             pack: 1
             pack_type: Release

+ 1 - 2
AI/BattleAI/StackWithBonuses.cpp

@@ -208,8 +208,7 @@ void StackWithBonuses::removeUnitBonus(const std::vector<Bonus> & bonus)
 				&& one.sid == b->sid
 				&& one.valType == b->valType
 				&& one.additionalInfo == b->additionalInfo
-				&& one.effectRange == b->effectRange
-				&& one.description == b->description;
+				&& one.effectRange == b->effectRange;
 		});
 
 		removeUnitBonus(selector);

+ 3 - 0
AI/VCAI/VCAI.cpp

@@ -2031,6 +2031,9 @@ void VCAI::tryRealize(Goals::Explore & g)
 
 void VCAI::tryRealize(Goals::RecruitHero & g)
 {
+	if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST)
+		throw cannotFulfillGoalException("Not enough gold to recruit hero!");
+
 	if(const CGTownInstance * t = findTownWithTavern())
 	{
 		recruitHero(t, true);

+ 5 - 0
CMakeLists.txt

@@ -393,6 +393,11 @@ if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" OR NOT WIN32)
 		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=array-bounds") # false positives in boost::multiarray during release build, keep as warning-only
 	endif()
 
+	if(CMAKE_CXX_COMPILER_ID STREQUAL "GNU" AND NOT WIN32)
+		# For gcc 14+ we can use -fhardened instead
+		set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -D_FORTIFY_SOURCE=2 -D_GLIBCXX_ASSERTIONS -fstack-protector-strong -fstack-clash-protection -fcf-protection=full")
+	endif()
+
 	# Fix string inspection with lldb
 	# https://stackoverflow.com/questions/58578615/cannot-inspect-a-stdstring-variable-in-lldb
 	if(CMAKE_CXX_COMPILER_ID STREQUAL "Clang")

+ 80 - 0
ChangeLog.md

@@ -1,18 +1,49 @@
 # 1.4.5 -> 1.5.0
 
 ### General
+* Added Portuguese (Brazilian) translation
 * Added option to disable cheats in game
+* Game will no longer run vcmiserver as a separate process on desktop systems
+
+### Stability
+* Fixed possible crash in Altar of Sacrifice
+* Fixed possible crash on activation of 'Enchanted' bonus
+* Fixed possible race condition on random maps generation on placement treasures near border with water zone
+* Fixed crash on missing video files
+* Fixed crash on using healing spell as 'casts before/after attack' bonus
+* Fixed crash on defeating hero that was located in boat on game start
+* Fixed possible crash on turn timer running out while player has town screen open
+* Fixed crash when player has manual control of arrow towers during siege
+* Fixed crash on attempt to attack with Magma Elementals with Erdamon as hero
+* Fixed crash on attempt to access removed Quest Guard
+
+### Multiplayer
+* Implemented new lobby, available in game with persistent accounts and chat
+* Removed old lobby previously available in launcher
+* Fixed potential crash that could occur if two players act at the very same time
 
 ### Interface
+* Game will now save last used difficulty settings
 * Town Portal dialog will now show town icons
 * Town Portal dialog will now show town info on right click
 * Town Portal dialog will center on town on clicking it
 * Town Portal dialog now uses same town ordering as in adventure map interface
+* Game will now remember scrolling position of hero backpack
 * Heroes can now be recruited from the tavern by double-clicking on them
 * Added status bar to the backpack window
 * Quick backpack window is now only available when enabled Interface enhancements
 * Fixed assembly of artifacts in the backpack when backpack is full
 * Attempt to use enemy turn replay feature will now show "Not implemented" message
+* It is now possible to configure size of small battle queue in config file
+* Opening hero window in town will now open exchange dialog if there are two heroes in town, allowing artifact exchange
+* Fixed positioning of FPS counter after resolution change
+* It is now possible to access extra options window from campaigns startup dialog
+* Size of message boxes should now match H3 better. Maximum-size message box will always be smaller than screen size
+* If monsters are willing to join for money, game will now show gold icon in this dialog box
+* Fixed visual duplication of artifacts on Altar of Sacrifice
+* Fixed translation of some bonuses using incorrect language
+* Added option to use 'nearest' rounding mode for UI scaling
+* Fixed various minor bugs in trade window interface
 
 ### Campaigns
 * Game will now correctly track who defeated the hero or wandering monsters for related quests and victory conditions
@@ -25,6 +56,7 @@
 * Fixed missing names for heroes who have their names customized in map after being transferred to the next scenario
 * Artifact transfer will now work correctly if the hero holding the transferable artifact is not also transferring
 * Fixed crash on opening of some campaigns in the French version from gog.com
+* Fixed crash on advancing to campaign mission in which you can pick hero as starting bonus
 * It is now possible to replay the intro movie from the scenario information window
 * When playing the intro video, the subtitles are now correctly synchronized with the audio
 
@@ -36,20 +68,68 @@
 * Disabling battle queue will now correctly reposition hero statistics preview popup
 * Fixed positioning of unit stack size label
 
+### Mechanics
+* It is no longer possible to learn spells from Pandora or events if hero can not learn them
+* Fixed behavior of 'Dimension Door' spell to be in line with H3:SoD
+* If it is not possible to cast 'Dimension Door', game will show message immediately on picking spell in spellbook
+* Added options to configure 'Dimension Door' spell to be in line with HotA
+* Casting 'Town Portal' while in boat will now show correct message box instead of server error
+* Game will now take mana before visiting town after casting 'Town Portal', allowing Mana Vortex to correctly replenish all mana points
+* Fixed loading of negative luck and morale in events, pandoras and quests on h3m maps
+* Fixed incorrect 'duplicate hero' error on loading of some vmap maps
+* Fixed previously broken digging of the Grail
+* Successful digging for Grail will now show correct message
+* Game will now correctly update movement range after rearranging armies
+* It is no longer possible for two towns with random names to have same name, just like in H3
+* Creatures that were consumed by Demon Summon ability will no longer return to life after the battle
+* Effects of melee-only or ranged-only spells, such as Bloodlust or Precision are no longer cumulative
+* It is no longer possible to use summoning spells if such spell would summon 0 creatures
+* It is now possible to assemble or disassemble artifacts while in Altar of Sacrifice
+* It is no longer possible to move war machines to Altar of Sacrifice
+
+### Random Maps Generator
+* Game will now save last used RMG settings in game and in editor
+* Reduced number of obstacles placed in water zones
+* Treasure values in water zone should now be similar to values from HotA, due to bugs in H3:SoD values
+* Random map templates can now have optional description visible in random map setup
+* Implemented Penrose tiling to produce more natural zone edges
+* Increased minimal density of obstacles on surface level of the map
+* Decreased minimal density of obstacles on undergound level of the map
+* Density of objects should now closely resemble H3 RMG
+* Generator will now avoid routing road under guarded objects whenever possible
+* Interactive objects will now appear on top of static objects
+* Windmill will now appear on top of all other objects
+
 ### Launcher
 * Added Spanish translation to launcher
+* Added Portuguese translation to launcher
 
 ### Map Editor
 * Added Chinese translation to map editor
+* Added Portuguese translation to map editor
+* Mod list in settings will now correctly show submods of submods
+* Fixed display of resource type and quantity in mines
+* Fixed inability to change object owner in editor
+* Added map sizes larger than XL in map editor
+* It is now possible to customize hero spells
 
 ### AI
 * Fixed possible crash on updating NKAI pathfinding data
 * Fixed counting mana usage cost of Fly spell
 * Added estimation of value of Pyramid and Cyclops Stockpile
+* Reduced memory usage and improved performance of AI pathfinding
+* Added experimental and disabled by default implementation of object graph
+* It is now possible to configure AI settings via config file
 
 ### Modding
 * Added new game setting that allows inviting heroes to taverns
 * Fixed reversed Overlord and Warlock classes mapping
+* Added 'selectAll' mode for configurable objects which grants all potential rewards 
+* It is now possible to use most of json5 format in vcmi json files
+* Main mod.json file (including any submods) now requires strict json, without comments or extra commas
+* Replaced bonus MANA_PER_KNOWLEDGE with MANA_PER_KNOWLEDGE_PERCENTAGE to avoid rounding error with mysticism
+* Factions can now be marked as 'special', banning them from random selection
+* Replaced 'convert txt' text export command with more convenient 'translate' and 'translate maps' commands
 
 # 1.4.4 -> 1.4.5
 

+ 3 - 6
Global.h

@@ -104,12 +104,6 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 
 #define _USE_MATH_DEFINES
 
-#ifndef NDEBUG
-// Enable additional debug checks from glibc / libstdc++ when building with enabled assertions
-// Since these defines must be declared BEFORE including glibc header we can not check for __GLIBCXX__ macro to detect that glibc is in use
-#  define _GLIBCXX_ASSERTIONS
-#endif
-
 #include <algorithm>
 #include <any>
 #include <array>
@@ -149,6 +143,9 @@ static_assert(sizeof(bool) == 1, "Bool needs to be 1 byte in size.");
 #if BOOST_VERSION > 105000
 #  define BOOST_THREAD_VERSION 3
 #endif
+#if BOOST_VERSION == 107400
+#  define BOOST_ALLOW_DEPRECATED_HEADERS
+#endif
 #define BOOST_THREAD_DONT_PROVIDE_THREAD_DESTRUCTOR_CALLS_TERMINATE_IF_JOINABLE 1
 //need to link boost thread dynamically to avoid https://stackoverflow.com/questions/35978572/boost-thread-interupt-does-not-work-when-crossing-a-dll-boundary
 #define BOOST_THREAD_USE_DLL //for example VCAI::finish() may freeze on thread join after interrupt when linking this statically

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

@@ -117,6 +117,8 @@
 	"vcmi.server.errors.modNoDependency" : "Failed to load mod {'%s'}!\n It depends on mod {'%s'} which is not active!\n",
 	"vcmi.server.errors.modConflict" : "Failed to load mod {'%s'}!\n Conflicts with active mod {'%s'}!\n",
 	"vcmi.server.errors.unknownEntity" : "Failed to load save! Unknown entity '%s' found in saved game! Save may not be compatible with currently installed version of mods!",
+	
+	"vcmi.dimensionDoor.seaToLandError" : "It's not possible to teleport from sea to land or vice versa with a Dimension Door.",
 
 	"vcmi.settingsMainWindow.generalTab.hover" : "General",
 	"vcmi.settingsMainWindow.generalTab.help"     : "Switches to General Options tab, which contains settings related to general game client behavior.",

+ 537 - 0
Mods/vcmi/config/vcmi/portuguese.json

@@ -0,0 +1,537 @@
+{
+	"vcmi.adventureMap.monsterThreat.title"     : "\n\nAmeaça: ",
+	"vcmi.adventureMap.monsterThreat.levels.0"  : "Sem Esforço",
+	"vcmi.adventureMap.monsterThreat.levels.1"  : "Muito Fraca",
+	"vcmi.adventureMap.monsterThreat.levels.2"  : "Fraca",
+	"vcmi.adventureMap.monsterThreat.levels.3"  : "Um pouco mais fraca",
+	"vcmi.adventureMap.monsterThreat.levels.4"  : "Igual",
+	"vcmi.adventureMap.monsterThreat.levels.5"  : "Um pouco mais forte",
+	"vcmi.adventureMap.monsterThreat.levels.6"  : "Forte",
+	"vcmi.adventureMap.monsterThreat.levels.7"  : "Muito Forte",
+	"vcmi.adventureMap.monsterThreat.levels.8"  : "Desafiante",
+	"vcmi.adventureMap.monsterThreat.levels.9"  : "Dominante",
+	"vcmi.adventureMap.monsterThreat.levels.10" : "Mortal",
+	"vcmi.adventureMap.monsterThreat.levels.11" : "Impossível",
+
+	"vcmi.adventureMap.confirmRestartGame"               : "Tem certeza de que deseja reiniciar o jogo?",
+	"vcmi.adventureMap.noTownWithMarket"                 : "Não há mercados disponíveis!",
+	"vcmi.adventureMap.noTownWithTavern"                 : "Não há cidades disponíveis com tavernas!",
+	"vcmi.adventureMap.spellUnknownProblem"              : "Há um problema desconhecido com este feitiço! Não há mais informações disponíveis.",
+	"vcmi.adventureMap.playerAttacked"                   : "O jogador foi atacado: %s",
+	"vcmi.adventureMap.moveCostDetails"                  : "Pontos de movimento - Custo: %TURNS turnos + %POINTS pontos, Pontos restantes: %REMAINING",
+	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Pontos de movimento - Custo: %POINTS pontos, Pontos restantes: %REMAINING",
+	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Desculpe, a repetição do turno do oponente ainda não está implementada!",
+
+	"vcmi.capitalColors.0" : "Vermelho",
+	"vcmi.capitalColors.1" : "Azul",
+	"vcmi.capitalColors.2" : "Bege",
+	"vcmi.capitalColors.3" : "Verde",
+	"vcmi.capitalColors.4" : "Laranja",
+	"vcmi.capitalColors.5" : "Roxo",
+	"vcmi.capitalColors.6" : "Turquesa",
+	"vcmi.capitalColors.7" : "Rosa",
+	
+	"vcmi.heroOverview.startingArmy" : "Unidades Iniciais",
+	"vcmi.heroOverview.warMachine" : "Máquinas de Guerra",
+	"vcmi.heroOverview.secondarySkills" : "Habilidades Secundárias",
+	"vcmi.heroOverview.spells" : "Feitiços",
+	
+	"vcmi.radialWheel.mergeSameUnit" : "Mesclar criaturas iguais",
+	"vcmi.radialWheel.fillSingleUnit" : "Preencher com criaturas únicas",
+	"vcmi.radialWheel.splitSingleUnit" : "Dividir uma criatura única",
+	"vcmi.radialWheel.splitUnitEqually" : "Dividir criaturas igualmente",
+	"vcmi.radialWheel.moveUnit" : "Mover criaturas para outro exército",
+	"vcmi.radialWheel.splitUnit" : "Dividir criatura para outro espaço",
+	
+	"vcmi.radialWheel.heroGetArmy" : "Obter exército de outro herói",
+	"vcmi.radialWheel.heroSwapArmy" : "Trocar exército com outro herói",
+	"vcmi.radialWheel.heroExchange" : "Abrir troca de heróis",
+	"vcmi.radialWheel.heroGetArtifacts" : "Obter artefatos de outro herói",
+	"vcmi.radialWheel.heroSwapArtifacts" : "Trocar artefatos com outro herói",
+	"vcmi.radialWheel.heroDismiss" : "Dispensar herói",
+
+	"vcmi.radialWheel.moveTop" : "Mover para o topo",
+	"vcmi.radialWheel.moveUp" : "Mover para cima",
+	"vcmi.radialWheel.moveDown" : "Mover para baixo",
+	"vcmi.radialWheel.moveBottom" : "Mover para o fundo",
+
+	"vcmi.spellBook.search" : "procurar...",
+
+	"vcmi.mainMenu.serverConnecting" : "Conectando...",
+	"vcmi.mainMenu.serverAddressEnter" : "Insira o endereço:",
+	"vcmi.mainMenu.serverConnectionFailed" : "Falha ao conectar",
+	"vcmi.mainMenu.serverClosing" : "Fechando...",
+	"vcmi.mainMenu.hostTCP" : "Hospedar jogo TCP/IP",
+	"vcmi.mainMenu.joinTCP" : "Entrar em jogo TCP/IP",
+	
+	"vcmi.lobby.filepath" : "Caminho do arquivo",
+	"vcmi.lobby.creationDate" : "Data de criação",
+	"vcmi.lobby.scenarioName" : "Nome do cenário",
+	"vcmi.lobby.mapPreview" : "Visualização do mapa",
+	"vcmi.lobby.noPreview" : "sem visualização",
+	"vcmi.lobby.noUnderground" : "sem subterrâneo",
+	"vcmi.lobby.sortDate" : "Classifica mapas por data de alteração",
+	"vcmi.lobby.backToLobby" : "Voltar para a sala de espera",
+	
+	"vcmi.lobby.login.title" : "Sala de Espera Online do VCMI",
+	"vcmi.lobby.login.username" : "Nome de usuário:",
+	"vcmi.lobby.login.connecting" : "Conectando...",
+	"vcmi.lobby.login.error" : "Erro de conexão: %s",
+	"vcmi.lobby.login.create" : "Nova Conta",
+	"vcmi.lobby.login.login" : "Login",
+	"vcmi.lobby.login.as" : "Logar como %s",
+	"vcmi.lobby.header.rooms" : "Salas de Jogo - %d",
+	"vcmi.lobby.header.channels" : "Canais de Bate-papo",
+	"vcmi.lobby.header.chat.global" : "Bate-papo Global do Jogo - %s", // %s -> nome do idioma
+	"vcmi.lobby.header.chat.match" : "Bate-papo do jogo anterior em %s", // %s -> data e hora de início do jogo
+	"vcmi.lobby.header.chat.player" : "Bate-papo privado com %s", // %s -> apelido de outro jogador
+	"vcmi.lobby.header.history" : "Seus Jogos Anteriores",
+	"vcmi.lobby.header.players" : "Jogadores Online - %d",
+	"vcmi.lobby.match.solo" : "Jogo para um jogador",
+	"vcmi.lobby.match.duel" : "Jogo com %s", // %s -> apelido de outro jogador
+	"vcmi.lobby.match.multi" : "%d jogadores",
+	"vcmi.lobby.room.create" : "Criar Nova Sala",
+	"vcmi.lobby.room.players.limit" : "Limite de Jogadores",
+	"vcmi.lobby.room.description.public" : "Qualquer jogador pode entrar na sala pública.",
+	"vcmi.lobby.room.description.private" : "Apenas jogadores convidados podem entrar na sala privada.",
+	"vcmi.lobby.room.description.new" : "Para iniciar o jogo, selecione um cenário ou configure um mapa aleatório.",
+	"vcmi.lobby.room.description.load" : "Para iniciar o jogo, use um de seus jogos salvos.",
+	"vcmi.lobby.room.description.limit" : "Até %d jogadores podem entrar na sua sala, incluindo você.",
+	"vcmi.lobby.invite.header" : "Convidar Jogadores",
+	"vcmi.lobby.invite.notification" : "O jogador te convidou para a sala de jogo dele. Agora você pode entrar na sala privada dele.",
+	"vcmi.lobby.room.new" : "Novo Jogo",
+	"vcmi.lobby.room.load" : "Carregar Jogo",
+	"vcmi.lobby.room.type" : "Tipo de Sala",
+	"vcmi.lobby.room.mode" : "Modo de Jogo",
+	"vcmi.lobby.room.state.public" : "Pública",
+	"vcmi.lobby.room.state.private" : "Privada",
+	"vcmi.lobby.room.state.busy" : "Em Jogo",
+	"vcmi.lobby.room.state.invited" : "Convidado",
+
+	"vcmi.client.errors.invalidMap" : "{Mapa ou campanha inválido}\n\nFalha ao iniciar o jogo! O mapa ou campanha selecionado pode ser inválido ou corrompido. Motivo:\n%s",
+	"vcmi.client.errors.missingCampaigns" : "{Arquivos de dados ausentes}\n\nOs arquivos de dados das campanhas não foram encontrados! Você pode estar usando arquivos de dados incompletos ou corrompidos do Heroes 3. Por favor, reinstale os dados do jogo.",
+	"vcmi.server.errors.disconnected" : "{Erro de Rede}\n\nA conexão com o servidor do jogo foi perdida!",
+	"vcmi.server.errors.existingProcess" : "Outro processo do servidor VCMI está em execução. Por favor, termine-o antes de iniciar um novo jogo.",
+	"vcmi.server.errors.modsToEnable"    : "{Os seguintes mods são necessários}",
+	"vcmi.server.errors.modsToDisable"   : "{Os seguintes mods devem ser desativados}",
+	"vcmi.server.errors.modNoDependency" : "Falha ao carregar mod {'%s'}!\n Ele depende do mod {'%s'} que não está ativo!\n",
+	"vcmi.server.errors.modConflict" : "Falha ao carregar mod {'%s'}!\n Conflita com o mod ativo {'%s'}!\n",
+	"vcmi.server.errors.unknownEntity" : "Falha ao carregar salvamento! Entidade desconhecida '%s' encontrada no jogo salvo! O salvamento pode não ser compatível com a versão atualmente instalada dos mods!",
+
+	"vcmi.settingsMainWindow.generalTab.hover" : "Geral",
+	"vcmi.settingsMainWindow.generalTab.help"     : "Muda para a aba de Opções Gerais, que contém configurações relacionadas ao comportamento geral do cliente do jogo.",
+	"vcmi.settingsMainWindow.battleTab.hover" : "Batalha",
+	"vcmi.settingsMainWindow.battleTab.help"     : "Muda para a aba de Opções de Batalha, que permite configurar o comportamento do jogo durante as batalhas.",
+	"vcmi.settingsMainWindow.adventureTab.hover" : "Mapa de Aventura",
+	"vcmi.settingsMainWindow.adventureTab.help"  : "Muda para a aba de Opções do Mapa de Aventura (o mapa de aventura é a seção do jogo onde os jogadores podem controlar os movimentos de seus heróis).",
+
+	"vcmi.systemOptions.videoGroup" : "Configurações de Vídeo",
+	"vcmi.systemOptions.audioGroup" : "Configurações de Áudio",
+	"vcmi.systemOptions.otherGroup" : "Outras Configurações", // não utilizado no momento
+	"vcmi.systemOptions.townsGroup" : "Tela da Cidade",
+
+	"vcmi.systemOptions.fullscreenBorderless.hover" : "Tela Cheia (sem bordas)",
+	"vcmi.systemOptions.fullscreenBorderless.help"  : "{Tela Cheia sem Bordas}\n\nSe selecionado, o VCMI será executado em modo de tela cheia sem bordas. Neste modo, o jogo sempre usará a mesma resolução que a área de trabalho, ignorando a resolução selecionada.",
+	"vcmi.systemOptions.fullscreenExclusive.hover"  : "Tela Cheia (exclusiva)",
+	"vcmi.systemOptions.fullscreenExclusive.help"   : "{Tela Cheia}\n\nSe selecionado, o VCMI será executado em modo de tela cheia exclusiva. Neste modo, o jogo mudará a resolução do monitor para a resolução selecionada.",
+	"vcmi.systemOptions.resolutionButton.hover" : "Resolução: %wx%h",
+	"vcmi.systemOptions.resolutionButton.help"  : "{Selecionar Resolução}\n\nMuda a resolução da tela do jogo.",
+	"vcmi.systemOptions.resolutionMenu.hover"   : "Selecionar Resolução",
+	"vcmi.systemOptions.resolutionMenu.help"    : "Muda a resolução da tela do jogo.",
+	"vcmi.systemOptions.scalingButton.hover"   : "Escala da Interface: %p%",
+	"vcmi.systemOptions.scalingButton.help"    : "{Escala da Interface}\n\nAlterar escala da interface do jogo.",
+	"vcmi.systemOptions.scalingMenu.hover"     : "Selecionar Escala da Interface",
+	"vcmi.systemOptions.scalingMenu.help"      : "Altera a escala da interface do jogo.",
+	"vcmi.systemOptions.longTouchButton.hover"   : "Intervalo de Toque Longo: %d ms", // Translation note: "ms" = "milliseconds"
+	"vcmi.systemOptions.longTouchButton.help"    : "{Intervalo de Toque Longo}\n\nAo usar a tela sensível ao toque, as janelas pop-up aparecerão após tocar na tela por uma duração especificada, em milissegundos.",
+	"vcmi.systemOptions.longTouchMenu.hover"     : "Selecionar Intervalo de Toque Longo",
+	"vcmi.systemOptions.longTouchMenu.help"      : "Muda a duração do intervalo de toque longo.",
+	"vcmi.systemOptions.longTouchMenu.entry"     : "%d milissegundos",
+	"vcmi.systemOptions.framerateButton.hover"  : "Mostrar FPS",
+	"vcmi.systemOptions.framerateButton.help"   : "{Mostrar FPS}\n\nAtiva ou desativa a visibilidade do contador de Quadros Por Segundo no canto da janela do jogo.",
+	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Resposta tátil",
+	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Resposta tátil}\n\nAtiva ou desativa a resposta tátil nos toques na tela.",
+	"vcmi.systemOptions.enableUiEnhancementsButton.hover"  : "Aprimoramentos da Interface",
+	"vcmi.systemOptions.enableUiEnhancementsButton.help"   : "{Aprimoramentos da Interface}\n\nAtiva ou desativa várias melhorias de interface. Como um botão de mochila etc. Desative para ter uma experiência mais clássica.",
+	"vcmi.systemOptions.enableLargeSpellbookButton.hover"  : "Grimório Grande",
+	"vcmi.systemOptions.enableLargeSpellbookButton.help"   : "{Grimório Grande}\n\nAtiva um grimório maior que comporta mais feitiços por página. A animação de mudança de página do grimório não funciona com esta configuração ativada.",
+	"vcmi.systemOptions.audioMuteFocus.hover"  : "Silenciar na inatividade",
+	"vcmi.systemOptions.audioMuteFocus.help"   : "{Silenciar na inatividade}\n\nSilencia o áudio quando a janela está inativa. As exceções são mensagens no jogo e som de novo turno.",
+
+	"vcmi.adventureOptions.infoBarPick.hover" : "Mensagens no Painel de Informações",
+	"vcmi.adventureOptions.infoBarPick.help" : "{Mostra as Mensagens no Painel de Informações}\n\nSempre que possível, as mensagens do jogo provenientes de objetos no mapa serão mostradas no painel de informações, em vez de aparecerem em uma janela separada.",
+	"vcmi.adventureOptions.numericQuantities.hover" : "Quantidades Numéricas de Criaturas",
+	"vcmi.adventureOptions.numericQuantities.help" : "{Quantidades Numéricas de Criaturas}\n\nMostra as quantidades aproximadas de criaturas inimigas no formato numérico A-B.",
+	"vcmi.adventureOptions.forceMovementInfo.hover" : "Mostrar Sempre o Custo de Movimento",
+	"vcmi.adventureOptions.forceMovementInfo.help" : "{Mostrar Sempre o Custo de Movimento}\n\nSempre mostra os dados de pontos de movimento na barra de status (em vez de apenas visualizá-los enquanto você mantém pressionada a tecla ALT).",
+	"vcmi.adventureOptions.showGrid.hover" : "Mostrar Grade",
+	"vcmi.adventureOptions.showGrid.help" : "{Mostrar Grade}\n\nMostra a sobreposição da grade, destacando as fronteiras entre as telhas do mapa de aventura.",
+	"vcmi.adventureOptions.borderScroll.hover" : "Rolagem de Borda",
+	"vcmi.adventureOptions.borderScroll.help" : "{Rolagem de Borda}\n\nFaz o mapa de aventura rolar quando o cursor está adjacente à borda da janela. Pode ser desativado mantendo pressionada a tecla CTRL.",
+	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Gerenciar Criaturas no Painel de Info.",
+	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Gerencia as Criaturas no Painel de Informações}\n\nPermite reorganizar criaturas no painel de informações em vez de alternar entre os componentes padrão.",
+	"vcmi.adventureOptions.leftButtonDrag.hover" : "Arrastar Mapa com o Botão Esquerdo",
+	"vcmi.adventureOptions.leftButtonDrag.help" : "{Arrastar Mapa com o Botão Esquerdo}\n\nQuando ativado, mover o mouse com o botão esquerdo pressionado irá arrastar a visualização do mapa de aventura.",
+	"vcmi.adventureOptions.smoothDragging.hover" : "Arrastar Suavemente o Mapa",
+	"vcmi.adventureOptions.smoothDragging.help" : "{Arrasta o Mapa Suavemente}\n\nQuando ativado, o arrasto do mapa tem um efeito de movimento moderno.",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Omitir Efeitos de Desvanecimento",
+	"vcmi.adventureOptions.skipAdventureMapAnimations.help" : "{Omitir Efeitos de Desvanecimento}\n\nQuando ativado, omite o desvanecimento de objetos e efeitos semelhantes (coleta de recursos, embarque em navios etc). Torna a interface do usuário mais reativa em alguns casos em detrimento da estética. Especialmente útil em jogos PvP. Para obter velocidade de movimento máxima, o pulo está ativo independentemente desta configuração.",
+	"vcmi.adventureOptions.mapScrollSpeed1.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed5.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed6.hover": "",
+	"vcmi.adventureOptions.mapScrollSpeed1.help" : "Define a velocidade de rolagem do mapa como muito lenta.",
+	"vcmi.adventureOptions.mapScrollSpeed5.help" : "Define a velocidade de rolagem do mapa como muito rápida.",
+	"vcmi.adventureOptions.mapScrollSpeed6.help" : "Define a velocidade de rolagem do mapa como instantânea.",
+	"vcmi.adventureOptions.hideBackground.hover" : "Ocultar Fundo",
+	"vcmi.adventureOptions.hideBackground.help" : "{Ocultar Fundo}\n\nOculta o mapa de aventura no fundo e mostra uma textura em vez disso.",
+
+	"vcmi.battleOptions.queueSizeLabel.hover": "Mostrar Fila de Ordem de Turno",
+	"vcmi.battleOptions.queueSizeNoneButton.hover": "DESL.",
+	"vcmi.battleOptions.queueSizeAutoButton.hover": "AUTO.",
+	"vcmi.battleOptions.queueSizeSmallButton.hover": "PEQU.",
+	"vcmi.battleOptions.queueSizeBigButton.hover": "GRAN.",
+	"vcmi.battleOptions.queueSizeNoneButton.help": "Não exibir Fila de Ordem de Turno.",
+	"vcmi.battleOptions.queueSizeAutoButton.help": "Ajusta automaticamente o tamanho da fila de ordem de turno com base na resolução do jogo (o tamanho PEQUENO é usado ao jogar o jogo em uma resolução com altura inferior a 700 pixels, o tamanho GRANDE é usado caso contrário).",
+	"vcmi.battleOptions.queueSizeSmallButton.help": "Define o tamanho da fila de ordem de turno como PEQUENO.",
+	"vcmi.battleOptions.queueSizeBigButton.help": "Define o tamanho da fila de ordem de turno como GRANDE (não suportado se a altura da resolução do jogo for inferior a 700 pixels).",
+	"vcmi.battleOptions.animationsSpeed1.hover": "",
+	"vcmi.battleOptions.animationsSpeed5.hover": "",
+	"vcmi.battleOptions.animationsSpeed6.hover": "",
+	"vcmi.battleOptions.animationsSpeed1.help": "Define a velocidade da animação como muito lenta.",
+	"vcmi.battleOptions.animationsSpeed5.help": "Define a velocidade da animação como muito rápida.",
+	"vcmi.battleOptions.animationsSpeed6.help": "Define a velocidade da animação como instantânea.",
+	"vcmi.battleOptions.movementHighlightOnHover.hover": "Destacar Movimento ao Passar o Mouse",
+	"vcmi.battleOptions.movementHighlightOnHover.help": "{Destaca o Movimento ao Passar o Mouse}\n\nDestaca o alcance de movimento da unidade quando você passa o mouse sobre ela.",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.hover": "Mostrar Limites de Alcance de Atiradores",
+	"vcmi.battleOptions.rangeLimitHighlightOnHover.help": "{Mostra o Limites de Alcance dos Atiradores ao Passar o Mouse}\n\nMostra os limites de alcance do atirador quando você passa o mouse sobre ele.",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.hover": "Mostrar Janelas de Estatísticas de Heróis",
+	"vcmi.battleOptions.showStickyHeroInfoWindows.help": "{Mostra as Janelas de Estatísticas de Heróis}\n\nAlterna permanentemente as janelas de estatísticas dos heróis que mostram estatísticas primárias e pontos de feitiço.",
+	"vcmi.battleOptions.skipBattleIntroMusic.hover": "Pular Música de Introdução",
+	"vcmi.battleOptions.skipBattleIntroMusic.help": "{Pula a Música de Introdução}\n\nPermite ações durante a música de introdução que toca no início de cada batalha.",
+	"vcmi.battleOptions.endWithAutocombat.hover": "Terminar a Batalha",
+	"vcmi.battleOptions.endWithAutocombat.help": "{Termina a Batalha}\n\nO Combate Automático reproduz a batalha até o final instantâneo.",
+
+	"vcmi.adventureMap.revisitObject.hover" : "Revisitar Objeto",
+	"vcmi.adventureMap.revisitObject.help" : "{Revisitar Objeto}\n\nSe um herói estiver atualmente em um Objeto do Mapa, ele pode revisitar o local.",
+
+	"vcmi.battleWindow.pressKeyToSkipIntro" : "Pressione qualquer tecla para começar a batalha imediatamente",
+	"vcmi.battleWindow.damageEstimation.melee" : "Atacar %CREATURE (%DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.meleeKills" : "Atacar %CREATURE (%DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.ranged" : "Atirar em %CREATURE (%SHOTS, %DAMAGE).",
+	"vcmi.battleWindow.damageEstimation.rangedKills" : "Atirar em %CREATURE (%SHOTS, %DAMAGE, %KILLS).",
+	"vcmi.battleWindow.damageEstimation.shots" : "%d tiros restantes",
+	"vcmi.battleWindow.damageEstimation.shots.1" : "%d tiro restante",
+	"vcmi.battleWindow.damageEstimation.damage" : "%d de dano",
+	"vcmi.battleWindow.damageEstimation.damage.1" : "%d de dano",
+	"vcmi.battleWindow.damageEstimation.kills" : "%d morrerão",
+	"vcmi.battleWindow.damageEstimation.kills.1" : "%d morrerá",
+	"vcmi.battleWindow.killed" : "Eliminados",
+	"vcmi.battleWindow.accurateShot.resultDescription.0" : "%d %s morreram por tiros precisos!",
+	"vcmi.battleWindow.accurateShot.resultDescription.1" : "%d %s morreu com um tiro preciso!",
+	"vcmi.battleWindow.accurateShot.resultDescription.2" : "%d %s morreram por tiros precisos!",
+	"vcmi.battleWindow.endWithAutocombat" : "Tem certeza de que deseja terminar a batalha com o combate automático?",
+
+	"vcmi.battleResultsWindow.applyResultsLabel" : "Aplicar resultado da batalha",
+
+	"vcmi.tutorialWindow.title" : "Introdução à Tela Sensível ao Toque",
+	"vcmi.tutorialWindow.decription.RightClick" : "Toque e mantenha pressionado o elemento sobre o qual deseja clicar com o botão direito. Toque na área livre para fechar.",
+	"vcmi.tutorialWindow.decription.MapPanning" : "Toque e arraste com um dedo para mover o mapa.",
+	"vcmi.tutorialWindow.decription.MapZooming" : "Faça um movimento de pinça com dois dedos para alterar o zoom do mapa.",
+	"vcmi.tutorialWindow.decription.RadialWheel" : "Deslize para abrir a roda radial para várias ações, como gerenciamento de criaturas/heróis e ordenação de cidades.",
+	"vcmi.tutorialWindow.decription.BattleDirection" : "Para atacar de uma direção específica, deslize na direção de onde o ataque será feito.",
+	"vcmi.tutorialWindow.decription.BattleDirectionAbort" : "O gesto de direção do ataque pode ser cancelado se o dedo estiver longe o suficiente.",
+	"vcmi.tutorialWindow.decription.AbortSpell" : "Toque e mantenha pressionado para cancelar um feitiço.",
+
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.hover" : "Mostrar Criaturas Disponíveis",
+	"vcmi.otherOptions.availableCreaturesAsDwellingLabel.help" : "{Mostrar Criaturas Disponíveis}\n\nMostra o número de criaturas disponíveis para compra em vez de seu crescimento no resumo da cidade (canto inferior esquerdo da tela da cidade).",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.hover" : "Mostrar Crescimento Semanal de Criaturas",
+	"vcmi.otherOptions.creatureGrowthAsDwellingLabel.help" : "{Mostrar Crescimento Semanal de Criaturas}\n\nMostra o crescimento semanal das criaturas em vez da quantidade disponível no resumo da cidade (canto inferior esquerdo da tela da cidade).",
+	"vcmi.otherOptions.compactTownCreatureInfo.hover" : "Informações Compactas de Criaturas",
+	"vcmi.otherOptions.compactTownCreatureInfo.help" : "{Informações Compactas de Criaturas}\n\nMostra informações menores para criaturas da cidade no resumo da cidade (canto inferior esquerdo da tela da cidade).",
+
+	"vcmi.townHall.missingBase"             : "A construção base %s deve ser construída primeiro",
+	"vcmi.townHall.noCreaturesToRecruit"	: "Não há criaturas para recrutar!",
+	"vcmi.townHall.greetingManaVortex"	: "Ao se aproximar de %s, seu corpo é preenchido com nova energia. Você dobrou seus pontos de mana normais.",
+	"vcmi.townHall.greetingKnowledge"	: "Estudando os glifos de %s, você adquire uma visão dos segredos sobre o funcionamento de várias magias (+1 de Conhecimento).",
+	"vcmi.townHall.greetingSpellPower"	: "%s ensina novas maneiras de concentrar seus poderes mágicos (+1 de Força).",
+	"vcmi.townHall.greetingExperience"	: "Uma visita em %s ensina muitas habilidades novas (+1000 de Experiência).",
+	"vcmi.townHall.greetingAttack"		: "Algum tempo passado em %s permite que você aprenda habilidades de combate mais eficazes (+1 de Habilidade de Ataque).",
+	"vcmi.townHall.greetingDefence"		: "Ao passar um tempo em %s, os guerreiros experientes lá dentro te ensinam habilidades defensivas adicionais (+1 de Defesa).",
+	"vcmi.townHall.hasNotProduced"		: "%s ainda não produziu nada.",
+	"vcmi.townHall.hasProduced"		: "%s produziu %d %s nesta semana.",
+	"vcmi.townHall.greetingCustomBonus"	: "%s dá +%d %s%s",
+	"vcmi.townHall.greetingCustomUntil"	: " até a próxima batalha.",
+	"vcmi.townHall.greetingInTownMagicWell" : "%s restaurou seus pontos de mana para o máximo.",
+
+	"vcmi.logicalExpressions.anyOf"  : "Qualquer um dos seguintes:",
+	"vcmi.logicalExpressions.allOf"  : "Todos os seguintes:",
+	"vcmi.logicalExpressions.noneOf" : "Nenhum dos seguintes:",
+
+	"vcmi.heroWindow.openCommander.hover" : "Abrir janela de informações do comandante",
+	"vcmi.heroWindow.openCommander.help" : "Mostra detalhes sobre o comandante deste herói.",
+	"vcmi.heroWindow.openBackpack.hover" : "Abrir janela da mochila de artefatos",
+	"vcmi.heroWindow.openBackpack.help" : "Abre a janela que facilita o gerenciamento da mochila de artefatos.",
+
+	"vcmi.tavernWindow.inviteHero" : "Convidar herói",
+
+	"vcmi.commanderWindow.artifactMessage" : "Você quer devolver este artefato ao herói?",
+
+	"vcmi.creatureWindow.showBonuses.hover" : "Alternar para visualização de bônus",
+	"vcmi.creatureWindow.showBonuses.help" : "Exibe todos os bônus ativos do comandante.",
+	"vcmi.creatureWindow.showSkills.hover" : "Alternar para visualização de habilidades",
+	"vcmi.creatureWindow.showSkills.help" : "Exibe todas as habilidades aprendidas do comandante.",
+	"vcmi.creatureWindow.returnArtifact.hover" : "Devolver artefato",
+	"vcmi.creatureWindow.returnArtifact.help" : "Clique neste botão para devolver o artefato para a mochila do herói.",
+
+	"vcmi.questLog.hideComplete.hover" : "Ocultar missões completas",
+	"vcmi.questLog.hideComplete.help" : "Oculta todas as missões completas.",
+
+	"vcmi.randomMapTab.widgets.randomTemplate" : "(Aleatório)",
+	"vcmi.randomMapTab.widgets.templateLabel" : "Modelo",
+	"vcmi.randomMapTab.widgets.teamAlignmentsButton" : "Configuração...",
+	"vcmi.randomMapTab.widgets.teamAlignmentsLabel" : "Alinhamentos de Equipe",
+	"vcmi.randomMapTab.widgets.roadTypesLabel" : "Tipos de Estrada",
+
+	"vcmi.optionsTab.turnOptions.hover" : "Opções de Turno",
+	"vcmi.optionsTab.turnOptions.help" : "Selecione as opções de cronômetro do turno e turnos simultâneos",
+	"vcmi.optionsTab.selectPreset" : "Predefinição",
+
+	"vcmi.optionsTab.chessFieldBase.hover" : "Cronômetro Base",
+	"vcmi.optionsTab.chessFieldTurn.hover" : "Cronômetro do Turno",
+	"vcmi.optionsTab.chessFieldBattle.hover" : "Cronômetro da Batalha",
+	"vcmi.optionsTab.chessFieldUnit.hover" : "Cronômetro da Unidade",
+	"vcmi.optionsTab.chessFieldBase.help" : "Usado quando o {Cronômetro do Turno} chega a 0. Definido uma vez no início do jogo. Ao atingir zero, encerra o turno atual. Qualquer combate em curso terminará com uma derrota.",
+	"vcmi.optionsTab.chessFieldTurnAccumulate.help" : "Usado fora de combate ou quando o {Cronômetro da Batalha} se esgota. Restaurado a cada turno. O tempo restante é adicionado ao {Tempo Base} no final do turno.",
+	"vcmi.optionsTab.chessFieldTurnDiscard.help" : "Usado fora de combate ou quando o {Cronômetro da Batalha} se esgota. Restaurado a cada turno. Qualquer tempo não utilizado é perdido.",
+	"vcmi.optionsTab.chessFieldBattle.help" : "Usado em batalhas com a IA ou em combates PvP quando o {Cronômetro da Unidade} se esgota. Restaurado no início de cada combate.",
+	"vcmi.optionsTab.chessFieldUnitAccumulate.help" : "Usado ao selecionar ação da unidade em combates PvP. O tempo restante é adicionado ao {Cronômetro da Batalha} no final do turno da unidade.",
+	"vcmi.optionsTab.chessFieldUnitDiscard.help" : "Usado ao selecionar ação da unidade em combates PvP. Restaurado no início do turno de cada unidade. Qualquer tempo não utilizado é perdido.",
+
+	"vcmi.optionsTab.accumulate" : "Acumular",
+
+	"vcmi.optionsTab.simturnsTitle" : "Turnos Simultâneos",
+	"vcmi.optionsTab.simturnsMin.hover" : "Pelo menos por",
+	"vcmi.optionsTab.simturnsMax.hover" : "No máximo por",
+	"vcmi.optionsTab.simturnsAI.hover" : "(Experimental) Turnos Simultâneos da IA",
+	"vcmi.optionsTab.simturnsMin.help" : "Jogue simultaneamente por um número especificado de dias. Contatos entre jogadores durante este período estão bloqueados.",
+	"vcmi.optionsTab.simturnsMax.help" : "Jogue simultaneamente por um número especificado de dias ou até entrar em contato com outro jogador.",
+	"vcmi.optionsTab.simturnsAI.help" : "{Turnos Simultâneos da IA}\nOpção experimental. Permite que os jogadores da IA ajam ao mesmo tempo que o jogador humano quando os turnos simultâneos estão ativados.",
+
+	"vcmi.optionsTab.turnTime.select" : "Selecionar cronômetro do turno",
+	"vcmi.optionsTab.turnTime.unlimited" : "Tempo de turno ilimitado",
+	"vcmi.optionsTab.turnTime.classic.1" : "Cronômetro clássico: 1 minuto",
+	"vcmi.optionsTab.turnTime.classic.2" : "Cronômetro clássico: 2 minutos",
+	"vcmi.optionsTab.turnTime.classic.5" : "Cronômetro clássico: 5 minutos",
+	"vcmi.optionsTab.turnTime.classic.10" : "Cronômetro clássico: 10 minutos",
+	"vcmi.optionsTab.turnTime.classic.20" : "Cronômetro clássico: 20 minutos",
+	"vcmi.optionsTab.turnTime.classic.30" : "Cronômetro clássico: 30 minutos",
+	"vcmi.optionsTab.turnTime.chess.20" : "Xadrez: 20:00 + 10:00 + 02:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.16" : "Xadrez: 16:00 + 08:00 + 01:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.8" : "Xadrez: 08:00 + 04:00 + 01:00 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.4" : "Xadrez: 04:00 + 02:00 + 00:30 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.2" : "Xadrez: 02:00 + 01:00 + 00:15 + 00:00",
+	"vcmi.optionsTab.turnTime.chess.1" : "Xadrez: 01:00 + 01:00 + 00:00 + 00:00",
+
+	"vcmi.optionsTab.simturns.select" : "Selecionar turnos simultâneos",
+	"vcmi.optionsTab.simturns.none" : "Sem turnos simultâneos",
+	"vcmi.optionsTab.simturns.tillContactMax" : "Turnos simultâneos: Até o contato",
+	"vcmi.optionsTab.simturns.tillContact1" : "Turnos simultâneos: 1 semana, interromper no contato",
+	"vcmi.optionsTab.simturns.tillContact2" : "Turnos simultâneos: 2 semanas, interromper no contato",
+	"vcmi.optionsTab.simturns.tillContact4" : "Turnos simultâneos: 1 mês, interromper no contato",
+	"vcmi.optionsTab.simturns.blocked1" : "Turnos simultâneos: 1 semana, contatos bloqueados",
+	"vcmi.optionsTab.simturns.blocked2" : "Turnos simultâneos: 2 semanas, contatos bloqueados",
+	"vcmi.optionsTab.simturns.blocked4" : "Turnos simultâneos: 1 mês, contatos bloqueados",
+	
+	// Translation note: translate strings below using form that is correct for "0 days", "1 day" and "2 days" in your language
+	// Using this information, VCMI will automatically select correct plural form for every possible amount
+	"vcmi.optionsTab.simturns.days.0" : " %d dias",
+	"vcmi.optionsTab.simturns.days.1" : " %d dia",
+	"vcmi.optionsTab.simturns.days.2" : " %d dias",
+	"vcmi.optionsTab.simturns.weeks.0" : " %d semanas",
+	"vcmi.optionsTab.simturns.weeks.1" : " %d semana",
+	"vcmi.optionsTab.simturns.weeks.2" : " %d semanas",
+	"vcmi.optionsTab.simturns.months.0" : " %d meses",
+	"vcmi.optionsTab.simturns.months.1" : " %d mês",
+	"vcmi.optionsTab.simturns.months.2" : " %d meses",
+
+	"vcmi.optionsTab.extraOptions.hover" : "Opções Extras",
+	"vcmi.optionsTab.extraOptions.help" : "Configurações adicionais para o jogo",
+
+	"vcmi.optionsTab.cheatAllowed.hover" : "Permitir trapaças",
+	"vcmi.optionsTab.unlimitedReplay.hover" : "Repetição de batalha ilimitada",
+	"vcmi.optionsTab.cheatAllowed.help" : "{Permitir trapaças}\nPermite a entrada de trapaças durante o jogo.",
+	"vcmi.optionsTab.unlimitedReplay.help" : "{Repetição de batalha ilimitada}\nSem limite de repetição de batalhas.",
+
+	// Custom victory conditions for H3 campaigns and HotA maps
+	"vcmi.map.victoryCondition.daysPassed.toOthers" : "O inimigo conseguiu sobreviver até este dia. A vitória é deles!",
+	"vcmi.map.victoryCondition.daysPassed.toSelf" : "Parabéns! Você conseguiu sobreviver. A vitória é sua!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toOthers" : "O inimigo derrotou todos os monstros que assolam esta terra e reivindica a vitória!",
+	"vcmi.map.victoryCondition.eliminateMonsters.toSelf" : "Parabéns! Você derrotou todos os monstros que assolam esta terra e pode reivindicar a vitória!",
+	"vcmi.map.victoryCondition.collectArtifacts.message" : "Adquirir Três Artefatos",
+	"vcmi.map.victoryCondition.angelicAlliance.toSelf" : "Parabéns! Todos os seus inimigos foram derrotados e você tem a Aliança Angelical! A vitória é sua!",
+	"vcmi.map.victoryCondition.angelicAlliance.message" : "Derrote Todos os Inimigos e crie a Aliança Angelical",
+	"vcmi.map.victoryCondition.angelicAlliancePartLost.toSelf" : "Infelizmente, você perdeu parte da Aliança Angelical. Tudo está perdido.",
+
+	// few strings from WoG used by vcmi
+	"vcmi.stackExperience.description" : "» D e t a l h e s   d e   E x p e r i ê n c i a   d o   G r u p o «\n\nTipo de Criatura ................... : %s\nRank de Experiência ................. : %s (%i)\nPontos de Experiência ............... : %i\nPontos de Experiência até o Próximo Ranque .. : %i\nMáximo de Experiência por Batalha ... : %i%% (%i)\nNúmero de Criaturas no grupo .... : %i\nNovos Recrutas Máximos\n sem perder o Ranque atual .... : %i\nMultiplicador de Experiência ........... : %.2f\nMultiplicador de Upgrade .............. : %.2f\nExperiência após o Ranque 10 ........ : %i\nNovos Recrutas Máximos para permanecer\n no Ranque 10 se na Experiência Máxima : %i",
+	"vcmi.stackExperience.rank.0" : "Básico",
+	"vcmi.stackExperience.rank.1" : "Novato",
+	"vcmi.stackExperience.rank.2" : "Treinado",
+	"vcmi.stackExperience.rank.3" : "Experiente",
+	"vcmi.stackExperience.rank.4" : "Consagrado",
+	"vcmi.stackExperience.rank.5" : "Veterano",
+	"vcmi.stackExperience.rank.6" : "Adepto",
+	"vcmi.stackExperience.rank.7" : "Experiente",
+	"vcmi.stackExperience.rank.8" : "Elite",
+	"vcmi.stackExperience.rank.9" : "Mestre",
+	"vcmi.stackExperience.rank.10" : "Ás",
+
+	"core.bonus.ADDITIONAL_ATTACK.name" : "Ataque Duplo",
+	"core.bonus.ADDITIONAL_ATTACK.description" : "Ataca duas vezes",
+	"core.bonus.ADDITIONAL_RETALIATION.name" : "Contra-ataques Adicionais",
+	"core.bonus.ADDITIONAL_RETALIATION.description" : "Pode contra-atacar ${val} vezes extras",
+	"core.bonus.AIR_IMMUNITY.name" : "Imunidade ao Ar",
+	"core.bonus.AIR_IMMUNITY.description" : "Imune a todos os feitiços da escola de magia do Ar",
+	"core.bonus.ATTACKS_ALL_ADJACENT.name" : "Ataque em Todas as Direções",
+	"core.bonus.ATTACKS_ALL_ADJACENT.description" : "Ataca todos os inimigos adjacentes",
+	"core.bonus.BLOCKS_RETALIATION.name" : "Sem Contra-ataques",
+	"core.bonus.BLOCKS_RETALIATION.description" : "O inimigo não pode contra-atacar",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.name" : "Sem Contra-ataques à Distância",
+	"core.bonus.BLOCKS_RANGED_RETALIATION.description" : "O inimigo não pode contra-atacar usando um ataque à distância",
+	"core.bonus.CATAPULT.name" : "Catapulta",
+	"core.bonus.CATAPULT.description" : "Ataca as muralhas de cerco",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.name" : "Reduz Custo de Conjuração (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ALLY.description" : "Reduz o custo de conjuração de feitiços para o herói em ${val}",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.name" : "Absorvedor Mágico (${val})",
+	"core.bonus.CHANGES_SPELL_COST_FOR_ENEMY.description" : "Aumenta o custo de conjuração dos feitiços inimigos em ${val}",
+	"core.bonus.CHARGE_IMMUNITY.name" : "Imunidade à Carga",
+	"core.bonus.CHARGE_IMMUNITY.description" : "Imune à Carga do Cavaleiro e do Campeão",
+	"core.bonus.DARKNESS.name" : "Cobertura de Escuridão",
+	"core.bonus.DARKNESS.description" : "Cria um véu de escuridão com um raio de ${val}",
+	"core.bonus.DEATH_STARE.name" : "Olhar da Morte (${val}%)",
+	"core.bonus.DEATH_STARE.description" : "Tem ${val}% de chance de matar uma única criatura",
+	"core.bonus.DEFENSIVE_STANCE.name" : "Bônus de Defesa",
+	"core.bonus.DEFENSIVE_STANCE.description" : "+${val} de defesa ao se defender",
+	"core.bonus.DESTRUCTION.name" : "Destruição",
+	"core.bonus.DESTRUCTION.description" : "Tem ${val}% de chance de matar unidades extras após o ataque",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.name" : "Golpe de Morte",
+	"core.bonus.DOUBLE_DAMAGE_CHANCE.description" : "Tem ${val}% de chance de causar o dobro do dano base ao atacar",
+	"core.bonus.DRAGON_NATURE.name" : "Dragão",
+	"core.bonus.DRAGON_NATURE.description" : "A criatura possui Natureza de Dragão",
+	"core.bonus.EARTH_IMMUNITY.name" : "Imunidade à Terra",
+	"core.bonus.EARTH_IMMUNITY.description" : "Imune a todos os feitiços da escola de magia da Terra",
+	"core.bonus.ENCHANTER.name" : "Encantador",
+	"core.bonus.ENCHANTER.description" : "Pode lançar ${subtype.spell} em massa a cada turno",
+	"core.bonus.ENCHANTED.name" : "Encantado",
+	"core.bonus.ENCHANTED.description" : "Afetado por ${subtype.spell} permanente",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.name" : "Ignorar Ataque (${val}%)",
+	"core.bonus.ENEMY_ATTACK_REDUCTION.description" : "Ao ser atacado, ${val}% do ataque do atacante é ignorado",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.name" : "Ignorar Defesa (${val}%)",
+	"core.bonus.ENEMY_DEFENCE_REDUCTION.description" : "Ao atacar, ${val}% da defesa do defensor é ignorada",
+	"core.bonus.FIRE_IMMUNITY.name" : "Imunidade ao Fogo",
+	"core.bonus.FIRE_IMMUNITY.description" : "Imune a todos os feitiços da escola de magia do Fogo",
+	"core.bonus.FIRE_SHIELD.name" : "Escudo de Fogo (${val}%)",
+	"core.bonus.FIRE_SHIELD.description" : "Reflete parte do dano corpo a corpo",
+	"core.bonus.FIRST_STRIKE.name" : "Primeiro Ataque",
+	"core.bonus.FIRST_STRIKE.description" : "Esta criatura contra-atacará antes de ser atacada",
+	"core.bonus.FEAR.name" : "Medo",
+	"core.bonus.FEAR.description" : "Causa Medo em uma pilha inimiga",
+	"core.bonus.FEARLESS.name" : "Destemido",
+	"core.bonus.FEARLESS.description" : "Imune à habilidade de Medo",
+	"core.bonus.FEROCITY.name" : "Ferocidade",
+	"core.bonus.FEROCITY.description" : "Ataca ${val} vezes adicionais se matar alguém",
+	"core.bonus.FLYING.name" : "Voo",
+	"core.bonus.FLYING.description" : "Voar ao se mover (ignora obstáculos)",
+	"core.bonus.FREE_SHOOTING.name" : "Tiro Livre",
+	"core.bonus.FREE_SHOOTING.description" : "Pode usar ataques à distância em combate corpo a corpo",
+	"core.bonus.GARGOYLE.name" : "Gárgula",
+	"core.bonus.GARGOYLE.description" : "Não pode ser levantado ou curado",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.name" : "Redução de Dano (${val}%)",
+	"core.bonus.GENERAL_DAMAGE_REDUCTION.description" : "Reduz o dano físico de ataques à distância ou corpo a corpo",
+	"core.bonus.HATE.name" : "Odioso contra ${subtype.creature}",
+	"core.bonus.HATE.description" : "Causa ${val}% a mais de dano a ${subtype.creature}",
+	"core.bonus.HEALER.name" : "Curandeiro",
+	"core.bonus.HEALER.description" : "Cura unidades aliadas",
+	"core.bonus.HP_REGENERATION.name" : "Regeneração",
+	"core.bonus.HP_REGENERATION.description" : "Cura ${val} pontos de vida a cada rodada",
+	"core.bonus.JOUSTING.name" : "Carga do Campeão",
+	"core.bonus.JOUSTING.description" : "+${val}% de dano para cada hexágono percorrido",
+	"core.bonus.KING.name" : "Rei",
+	"core.bonus.KING.description" : "Vulnerável ao nível MATADOR ${val} ou superior",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.name" : "Imunidade a Feitiços 1-${val}",
+	"core.bonus.LEVEL_SPELL_IMMUNITY.description" : "Imune a feitiços dos níveis 1-${val}",
+	"core.bonus.LIMITED_SHOOTING_RANGE.name" : "Alcance de Tiro Limitado",
+	"core.bonus.LIMITED_SHOOTING_RANGE.description" : "Incapaz de mirar unidades a uma distância maior que ${val} hexágonos",
+	"core.bonus.LIFE_DRAIN.name" : "Drenar Vida (${val}%)",
+	"core.bonus.LIFE_DRAIN.description" : "Drena ${val}% do dano causado",
+	"core.bonus.MANA_CHANNELING.name" : "Canalização Mágica ${val}%",
+	"core.bonus.MANA_CHANNELING.description" : "Dá ao seu herói ${val}% da mana gasta pelo inimigo",
+	"core.bonus.MANA_DRAIN.name" : "Drenagem de Mana",
+	"core.bonus.MANA_DRAIN.description" : "Drena ${val} de mana a cada turno",
+	"core.bonus.MAGIC_MIRROR.name" : "Espelho Mágico (${val}%)",
+	"core.bonus.MAGIC_MIRROR.description" : "Tem ${val}% de chance de redirecionar um feitiço ofensivo para uma unidade inimiga",
+	"core.bonus.MAGIC_RESISTANCE.name" : "Resistência Mágica (${val}%)",
+	"core.bonus.MAGIC_RESISTANCE.description" : "Tem ${val}% de chance de resistir a um feitiço inimigo",
+	"core.bonus.MIND_IMMUNITY.name" : "Imunidade a Feitiços Mentais",
+	"core.bonus.MIND_IMMUNITY.description" : "Imune a feitiços do tipo Mental",
+	"core.bonus.NO_DISTANCE_PENALTY.name" : "Sem Penalidade de Distância",
+	"core.bonus.NO_DISTANCE_PENALTY.description" : "Causa dano total a qualquer distância",
+	"core.bonus.NO_MELEE_PENALTY.name" : "Sem Penalidade em Combate Corpo a Corpo",
+	"core.bonus.NO_MELEE_PENALTY.description" : "A criatura não tem Penalidade em Combate Corpo a Corpo",
+	"core.bonus.NO_MORALE.name" : "Moral Neutra",
+	"core.bonus.NO_MORALE.description" : "A criatura é imune aos efeitos de moral",
+	"core.bonus.NO_WALL_PENALTY.name" : "Sem Penalidade de Muralha",
+	"core.bonus.NO_WALL_PENALTY.description" : "Dano total durante cerco",
+	"core.bonus.NON_LIVING.name" : "Não Vivo",
+	"core.bonus.NON_LIVING.description" : "Imune a muitos efeitos",
+	"core.bonus.RANDOM_SPELLCASTER.name" : "Lançador de Feitiços Aleatório",
+	"core.bonus.RANDOM_SPELLCASTER.description" : "Pode lançar um feitiço aleatório",
+	"core.bonus.RANGED_RETALIATION.name" : "Contra-ataques à Distância",
+	"core.bonus.RANGED_RETALIATION.description" : "Pode realizar contra-ataques à distância",
+	"core.bonus.RECEPTIVE.name" : "Receptivo",
+	"core.bonus.RECEPTIVE.description" : "Sem Imunidade a Feitiços Amigáveis",
+	"core.bonus.REBIRTH.name" : "Renascimento (${val}%)",
+	"core.bonus.REBIRTH.description" : "${val}% da pilha ressurgirá após a morte",
+	"core.bonus.RETURN_AFTER_STRIKE.name" : "Atacar e Voltar",
+	"core.bonus.RETURN_AFTER_STRIKE.description" : "Volta após o ataque corpo a corpo",
+	"core.bonus.REVENGE.name" : "Vingança",
+	"core.bonus.REVENGE.description" : "Causa dano extra com base na saúde perdida do atacante em batalha",
+	"core.bonus.SHOOTER.name" : "À Distância",
+	"core.bonus.SHOOTER.description" : "A criatura pode atirar",
+	"core.bonus.SHOOTS_ALL_ADJACENT.name" : "Atirar em Tudo ao Redor",
+	"core.bonus.SHOOTS_ALL_ADJACENT.description" : "Os ataques à distância desta criatura atingem todos os alvos em uma pequena área",
+	"core.bonus.SOUL_STEAL.name" : "Roubo de Alma",
+	"core.bonus.SOUL_STEAL.description" : "Ganha ${val} novas criaturas para cada inimigo morto",
+	"core.bonus.SPELLCASTER.name" : "Lançador de Feitiços",
+	"core.bonus.SPELLCASTER.description" : "Pode lançar ${subtype.spell}",
+	"core.bonus.SPELL_AFTER_ATTACK.name" : "Lançar Após Ataque",
+	"core.bonus.SPELL_AFTER_ATTACK.description" : "Tem ${val}% de chance de lançar ${subtype.spell} após atacar",
+	"core.bonus.SPELL_BEFORE_ATTACK.name" : "Lançar Antes do Ataque",
+	"core.bonus.SPELL_BEFORE_ATTACK.description" : "Tem ${val}% de chance de lançar ${subtype.spell} antes de atacar",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.name" : "Resistência a Feitiços",
+	"core.bonus.SPELL_DAMAGE_REDUCTION.description" : "Dano de feitiços reduzido em ${val}%.",
+	"core.bonus.SPELL_IMMUNITY.name" : "Imunidade a Feitiços",
+	"core.bonus.SPELL_IMMUNITY.description" : "Imune a ${subtype.spell}",
+	"core.bonus.SPELL_LIKE_ATTACK.name" : "Ataque Similar a Feitiço",
+	"core.bonus.SPELL_LIKE_ATTACK.description" : "Ataques com ${subtype.spell}",
+	"core.bonus.SPELL_RESISTANCE_AURA.name" : "Aura de Resistência a Feitiços",
+	"core.bonus.SPELL_RESISTANCE_AURA.description" : "Pilhas próximas ganham ${val}% de resistência a magia",
+	"core.bonus.SUMMON_GUARDIANS.name" : "Invocar Guardiões",
+	"core.bonus.SUMMON_GUARDIANS.description" : "No início da batalha, invoca ${subtype.creature} (${val}%)",
+	"core.bonus.SYNERGY_TARGET.name" : "Alvo Sinergizável",
+	"core.bonus.SYNERGY_TARGET.description" : "Esta criatura é vulnerável ao efeito de sinergia",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.name" : "Sopro",
+	"core.bonus.TWO_HEX_ATTACK_BREATH.description" : "Ataque de Sopro (alcance de 2 hexágonos)",
+	"core.bonus.THREE_HEADED_ATTACK.name" : "Ataque das Três Cabeças",
+	"core.bonus.THREE_HEADED_ATTACK.description" : "Ataca três unidades adjacentes",
+	"core.bonus.TRANSMUTATION.name" : "Transmutação",
+	"core.bonus.TRANSMUTATION.description" : "${val}% de chance de transformar a unidade atacada em um tipo diferente",
+	"core.bonus.UNDEAD.name" : "Morto-vivo",
+	"core.bonus.UNDEAD.description" : "A criatura é um Morto-vivo",
+	"core.bonus.UNLIMITED_RETALIATIONS.name" : "Contra-ataques Ilimitadas",
+	"core.bonus.UNLIMITED_RETALIATIONS.description" : "Pode contra-atacar contra um número ilimitado de ataques",
+	"core.bonus.WATER_IMMUNITY.name" : "Imunidade à Água",
+	"core.bonus.WATER_IMMUNITY.description" : "Imune a todos os feitiços da escola de magia da Água",
+	"core.bonus.WIDE_BREATH.name" : "Sopro Amplo",
+	"core.bonus.WIDE_BREATH.description" : "Ataque de sopro amplo (vários hexágonos)"
+}

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

@@ -112,6 +112,8 @@
 	"vcmi.server.errors.modNoDependency" : "Не вдалося увімкнути мод {'%s'}!\n Модифікація потребує мод {'%s'} який зараз не активний!\n",
 	"vcmi.server.errors.modConflict" : "Не вдалося увімкнути мод {'%s'}!\n Конфліктує з активним модом {'%s'}!\n",
 	"vcmi.server.errors.unknownEntity" : "Не вдалося завантажити гру! У збереженій грі знайдено невідомий об'єкт '%s'! Це збереження може бути несумісним зі встановленою версією модифікацій!",
+	
+	"vcmi.dimensionDoor.seaToLandError" : "Неможливо телепортуватися з моря на сушу або навпаки за допомогою просторової брами",
 
 	"vcmi.settingsMainWindow.generalTab.hover" : "Загальні",
 	"vcmi.settingsMainWindow.generalTab.help"     : "Перемикає на вкладку загальних параметрів, яка містить налаштування, пов'язані із загальною поведінкою ігрового клієнта",

+ 11 - 0
Mods/vcmi/mod.json

@@ -55,6 +55,17 @@
 		]
 	},
 
+	"portuguese" : {
+		"name" : "VCMI - arquivos essenciais",
+		"description" : "Arquivos essenciais para executar o VCMI corretamente",
+		"author" : "Altieres Lima",
+
+		"skipValidation" : true,
+		"translations" : [
+			"config/vcmi/portuguese.json"
+		]
+	},
+
 	"russian" : {
 		"name" : "Ключевые файлы VCMI",
 		"description" : "Файлы, необходимые для полноценной работы VCMI",

+ 1 - 0
client/CGameInfo.cpp

@@ -37,6 +37,7 @@ void CGameInfo::setFromLib()
 	terrainTypeHandler = VLC->terrainTypeHandler;
 	battleFieldHandler = VLC->battlefieldsHandler;
 	obstacleHandler = VLC->obstacleHandler;
+	//TODO: biomeHandler?
 }
 
 const ArtifactService * CGameInfo::artifacts() const

+ 0 - 2
client/CMT.cpp

@@ -502,8 +502,6 @@ void handleQuit(bool ask)
 		return;
 	}
 
-	CCS->curh->set(Cursor::Map::POINTER);
-
 	if (LOCPLINT)
 		LOCPLINT->showYesNoDialog(CGI->generaltexth->allTexts[69], quitApplication, nullptr);
 	else

+ 14 - 6
client/CMakeLists.txt

@@ -128,21 +128,25 @@ set(client_SRCS
 	widgets/RadialMenu.cpp
 	widgets/markets/CAltarArtifacts.cpp
 	widgets/markets/CAltarCreatures.cpp
-	widgets/markets/CTradeBase.cpp
+	widgets/markets/CArtifactsBuying.cpp
+	widgets/markets/CArtifactsSelling.cpp
+	widgets/markets/CFreelancerGuild.cpp
+	widgets/markets/CMarketResources.cpp
+	widgets/markets/CTransferResources.cpp
+	widgets/markets/CMarketBase.cpp
 	widgets/markets/TradePanels.cpp
 
-	windows/CAltarWindow.cpp
 	windows/CCastleInterface.cpp
 	windows/CCreatureWindow.cpp
 	windows/CHeroOverview.cpp
 	windows/CHeroWindow.cpp
 	windows/CKingdomInterface.cpp
 	windows/CMapOverview.cpp
+	windows/CMarketWindow.cpp
 	windows/CMessage.cpp
 	windows/CPuzzleWindow.cpp
 	windows/CQuestLog.cpp
 	windows/CSpellWindow.cpp
-	windows/CTradeWindow.cpp
 	windows/CTutorialWindow.cpp
 	windows/CWindowObject.cpp
 	windows/CreaturePurchaseCard.cpp
@@ -316,10 +320,14 @@ set(client_HEADERS
 	widgets/RadialMenu.h
 	widgets/markets/CAltarArtifacts.h
 	widgets/markets/CAltarCreatures.h
-	widgets/markets/CTradeBase.h
+	widgets/markets/CArtifactsBuying.h
+	widgets/markets/CArtifactsSelling.h
+	widgets/markets/CFreelancerGuild.h
+	widgets/markets/CMarketResources.h
+	widgets/markets/CTransferResources.h
+	widgets/markets/CMarketBase.h
 	widgets/markets/TradePanels.h
 
-	windows/CAltarWindow.h
 	windows/CCastleInterface.h
 	windows/CCreatureWindow.h
 	windows/CHeroOverview.h
@@ -327,10 +335,10 @@ set(client_HEADERS
 	windows/CKingdomInterface.h
 	windows/CMessage.h
 	windows/CMapOverview.h
+	windows/CMarketWindow.h
 	windows/CPuzzleWindow.h
 	windows/CQuestLog.h
 	windows/CSpellWindow.h
-	windows/CTradeWindow.h
 	windows/CTutorialWindow.h
 	windows/CWindowObject.h
 	windows/CreaturePurchaseCard.h

+ 7 - 0
client/CMusicHandler.cpp

@@ -322,6 +322,13 @@ void CSoundHandler::setCallback(int channel, std::function<void()> function)
 		iter->second.push_back(function);
 }
 
+void CSoundHandler::resetCallback(int channel)
+{
+	boost::mutex::scoped_lock lockGuard(mutexCallbacks);
+
+	callbacks.erase(channel);
+}
+
 void CSoundHandler::soundFinishedCallback(int channel)
 {
 	boost::mutex::scoped_lock lockGuard(mutexCallbacks);

+ 1 - 0
client/CMusicHandler.h

@@ -85,6 +85,7 @@ public:
 	void stopSound(int handler);
 
 	void setCallback(int channel, std::function<void()> function);
+	void resetCallback(int channel);
 	void soundFinishedCallback(int channel);
 
 	int ambientGetRange() const;

+ 15 - 80
client/CPlayerInterface.cpp

@@ -49,15 +49,14 @@
 #include "widgets/CComponent.h"
 #include "widgets/CGarrisonInt.h"
 
-#include "windows/CAltarWindow.h"
 #include "windows/CCastleInterface.h"
 #include "windows/CCreatureWindow.h"
 #include "windows/CHeroWindow.h"
 #include "windows/CKingdomInterface.h"
+#include "windows/CMarketWindow.h"
 #include "windows/CPuzzleWindow.h"
 #include "windows/CQuestLog.h"
 #include "windows/CSpellWindow.h"
-#include "windows/CTradeWindow.h"
 #include "windows/CTutorialWindow.h"
 #include "windows/GUIClasses.h"
 #include "windows/InfoWindows.h"
@@ -194,6 +193,11 @@ void CPlayerInterface::playerEndsTurn(PlayerColor player)
 				GH.windows().popWindows(1);
 		}
 
+		if(castleInt)
+			castleInt->close();
+
+		castleInt = nullptr;
+
 		// remove all pending dialogs that do not expect query answer
 		vstd::erase_if(dialogs, [](const std::shared_ptr<CInfoWindow> & window){
 			return window->ID == QueryID::NONE;
@@ -420,8 +424,8 @@ void CPlayerInterface::heroPrimarySkillChanged(const CGHeroInstance * hero, Prim
 	EVENT_HANDLER_CALLED_BY_CLIENT;
 	if (which == PrimarySkill::EXPERIENCE)
 	{
-		for (auto ctw : GH.windows().findWindows<CAltarWindow>())
-			ctw->updateExpToLevel();
+		for(auto ctw : GH.windows().findWindows<CMarketWindow>())
+			ctw->updateHero();
 	}
 	else
 		adventureInt->onHeroChanged(hero);
@@ -450,8 +454,8 @@ void CPlayerInterface::heroMovePointsChanged(const CGHeroInstance * hero)
 void CPlayerInterface::receivedResource()
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	for (auto mw : GH.windows().findWindows<CMarketplaceWindow>())
-		mw->resourceChanged();
+	for (auto mw : GH.windows().findWindows<CMarketWindow>())
+		mw->updateResource();
 
 	GH.windows().totalRedraw();
 }
@@ -1556,31 +1560,6 @@ void CPlayerInterface::gameOver(PlayerColor player, const EVictoryLossCheckResul
 
 		GH.curInt = previousInterface;
 		LOCPLINT = previousInterface;
-
-		if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool()) //all human players eliminated
-		{
-			if(adventureInt)
-			{
-				GH.windows().popWindows(GH.windows().count());
-				adventureInt.reset();
-			}
-		}
-
-		if (victoryLossCheckResult.victory() && LOCPLINT == this)
-		{
-			// end game if current human player has won
-			CSH->sendClientDisconnecting();
-			requestReturningToMainMenu(true);
-		}
-		else if(CSH->howManyPlayerInterfaces() == 1 && !settings["session"]["spectate"].Bool())
-		{
-			//all human players eliminated
-			CSH->sendClientDisconnecting();
-			requestReturningToMainMenu(false);
-		}
-
-		if (GH.curInt == this)
-			GH.curInt = nullptr;
 	}
 }
 
@@ -1669,13 +1648,13 @@ void CPlayerInterface::showMarketWindow(const IMarket *market, const CGHeroInsta
 	};
 
 	if(market->allowsTrade(EMarketMode::ARTIFACT_EXP) && visitor->getAlignment() != EAlignment::EVIL)
-		GH.windows().createAndPushWindow<CAltarWindow>(market, visitor, onWindowClosed, EMarketMode::ARTIFACT_EXP);
+		GH.windows().createAndPushWindow<CMarketWindow>(market, visitor, onWindowClosed, EMarketMode::ARTIFACT_EXP);
 	else if(market->allowsTrade(EMarketMode::CREATURE_EXP) && visitor->getAlignment() != EAlignment::GOOD)
-		GH.windows().createAndPushWindow<CAltarWindow>(market, visitor, onWindowClosed, EMarketMode::CREATURE_EXP);
+		GH.windows().createAndPushWindow<CMarketWindow>(market, visitor, onWindowClosed, EMarketMode::CREATURE_EXP);
 	else if(market->allowsTrade(EMarketMode::CREATURE_UNDEAD))
 		GH.windows().createAndPushWindow<CTransformerWindow>(market, visitor, onWindowClosed);
 	else if(!market->availableModes().empty())
-		GH.windows().createAndPushWindow<CMarketplaceWindow>(market, visitor, onWindowClosed, market->availableModes().front());
+		GH.windows().createAndPushWindow<CMarketWindow>(market, visitor, onWindowClosed, market->availableModes().front());
 }
 
 void CPlayerInterface::showUniversityWindow(const IMarket *market, const CGHeroInstance *visitor, QueryID queryID)
@@ -1696,8 +1675,8 @@ void CPlayerInterface::showHillFortWindow(const CGObjectInstance *object, const
 void CPlayerInterface::availableArtifactsChanged(const CGBlackMarket * bm)
 {
 	EVENT_HANDLER_CALLED_BY_CLIENT;
-	for (auto cmw : GH.windows().findWindows<CMarketplaceWindow>())
-		cmw->artifactsChanged(false);
+	for (auto cmw : GH.windows().findWindows<CMarketWindow>())
+		cmw->updateArtifacts();
 }
 
 void CPlayerInterface::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID)
@@ -1734,50 +1713,6 @@ void CPlayerInterface::showShipyardDialogOrProblemPopup(const IShipyard *obj)
 		showShipyardDialog(obj);
 }
 
-void CPlayerInterface::requestReturningToMainMenu(bool won)
-{
-	HighScoreParameter param;
-	param.difficulty = cb->getStartInfo()->difficulty;
-	param.day = cb->getDate();
-	param.townAmount = cb->howManyTowns();
-	param.usedCheat = cb->getPlayerState(*cb->getPlayerID())->cheated;
-	param.hasGrail = false;
-	for(const CGHeroInstance * h : cb->getHeroesInfo())
-		if(h->hasArt(ArtifactID::GRAIL))
-			param.hasGrail = true;
-	for(const CGTownInstance * t : cb->getTownsInfo())
-		if(t->builtBuildings.count(BuildingID::GRAIL))
-			param.hasGrail = true;
-	param.allDefeated = true;
-	for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player)
-	{
-		auto ps = cb->getPlayerState(player, false);
-		if(ps && player != *cb->getPlayerID())
-			if(!ps->checkVanquished())
-				param.allDefeated = false;
-	}
-	param.scenarioName = cb->getMapHeader()->name.toString();
-	param.playerName = cb->getStartInfo()->playerInfos.find(*cb->getPlayerID())->second.name;
-	HighScoreCalculation highScoreCalc;
-	highScoreCalc.parameters.push_back(param);
-	highScoreCalc.isCampaign = false;
-
-	if(won && cb->getStartInfo()->campState)
-		CSH->startCampaignScenario(param, cb->getStartInfo()->campState);
-	else
-	{
-		GH.dispatchMainThread(
-			[won, highScoreCalc]()
-			{
-				CSH->endGameplay();
-				GH.defActionsDef = 63;
-				CMM->menu->switchToTab("main");
-				GH.windows().createAndPushWindow<CHighScoreInputScreen>(won, highScoreCalc);
-			}
-		);
-	}
-}
-
 void CPlayerInterface::askToAssembleArtifact(const ArtifactLocation &al)
 {
 	if(auto hero = cb->getHero(al.artHolder))

+ 97 - 34
client/CServerHandler.cpp

@@ -34,7 +34,9 @@
 #include "../lib/TurnTimerInfo.h"
 #include "../lib/VCMIDirs.h"
 #include "../lib/campaign/CampaignState.h"
+#include "../lib/CPlayerState.h"
 #include "../lib/mapping/CMapInfo.h"
+#include "../lib/mapObjects/CGTownInstance.h"
 #include "../lib/mapObjects/MiscObjects.h"
 #include "../lib/modding/ModIncompatibility.h"
 #include "../lib/rmg/CMapGenOptions.h"
@@ -144,6 +146,11 @@ CServerHandler::CServerHandler()
 	registerTypesLobbyPacks(*applier);
 }
 
+void CServerHandler::setHighScoreCalc(const std::shared_ptr<HighScoreCalculation> &newHighScoreCalc)
+{
+	campaignScoreCalculator = newHighScoreCalc;
+}
+
 void CServerHandler::threadRunNetwork()
 {
 	logGlobal->info("Starting network thread");
@@ -625,7 +632,7 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta
 	if(CMM)
 		CMM->disable();
 
-	highScoreCalc = nullptr;
+	campaignScoreCalculator = nullptr;
 
 	switch(si->mode)
 	{
@@ -646,11 +653,62 @@ void CServerHandler::startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameSta
 	setState(EClientState::GAMEPLAY);
 }
 
+HighScoreParameter CServerHandler::prepareHighScores(PlayerColor player, bool victory)
+{
+	const auto * gs = client->gameState();
+	const auto * playerState = gs->getPlayerState(player);
+
+	HighScoreParameter param;
+	param.difficulty = gs->getStartInfo()->difficulty;
+	param.day = gs->getDate();
+	param.townAmount = gs->howManyTowns(player);
+	param.usedCheat = gs->getPlayerState(player)->cheated;
+	param.hasGrail = false;
+	for(const CGHeroInstance * h : playerState->heroes)
+		if(h->hasArt(ArtifactID::GRAIL))
+			param.hasGrail = true;
+	for(const CGTownInstance * t : playerState->towns)
+		if(t->builtBuildings.count(BuildingID::GRAIL))
+			param.hasGrail = true;
+	param.allDefeated = true;
+	for (PlayerColor otherPlayer(0); otherPlayer < PlayerColor::PLAYER_LIMIT; ++otherPlayer)
+	{
+		auto ps = gs->getPlayerState(otherPlayer, false);
+		if(ps && otherPlayer != player && !ps->checkVanquished())
+			param.allDefeated = false;
+	}
+	param.scenarioName = gs->getMapHeader()->name.toString();
+	param.playerName = gs->getStartInfo()->playerInfos.find(player)->second.name;
+
+	return param;
+}
+
+void CServerHandler::showHighScoresAndEndGameplay(PlayerColor player, bool victory)
+{
+	HighScoreParameter param = prepareHighScores(player, victory);
+
+	if(victory && client->gameState()->getStartInfo()->campState)
+	{
+		startCampaignScenario(param, client->gameState()->getStartInfo()->campState);
+	}
+	else
+	{
+		HighScoreCalculation scenarioHighScores;
+		scenarioHighScores.parameters.push_back(param);
+		scenarioHighScores.isCampaign = false;
+
+		endGameplay();
+		GH.defActionsDef = 63;
+		CMM->menu->switchToTab("main");
+		GH.windows().createAndPushWindow<CHighScoreInputScreen>(victory, scenarioHighScores);
+	}
+}
+
 void CServerHandler::endGameplay()
 {
 	// Game is ending
 	// Tell the network thread to reach a stable state
-	CSH->sendClientDisconnecting();
+	sendClientDisconnecting();
 	logNetwork->info("Closed connection.");
 
 	client->endGame();
@@ -682,49 +740,46 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared
 	if (!cs)
 		ourCampaign = si->campState;
 
-	if(highScoreCalc == nullptr)
+	if(campaignScoreCalculator == nullptr)
 	{
-		highScoreCalc = std::make_shared<HighScoreCalculation>();
-		highScoreCalc->isCampaign = true;
-		highScoreCalc->parameters.clear();
+		campaignScoreCalculator = std::make_shared<HighScoreCalculation>();
+		campaignScoreCalculator->isCampaign = true;
+		campaignScoreCalculator->parameters.clear();
 	}
 	param.campaignName = cs->getNameTranslated();
-	highScoreCalc->parameters.push_back(param);
+	campaignScoreCalculator->parameters.push_back(param);
 
-	GH.dispatchMainThread([ourCampaign, this]()
-	{
-		CSH->endGameplay();
+	endGameplay();
 
-		auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog;
-		auto finisher = [=]()
+	auto & epilogue = ourCampaign->scenario(*ourCampaign->lastScenario()).epilog;
+	auto finisher = [this, ourCampaign]()
+	{
+		if(ourCampaign->campaignSet != "" && ourCampaign->isCampaignFinished())
 		{
-			if(ourCampaign->campaignSet != "" && ourCampaign->isCampaignFinished())
-			{
-				Settings entry = persistentStorage.write["completedCampaigns"][ourCampaign->getFilename()];
-				entry->Bool() = true;
-			}
-
-			GH.windows().pushWindow(CMM);
-			GH.windows().pushWindow(CMM->menu);
+			Settings entry = persistentStorage.write["completedCampaigns"][ourCampaign->getFilename()];
+			entry->Bool() = true;
+		}
 
-			if(!ourCampaign->isCampaignFinished())
-				CMM->openCampaignLobby(ourCampaign);
-			else
-			{
-				CMM->openCampaignScreen(ourCampaign->campaignSet);
-				GH.windows().createAndPushWindow<CHighScoreInputScreen>(true, *highScoreCalc);
-			}
-		};
+		GH.windows().pushWindow(CMM);
+		GH.windows().pushWindow(CMM->menu);
 
-		if(epilogue.hasPrologEpilog)
-		{
-			GH.windows().createAndPushWindow<CPrologEpilogVideo>(epilogue, finisher);
-		}
+		if(!ourCampaign->isCampaignFinished())
+			CMM->openCampaignLobby(ourCampaign);
 		else
 		{
-			finisher();
+			CMM->openCampaignScreen(ourCampaign->campaignSet);
+			GH.windows().createAndPushWindow<CHighScoreInputScreen>(true, *campaignScoreCalculator);
 		}
-	});
+	};
+
+	if(epilogue.hasPrologEpilog)
+	{
+		GH.windows().createAndPushWindow<CPrologEpilogVideo>(epilogue, finisher);
+	}
+	else
+	{
+		finisher();
+	}
 }
 
 void CServerHandler::showServerError(const std::string & txt) const
@@ -853,6 +908,14 @@ void CServerHandler::onPacketReceived(const std::shared_ptr<INetworkConnection>
 
 void CServerHandler::onDisconnected(const std::shared_ptr<INetworkConnection> & connection, const std::string & errorMessage)
 {
+	if (connection != networkConnection)
+	{
+		// ServerHandler already closed this connection on its own
+		// This is the final call from network thread that informs serverHandler that connection has died
+		// ignore it since serverHandler have already shut down this connection (and possibly started a new one)
+		return;
+	}
+
 	waitForServerShutdown();
 
 	if(getState() == EClientState::DISCONNECTING)

+ 5 - 1
client/CServerHandler.h

@@ -106,7 +106,7 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor
 	std::unique_ptr<IServerRunner> serverRunner;
 	std::shared_ptr<CMapInfo> mapToStart;
 	std::vector<std::string> localPlayerNames;
-	std::shared_ptr<HighScoreCalculation> highScoreCalc;
+	std::shared_ptr<HighScoreCalculation> campaignScoreCalculator;
 
 	boost::thread threadNetwork;
 
@@ -128,6 +128,8 @@ class CServerHandler final : public IServerAPI, public LobbyInfo, public INetwor
 
 	bool isServerLocal() const;
 
+	HighScoreParameter prepareHighScores(PlayerColor player, bool victory);
+
 public:
 	/// High-level connection overlay that is capable of (de)serializing network data
 	std::shared_ptr<CConnection> logicConnection;
@@ -205,6 +207,7 @@ public:
 	void debugStartTest(std::string filename, bool save = false);
 
 	void startGameplay(VCMI_LIB_WRAP_NAMESPACE(CGameState) * gameState = nullptr);
+	void showHighScoresAndEndGameplay(PlayerColor player, bool victory);
 	void endGameplay();
 	void restartGameplay();
 	void startCampaignScenario(HighScoreParameter param, std::shared_ptr<CampaignState> cs = {});
@@ -216,6 +219,7 @@ public:
 
 	void visitForLobby(CPackForLobby & lobbyPack);
 	void visitForClient(CPackForClient & clientPack);
+	void setHighScoreCalc(const std::shared_ptr<HighScoreCalculation> &newHighScoreCalc);
 };
 
 extern CServerHandler * CSH;

+ 0 - 1
client/Client.h

@@ -217,7 +217,6 @@ public:
 	void setObjPropertyID(ObjectInstanceID objid, ObjProperty prop, ObjPropertyID identifier) override {};
 
 	void showInfoDialog(InfoWindow * iw) override {};
-	void showInfoDialog(const std::string & msg, PlayerColor player) override {};
 	void removeGUI() const;
 
 #if SCRIPTING_ENABLED

+ 84 - 17
client/ClientCommandManager.cpp

@@ -183,44 +183,108 @@ void ClientCommandManager::handleNotDialogCommand()
 	LOCPLINT->showingDialog->setn(false);
 }
 
-void ClientCommandManager::handleConvertTextCommand()
+void ClientCommandManager::handleTranslateGameCommand()
 {
-	logGlobal->info("Searching for available maps");
-	std::unordered_set<ResourcePath> mapList = CResourceHandler::get()->getFilteredFiles([&](const ResourcePath & ident)
+	std::map<std::string, std::map<std::string, std::string>> textsByMod;
+	VLC->generaltexth->exportAllTexts(textsByMod);
+
+	const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "translation";
+	boost::filesystem::create_directories(outPath);
+
+	for(const auto & modEntry : textsByMod)
 	{
-		return ident.getType() == EResType::MAP;
-	});
+		JsonNode output;
 
-	std::unordered_set<ResourcePath> campaignList = CResourceHandler::get()->getFilteredFiles([&](const ResourcePath & ident)
+		for(const auto & stringEntry : modEntry.second)
+		{
+			if(boost::algorithm::starts_with(stringEntry.first, "map."))
+				continue;
+			if(boost::algorithm::starts_with(stringEntry.first, "campaign."))
+				continue;
+
+			output[stringEntry.first].String() = stringEntry.second;
+		}
+
+		if (!output.isNull())
+		{
+			const boost::filesystem::path filePath = outPath / (modEntry.first + ".json");
+			std::ofstream file(filePath.c_str());
+			file << output.toString();
+		}
+	}
+
+	printCommandMessage("Translation export complete");
+}
+
+void ClientCommandManager::handleTranslateMapsCommand()
+{
+	CMapService mapService;
+
+	printCommandMessage("Searching for available maps");
+	std::unordered_set<ResourcePath> mapList = CResourceHandler::get()->getFilteredFiles([&](const ResourcePath & ident)
 	{
-		return ident.getType() == EResType::CAMPAIGN;
+		return ident.getType() == EResType::MAP;
 	});
 
-	CMapService mapService;
+	std::vector<std::unique_ptr<CMap>> loadedMaps;
+	std::vector<std::shared_ptr<CampaignState>> loadedCampaigns;
 
-	logGlobal->info("Loading maps for export");
+	printCommandMessage("Loading maps for export");
 	for (auto const & mapName : mapList)
 	{
 		try
 		{
 			// load and drop loaded map - we only need loader to run over all maps
-			mapService.loadMap(mapName, nullptr);
+			loadedMaps.push_back(mapService.loadMap(mapName, nullptr));
 		}
 		catch(std::exception & e)
 		{
-			logGlobal->error("Map %s is invalid. Message: %s", mapName.getName(), e.what());
+			logGlobal->warn("Map %s is invalid. Message: %s", mapName.getName(), e.what());
 		}
 	}
 
+	printCommandMessage("Searching for available campaigns");
+	std::unordered_set<ResourcePath> campaignList = CResourceHandler::get()->getFilteredFiles([&](const ResourcePath & ident)
+	{
+		return ident.getType() == EResType::CAMPAIGN;
+	});
+
 	logGlobal->info("Loading campaigns for export");
 	for (auto const & campaignName : campaignList)
 	{
-		auto state = CampaignHandler::getCampaign(campaignName.getName());
-		for (auto const & part : state->allScenarios())
-			state->getMap(part, nullptr);
+		loadedCampaigns.push_back(CampaignHandler::getCampaign(campaignName.getName()));
+		for (auto const & part : loadedCampaigns.back()->allScenarios())
+			loadedCampaigns.back()->getMap(part, nullptr);
 	}
 
-	VLC->generaltexth->dumpAllTexts();
+	std::map<std::string, std::map<std::string, std::string>> textsByMod;
+	VLC->generaltexth->exportAllTexts(textsByMod);
+
+	const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "translation";
+	boost::filesystem::create_directories(outPath);
+
+	for(const auto & modEntry : textsByMod)
+	{
+		JsonNode output;
+
+		for(const auto & stringEntry : modEntry.second)
+		{
+			if(boost::algorithm::starts_with(stringEntry.first, "map."))
+				output[stringEntry.first].String() = stringEntry.second;
+
+			if(boost::algorithm::starts_with(stringEntry.first, "campaign."))
+				output[stringEntry.first].String() = stringEntry.second;
+		}
+
+		if (!output.isNull())
+		{
+			const boost::filesystem::path filePath = outPath / (modEntry.first + ".json");
+			std::ofstream file(filePath.c_str());
+			file << output.toString();
+		}
+	}
+
+	printCommandMessage("Translation export complete");
 }
 
 void ClientCommandManager::handleGetConfigCommand()
@@ -522,8 +586,11 @@ void ClientCommandManager::processCommand(const std::string & message, bool call
 	else if(commandName == "not dialog")
 		handleNotDialogCommand();
 
-	else if(message=="convert txt")
-		handleConvertTextCommand();
+	else if(message=="translate" || message=="translate game")
+		handleTranslateGameCommand();
+
+	else if(message=="translate maps")
+		handleTranslateMapsCommand();
 
 	else if(message=="get config")
 		handleGetConfigCommand();

+ 5 - 2
client/ClientCommandManager.h

@@ -48,8 +48,11 @@ class ClientCommandManager //take mantis #2292 issue about account if thinking a
 	// Set the state indicating if dialog box is active to "no"
 	void handleNotDialogCommand();
 
-	// Dumps all game text, maps text and campaign maps text into Client log between BEGIN TEXT EXPORT and END TEXT EXPORT
-	void handleConvertTextCommand();
+	// Extracts all translateable game texts into Translation directory, separating files on per-mod basis
+	void handleTranslateGameCommand();
+
+	// Extracts all translateable texts from maps and campaigns into Translation directory, separating files on per-mod basis
+	void handleTranslateMapsCommand();
 
 	// Saves current game configuration into extracted/configuration folder
 	void handleGetConfigCommand();

+ 2 - 1
client/HeroMovementController.cpp

@@ -22,6 +22,7 @@
 
 #include "../CCallback.h"
 
+#include "../lib/CondSh.h"
 #include "../lib/pathfinder/CGPathNode.h"
 #include "../lib/mapObjects/CGHeroInstance.h"
 #include "../lib/networkPacks/PacksForClient.h"
@@ -233,7 +234,7 @@ void HeroMovementController::onMoveHeroApplied()
 	assert(currentlyMovingHero);
 	const auto * hero = currentlyMovingHero;
 
-	bool canMove = LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).nextNode().turns == 0;
+	bool canMove = LOCPLINT->localState->hasPath(hero) && LOCPLINT->localState->getPath(hero).nextNode().turns == 0 && !LOCPLINT->showingDialog->get();
 	bool wantStop = stoppingMovement;
 	bool canStop = !canMove || canHeroStopAtNode(LOCPLINT->localState->getPath(hero).currNode());
 

+ 15 - 0
client/NetPacksClient.cpp

@@ -15,6 +15,7 @@
 #include "CGameInfo.h"
 #include "windows/GUIClasses.h"
 #include "mapView/mapHandler.h"
+#include "adventureMap/AdventureMapInterface.h"
 #include "adventureMap/CInGameConsole.h"
 #include "battle/BattleInterface.h"
 #include "battle/BattleWindow.h"
@@ -408,6 +409,20 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
 {
 	callAllInterfaces(cl, &IGameEventsReceiver::gameOver, pack.player, pack.victoryLossCheckResult);
 
+	bool lastHumanEndsGame = CSH->howManyPlayerInterfaces() == 1 && vstd::contains(cl.playerint, pack.player) && cl.getPlayerState(pack.player)->human && !settings["session"]["spectate"].Bool();
+
+	if (lastHumanEndsGame)
+	{
+		assert(adventureInt);
+		if(adventureInt)
+		{
+			GH.windows().popWindows(GH.windows().count());
+			adventureInt.reset();
+		}
+
+		CSH->showHighScoresAndEndGameplay(pack.player, pack.victoryLossCheckResult.victory());
+	}
+
 	// In auto testing pack.mode we always close client if red pack.player won or lose
 	if(!settings["session"]["testmap"].isNull() && pack.player == PlayerColor(0))
 	{

+ 60 - 61
client/adventureMap/AdventureMapInterface.cpp

@@ -37,7 +37,7 @@
 #include "../CPlayerInterface.h"
 
 #include "../../CCallback.h"
-#include "../../lib/CConfigHandler.h"
+#include "../../lib/GameSettings.h"
 #include "../../lib/StartInfo.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/spells/CSpellHandler.h"
@@ -45,6 +45,8 @@
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/mapping/CMapDefines.h"
 #include "../../lib/pathfinder/CGPathNode.h"
+#include "../../lib/spells/ISpellMechanics.h"
+#include "../../lib/spells/Problem.h"
 
 std::shared_ptr<AdventureMapInterface> adventureInt;
 
@@ -501,44 +503,29 @@ const CGObjectInstance* AdventureMapInterface::getActiveObject(const int3 &mapPo
 	return *boost::range::max_element(bobjs, &CMapHandler::compareObjectBlitOrder);
 }
 
-void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos)
+void AdventureMapInterface::onTileLeftClicked(const int3 &targetPosition)
 {
 	if(!shortcuts->optionMapViewActive())
 		return;
 
-	//FIXME: this line breaks H3 behavior for Dimension Door
-	if(!LOCPLINT->cb->isVisible(mapPos))
-		return;
 	if(!LOCPLINT->makingTurn)
 		return;
 
-	const TerrainTile *tile = LOCPLINT->cb->getTile(mapPos);
-
-	const CGObjectInstance *topBlocking = getActiveObject(mapPos);
+	const CGObjectInstance *topBlocking = LOCPLINT->cb->isVisible(targetPosition) ? getActiveObject(targetPosition) : nullptr;
 
-	int3 selPos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
 	if(spellBeingCasted)
 	{
 		assert(shortcuts->optionSpellcasting());
+		assert(spellBeingCasted->id == SpellID::SCUTTLE_BOAT || spellBeingCasted->id == SpellID::DIMENSION_DOOR);
 
-		if (!isInScreenRange(selPos, mapPos))
-			return;
-
-		const TerrainTile *heroTile = LOCPLINT->cb->getTile(selPos);
-
-		switch(spellBeingCasted->id)
-		{
-		case SpellID::SCUTTLE_BOAT: //Scuttle Boat
-			if(topBlocking && topBlocking->ID == Obj::BOAT)
-				performSpellcasting(mapPos);
-			break;
-		case SpellID::DIMENSION_DOOR:
-			if(!tile || tile->isClear(heroTile))
-				performSpellcasting(mapPos);
-			break;
-		}
+		if(isValidAdventureSpellTarget(targetPosition))
+			performSpellcasting(targetPosition);
 		return;
 	}
+
+	if(!LOCPLINT->cb->isVisible(targetPosition))
+		return;
+
 	//check if we can select this object
 	bool canSelect = topBlocking && topBlocking->ID == Obj::HERO && topBlocking->tempOwner == LOCPLINT->playerID;
 	canSelect |= topBlocking && topBlocking->ID == Obj::TOWN && LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, topBlocking->tempOwner) != PlayerRelations::ENEMIES;
@@ -555,7 +542,7 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos)
 	{
 		isHero = true;
 
-		const CGPathNode *pn = LOCPLINT->cb->getPathsInfo(currentHero)->getPathInfo(mapPos);
+		const CGPathNode *pn = LOCPLINT->cb->getPathsInfo(currentHero)->getPathInfo(targetPosition);
 		if(currentHero == topBlocking) //clicked selected hero
 		{
 			LOCPLINT->openHeroWindow(currentHero);
@@ -569,7 +556,7 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos)
 		else //still here? we need to move hero if we clicked end of already selected path or calculate a new path otherwise
 		{
 			if(LOCPLINT->localState->hasPath(currentHero) &&
-			   LOCPLINT->localState->getPath(currentHero).endPos() == mapPos)//we'll be moving
+			   LOCPLINT->localState->getPath(currentHero).endPos() == targetPosition)//we'll be moving
 			{
 				assert(!CGI->mh->hasOngoingAnimations());
 				if(!CGI->mh->hasOngoingAnimations() && LOCPLINT->localState->getPath(currentHero).nextNode().turns == 0)
@@ -585,7 +572,7 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos)
 				}
 				else //remove old path and find a new one if we clicked on accessible tile
 				{
-					LOCPLINT->localState->setPath(currentHero, mapPos);
+					LOCPLINT->localState->setPath(currentHero, targetPosition);
 					onHeroChanged(currentHero);
 				}
 			}
@@ -603,63 +590,68 @@ void AdventureMapInterface::onTileLeftClicked(const int3 &mapPos)
 	}
 }
 
-void AdventureMapInterface::onTileHovered(const int3 &mapPos)
+void AdventureMapInterface::onTileHovered(const int3 &targetPosition)
 {
 	if(!shortcuts->optionMapViewActive())
 		return;
 
-	//may occur just at the start of game (fake move before full intiialization)
+	//may occur just at the start of game (fake move before full initialization)
 	if(!LOCPLINT->localState->getCurrentArmy())
 		return;
 
-	if(!LOCPLINT->cb->isVisible(mapPos))
-	{
-		CCS->curh->set(Cursor::Map::POINTER);
-		GH.statusbar()->clear();
-		return;
-	}
-	auto objRelations = PlayerRelations::ALLIES;
-	const CGObjectInstance *objAtTile = getActiveObject(mapPos);
-	if(objAtTile)
-	{
-		objRelations = LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, objAtTile->tempOwner);
-		std::string text = LOCPLINT->localState->getCurrentHero() ? objAtTile->getHoverText(LOCPLINT->localState->getCurrentHero()) : objAtTile->getHoverText(LOCPLINT->playerID);
-		boost::replace_all(text,"\n"," ");
-		GH.statusbar()->write(text);
-	}
-	else
-	{
-		std::string hlp = CGI->mh->getTerrainDescr(mapPos, false);
-		GH.statusbar()->write(hlp);
-	}
+	bool isTargetPositionVisible = LOCPLINT->cb->isVisible(targetPosition);
+	const CGObjectInstance *objAtTile = isTargetPositionVisible ? getActiveObject(targetPosition) : nullptr;
 
 	if(spellBeingCasted)
 	{
 		switch(spellBeingCasted->id)
 		{
 		case SpellID::SCUTTLE_BOAT:
-			{
-			int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
-
-			if(objAtTile && objAtTile->ID == Obj::BOAT && isInScreenRange(hpos, mapPos))
+			if(isValidAdventureSpellTarget(targetPosition))
 				CCS->curh->set(Cursor::Map::SCUTTLE_BOAT);
 			else
 				CCS->curh->set(Cursor::Map::POINTER);
 			return;
-			}
+
 		case SpellID::DIMENSION_DOOR:
+			if(isValidAdventureSpellTarget(targetPosition))
 			{
-				const TerrainTile * t = LOCPLINT->cb->getTile(mapPos, false);
-				int3 hpos = LOCPLINT->localState->getCurrentArmy()->getSightCenter();
-				if((!t || t->isClear(LOCPLINT->cb->getTile(hpos))) && isInScreenRange(hpos, mapPos))
-					CCS->curh->set(Cursor::Map::TELEPORT);
+				if(VLC->settings()->getBoolean(EGameSettings::DIMENSION_DOOR_TRIGGERS_GUARDS) && LOCPLINT->cb->isTileGuardedUnchecked(targetPosition))
+					CCS->curh->set(Cursor::Map::T1_ATTACK);
 				else
-					CCS->curh->set(Cursor::Map::POINTER);
+					CCS->curh->set(Cursor::Map::TELEPORT);
 				return;
 			}
+			else
+				CCS->curh->set(Cursor::Map::POINTER);
+			return;
+		default:
+			CCS->curh->set(Cursor::Map::POINTER);
+			return;
 		}
 	}
 
+	if(!isTargetPositionVisible)
+	{
+		CCS->curh->set(Cursor::Map::POINTER);
+		return;
+	}
+
+	auto objRelations = PlayerRelations::ALLIES;
+
+	if(objAtTile)
+	{
+		objRelations = LOCPLINT->cb->getPlayerRelations(LOCPLINT->playerID, objAtTile->tempOwner);
+		std::string text = LOCPLINT->localState->getCurrentHero() ? objAtTile->getHoverText(LOCPLINT->localState->getCurrentHero()) : objAtTile->getHoverText(LOCPLINT->playerID);
+		boost::replace_all(text,"\n"," ");
+		GH.statusbar()->write(text);
+	}
+	else if(isTargetPositionVisible)
+	{
+		std::string tileTooltipText = CGI->mh->getTerrainDescr(targetPosition, false);
+		GH.statusbar()->write(tileTooltipText);
+	}
+
 	if(LOCPLINT->localState->getCurrentArmy()->ID == Obj::TOWN || GH.isKeyboardCtrlDown())
 	{
 		if(objAtTile)
@@ -684,7 +676,7 @@ void AdventureMapInterface::onTileHovered(const int3 &mapPos)
 		std::array<Cursor::Map, 4> cursorVisit     = { Cursor::Map::T1_VISIT,      Cursor::Map::T2_VISIT,      Cursor::Map::T3_VISIT,      Cursor::Map::T4_VISIT,      };
 		std::array<Cursor::Map, 4> cursorSailVisit = { Cursor::Map::T1_SAIL_VISIT, Cursor::Map::T2_SAIL_VISIT, Cursor::Map::T3_SAIL_VISIT, Cursor::Map::T4_SAIL_VISIT, };
 
-		const CGPathNode * pathNode = LOCPLINT->cb->getPathsInfo(hero)->getPathInfo(mapPos);
+		const CGPathNode * pathNode = LOCPLINT->cb->getPathsInfo(hero)->getPathInfo(targetPosition);
 		assert(pathNode);
 
 		if((GH.isKeyboardAltDown() || settings["gameTweaks"]["forceMovementInfo"].Bool()) && pathNode->reachable()) //overwrite status bar text with movement info
@@ -932,3 +924,10 @@ void AdventureMapInterface::onScreenResize()
 	if (widgetActive)
 		activate();
 }
+
+bool AdventureMapInterface::isValidAdventureSpellTarget(int3 targetPosition) const
+{
+	spells::detail::ProblemImpl problem;
+
+	return spellBeingCasted->getAdventureMechanics().canBeCastAt(problem, LOCPLINT->cb.get(), LOCPLINT->localState->getCurrentHero(), targetPosition);
+}

+ 5 - 2
client/adventureMap/AdventureMapInterface.h

@@ -92,6 +92,9 @@ private:
 	/// casts current spell at specified location
 	void performSpellcasting(const int3 & castTarget);
 
+	/// performs clientside validation of valid targets for adventure spells
+	bool isValidAdventureSpellTarget(int3 targetPosition) const;
+
 	/// dim interface if some windows opened
 	void dim(Canvas & to);
 
@@ -170,10 +173,10 @@ public:
 	void onMapViewMoved(const Rect & visibleArea, int mapLevel);
 
 	/// called by MapView whenever tile is clicked
-	void onTileLeftClicked(const int3 & mapPos);
+	void onTileLeftClicked(const int3 & targetPosition);
 
 	/// called by MapView whenever tile is hovered
-	void onTileHovered(const int3 & mapPos);
+	void onTileHovered(const int3 & targetPosition);
 
 	/// called by MapView whenever tile is clicked
 	void onTileRightClicked(const int3 & mapPos);

+ 2 - 2
client/adventureMap/AdventureMapShortcuts.cpp

@@ -22,7 +22,7 @@
 #include "../mapView/mapHandler.h"
 #include "../windows/CKingdomInterface.h"
 #include "../windows/CSpellWindow.h"
-#include "../windows/CTradeWindow.h"
+#include "../windows/CMarketWindow.h"
 #include "../windows/settings/SettingsMainWindow.h"
 #include "AdventureMapInterface.h"
 #include "AdventureOptions.h"
@@ -342,7 +342,7 @@ void AdventureMapShortcuts::showMarketplace()
 	}
 
 	if(townWithMarket) //if any town has marketplace, open window
-		GH.windows().createAndPushWindow<CMarketplaceWindow>(townWithMarket, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
+		GH.windows().createAndPushWindow<CMarketWindow>(townWithMarket, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
 	else //if not - complain
 		LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.noTownWithMarket"));
 }

+ 7 - 3
client/eventsSDL/InputSourceKeyboard.cpp

@@ -33,6 +33,8 @@ InputSourceKeyboard::InputSourceKeyboard()
 
 void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
 {
+	std::string keyName = SDL_GetKeyName(key.keysym.sym);
+	logGlobal->trace("keyboard: key '%s' pressed", keyName);
 	assert(key.state == SDL_PRESSED);
 
 	if (SDL_IsTextInputActive() == SDL_TRUE)
@@ -85,8 +87,7 @@ void InputSourceKeyboard::handleEventKeyDown(const SDL_KeyboardEvent & key)
 		return;
 	}
 
-	auto shortcutsVector = GH.shortcuts().translateKeycode(key.keysym.sym);
-
+	auto shortcutsVector = GH.shortcuts().translateKeycode(keyName);
 	GH.events().dispatchShortcutPressed(shortcutsVector);
 }
 
@@ -95,6 +96,9 @@ void InputSourceKeyboard::handleEventKeyUp(const SDL_KeyboardEvent & key)
 	if(key.repeat != 0)
 		return; // ignore periodic event resends
 
+	std::string keyName = SDL_GetKeyName(key.keysym.sym);
+	logGlobal->trace("keyboard: key '%s' released", keyName);
+
 	if (SDL_IsTextInputActive() == SDL_TRUE)
 	{
 		if (key.keysym.sym >= ' ' && key.keysym.sym < 0x80)
@@ -103,7 +107,7 @@ void InputSourceKeyboard::handleEventKeyUp(const SDL_KeyboardEvent & key)
 
 	assert(key.state == SDL_RELEASED);
 
-	auto shortcutsVector = GH.shortcuts().translateKeycode(key.keysym.sym);
+	auto shortcutsVector = GH.shortcuts().translateKeycode(keyName);
 
 	GH.events().dispatchShortcutReleased(shortcutsVector);
 }

+ 7 - 0
client/eventsSDL/InputSourceText.cpp

@@ -21,6 +21,13 @@
 
 #include <SDL_events.h>
 
+InputSourceText::InputSourceText()
+{
+	// For whatever reason, in SDL text input is considered to be active by default at least on desktop platforms
+	// Apparently fixed in SDL3, but until then we need a workaround
+	SDL_StopTextInput();
+}
+
 void InputSourceText::handleEventTextInput(const SDL_TextInputEvent & text)
 {
 	GH.events().dispatchTextInput(text.text);

+ 2 - 0
client/eventsSDL/InputSourceText.h

@@ -21,6 +21,8 @@ struct SDL_TextInputEvent;
 class InputSourceText
 {
 public:
+	InputSourceText();
+
 	void handleEventTextInput(const SDL_TextInputEvent & current);
 	void handleEventTextEditing(const SDL_TextEditingEvent & current);
 

+ 1 - 1
client/globalLobby/GlobalLobbyClient.cpp

@@ -141,7 +141,7 @@ void GlobalLobbyClient::receiveChatHistory(const JsonNode & json)
 
 		chatHistory[channelKey].push_back(message);
 
-		if(lobbyWindowPtr)
+		if(lobbyWindowPtr && lobbyWindowPtr->isChannelOpen(channelType, channelName))
 			lobbyWindowPtr->onGameChatMessage(message.displayName, message.messageText, message.timeFormatted, channelType, channelName);
 	}
 }

+ 2 - 1
client/globalLobby/GlobalLobbyLoginWindow.cpp

@@ -38,7 +38,7 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
 
 	MetaString loginAs;
 	loginAs.appendTextID("vcmi.lobby.login.as");
-	loginAs.replaceTextID(CSH->getGlobalLobby().getAccountDisplayName());
+	loginAs.replaceRawString(CSH->getGlobalLobby().getAccountDisplayName());
 
 	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
 	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.login.title"));
@@ -65,6 +65,7 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
 	{
 		buttonLogin->block(true);
 		toggleMode->setSelected(0);
+		onLoginModeChanged(0); // call it manually to disable widgets - toggleMode will not emit this call if this is currenly selected option
 	}
 	else
 		toggleMode->setSelected(1);

+ 1 - 0
client/globalLobby/GlobalLobbyWindow.cpp

@@ -64,6 +64,7 @@ void GlobalLobbyWindow::doOpenChannel(const std::string & channelType, const std
 	widget->getGameChatHeader()->setText(text.toString());
 
 	// Update currently selected item in UI
+	// WARNING: this invalidates function parameters since some of them are members of objects that will be destroyed by reset
 	widget->getAccountList()->reset();
 	widget->getChannelList()->reset();
 	widget->getMatchList()->reset();

+ 1 - 1
client/gui/CGuiHandler.cpp

@@ -72,11 +72,11 @@ void CGuiHandler::init()
 {
 	inGuiThread = true;
 
-	inputHandlerInstance = std::make_unique<InputHandler>();
 	eventDispatcherInstance = std::make_unique<EventDispatcher>();
 	windowHandlerInstance = std::make_unique<WindowHandler>();
 	screenHandlerInstance = std::make_unique<ScreenHandler>();
 	renderHandlerInstance = std::make_unique<RenderHandler>();
+	inputHandlerInstance = std::make_unique<InputHandler>(); // Must be after windowHandlerInstance
 	shortcutsHandlerInstance = std::make_unique<ShortcutHandler>();
 	framerateManagerInstance = std::make_unique<FramerateManager>(settings["video"]["targetfps"].Integer());
 

+ 13 - 4
client/gui/EventDispatcher.cpp

@@ -213,12 +213,21 @@ void EventDispatcher::handleLeftButtonClick(const Point & position, int toleranc
 		if( i->receiveEvent(position, AEventsReceiver::LCLICK) || i == nearestElement)
 		{
 			if(isPressed)
+			{
+				i->mouseClickedState = isPressed;
 				i->clickPressed(position, lastActivated);
+			}
+			else
+			{
+				if (i->mouseClickedState)
+				{
+					i->mouseClickedState = isPressed;
+					i->clickReleased(position, lastActivated);
+				}
+				else
+					i->mouseClickedState = isPressed;
+			}
 
-			if (i->mouseClickedState && !isPressed)
-				i->clickReleased(position, lastActivated);
-
-			i->mouseClickedState = isPressed;
 			lastActivated = false;
 		}
 		else

+ 31 - 140
client/gui/ShortcutHandler.cpp

@@ -12,149 +12,40 @@
 
 #include "ShortcutHandler.h"
 #include "Shortcut.h"
-#include <SDL_keycode.h>
 
-std::vector<EShortcut> ShortcutHandler::translateKeycode(SDL_Keycode key) const
+#include "../../lib/json/JsonUtils.h"
+
+ShortcutHandler::ShortcutHandler()
 {
-	static const std::multimap<SDL_Keycode, EShortcut> keyToShortcut = {
-		{SDLK_RETURN,    EShortcut::GLOBAL_ACCEPT             },
-		{SDLK_KP_ENTER,  EShortcut::GLOBAL_ACCEPT             },
-		{SDLK_ESCAPE,    EShortcut::GLOBAL_CANCEL             },
-		{SDLK_RETURN,    EShortcut::GLOBAL_RETURN             },
-		{SDLK_KP_ENTER,  EShortcut::GLOBAL_RETURN             },
-		{SDLK_ESCAPE,    EShortcut::GLOBAL_RETURN             },
-		{SDLK_F4,        EShortcut::GLOBAL_FULLSCREEN         },
-		{SDLK_BACKSPACE, EShortcut::GLOBAL_BACKSPACE          },
-		{SDLK_TAB,       EShortcut::GLOBAL_MOVE_FOCUS         },
-		{SDLK_o,         EShortcut::GLOBAL_OPTIONS            },
-		{SDLK_LEFT,      EShortcut::MOVE_LEFT                 },
-		{SDLK_RIGHT,     EShortcut::MOVE_RIGHT                },
-		{SDLK_UP,        EShortcut::MOVE_UP                   },
-		{SDLK_DOWN,      EShortcut::MOVE_DOWN                 },
-		{SDLK_HOME,      EShortcut::MOVE_FIRST                },
-		{SDLK_END,       EShortcut::MOVE_LAST                 },
-		{SDLK_PAGEUP,    EShortcut::MOVE_PAGE_UP              },
-		{SDLK_PAGEDOWN,  EShortcut::MOVE_PAGE_DOWN            },
-		{SDLK_1,         EShortcut::SELECT_INDEX_1            },
-		{SDLK_2,         EShortcut::SELECT_INDEX_2            },
-		{SDLK_3,         EShortcut::SELECT_INDEX_3            },
-		{SDLK_4,         EShortcut::SELECT_INDEX_4            },
-		{SDLK_5,         EShortcut::SELECT_INDEX_5            },
-		{SDLK_6,         EShortcut::SELECT_INDEX_6            },
-		{SDLK_7,         EShortcut::SELECT_INDEX_7            },
-		{SDLK_8,         EShortcut::SELECT_INDEX_8            },
-		{SDLK_n,         EShortcut::MAIN_MENU_NEW_GAME        },
-		{SDLK_l,         EShortcut::MAIN_MENU_LOAD_GAME       },
-		{SDLK_h,         EShortcut::MAIN_MENU_HIGH_SCORES     },
-		{SDLK_c,         EShortcut::MAIN_MENU_CREDITS         },
-		{SDLK_q,         EShortcut::MAIN_MENU_QUIT            },
-		{SDLK_b,         EShortcut::MAIN_MENU_BACK            },
-		{SDLK_s,         EShortcut::MAIN_MENU_SINGLEPLAYER    },
-		{SDLK_m,         EShortcut::MAIN_MENU_MULTIPLAYER     },
-		{SDLK_c,         EShortcut::MAIN_MENU_CAMPAIGN        },
-		{SDLK_t,         EShortcut::MAIN_MENU_TUTORIAL        },
-		{SDLK_s,         EShortcut::MAIN_MENU_CAMPAIGN_SOD    },
-		{SDLK_r,         EShortcut::MAIN_MENU_CAMPAIGN_ROE    },
-		{SDLK_a,         EShortcut::MAIN_MENU_CAMPAIGN_AB     },
-		{SDLK_c,         EShortcut::MAIN_MENU_CAMPAIGN_CUSTOM },
-		{SDLK_b,         EShortcut::LOBBY_BEGIN_GAME          },
-		{SDLK_RETURN,    EShortcut::LOBBY_BEGIN_GAME          },
-		{SDLK_KP_ENTER,  EShortcut::LOBBY_BEGIN_GAME          },
-		{SDLK_l,         EShortcut::LOBBY_LOAD_GAME           },
-		{SDLK_RETURN,    EShortcut::LOBBY_LOAD_GAME           },
-		{SDLK_KP_ENTER,  EShortcut::LOBBY_LOAD_GAME           },
-		{SDLK_s,         EShortcut::LOBBY_SAVE_GAME           },
-		{SDLK_RETURN,    EShortcut::LOBBY_SAVE_GAME           },
-		{SDLK_KP_ENTER,  EShortcut::LOBBY_SAVE_GAME           },
-		{SDLK_r,         EShortcut::LOBBY_RANDOM_MAP          },
-		{SDLK_h,         EShortcut::LOBBY_HIDE_CHAT           },
-		{SDLK_a,         EShortcut::LOBBY_ADDITIONAL_OPTIONS  },
-		{SDLK_s,         EShortcut::LOBBY_SELECT_SCENARIO     },
-		{SDLK_e,         EShortcut::GAME_END_TURN             },
-		{SDLK_l,         EShortcut::GAME_LOAD_GAME            },
-		{SDLK_s,         EShortcut::GAME_SAVE_GAME            },
-		{SDLK_r,         EShortcut::GAME_RESTART_GAME         },
-		{SDLK_m,         EShortcut::GAME_TO_MAIN_MENU         },
-		{SDLK_q,         EShortcut::GAME_QUIT_GAME            },
-		{SDLK_b,         EShortcut::GAME_OPEN_MARKETPLACE     },
-		{SDLK_g,         EShortcut::GAME_OPEN_THIEVES_GUILD   },
-		{SDLK_TAB,       EShortcut::GAME_ACTIVATE_CONSOLE     },
-		{SDLK_o,         EShortcut::ADVENTURE_GAME_OPTIONS    },
-		{SDLK_F6,        EShortcut::ADVENTURE_TOGGLE_GRID     },
-		{SDLK_z,         EShortcut::ADVENTURE_SET_HERO_ASLEEP },
-		{SDLK_w,         EShortcut::ADVENTURE_SET_HERO_AWAKE  },
-		{SDLK_m,         EShortcut::ADVENTURE_MOVE_HERO       },
-		{SDLK_SPACE,     EShortcut::ADVENTURE_VISIT_OBJECT    },
-		{SDLK_KP_1,      EShortcut::ADVENTURE_MOVE_HERO_SW    },
-		{SDLK_KP_2,      EShortcut::ADVENTURE_MOVE_HERO_SS    },
-		{SDLK_KP_3,      EShortcut::ADVENTURE_MOVE_HERO_SE    },
-		{SDLK_KP_4,      EShortcut::ADVENTURE_MOVE_HERO_WW    },
-		{SDLK_KP_6,      EShortcut::ADVENTURE_MOVE_HERO_EE    },
-		{SDLK_KP_7,      EShortcut::ADVENTURE_MOVE_HERO_NW    },
-		{SDLK_KP_8,      EShortcut::ADVENTURE_MOVE_HERO_NN    },
-		{SDLK_KP_9,      EShortcut::ADVENTURE_MOVE_HERO_NE    },
-		{SDLK_DOWN,      EShortcut::ADVENTURE_MOVE_HERO_SS    },
-		{SDLK_LEFT,      EShortcut::ADVENTURE_MOVE_HERO_WW    },
-		{SDLK_RIGHT,     EShortcut::ADVENTURE_MOVE_HERO_EE    },
-		{SDLK_UP,        EShortcut::ADVENTURE_MOVE_HERO_NN    },
-		{SDLK_RETURN,    EShortcut::ADVENTURE_VIEW_SELECTED   },
-		{SDLK_KP_ENTER,  EShortcut::ADVENTURE_VIEW_SELECTED   },
- //		{SDLK_,          EShortcut::ADVENTURE_NEXT_OBJECT     },
-		{SDLK_t,         EShortcut::ADVENTURE_NEXT_TOWN       },
-		{SDLK_h,         EShortcut::ADVENTURE_NEXT_HERO       },
- //		{SDLK_,          EShortcut::ADVENTURE_FIRST_TOWN      },
-  //		{SDLK_,          EShortcut::ADVENTURE_FIRST_HERO      },
-		{SDLK_i,         EShortcut::ADVENTURE_VIEW_SCENARIO   },
-		{SDLK_d,         EShortcut::ADVENTURE_DIG_GRAIL       },
-		{SDLK_p,         EShortcut::ADVENTURE_VIEW_PUZZLE     },
-		{SDLK_v,         EShortcut::ADVENTURE_VIEW_WORLD      },
-		{SDLK_1,         EShortcut::ADVENTURE_VIEW_WORLD_X1   },
-		{SDLK_2,         EShortcut::ADVENTURE_VIEW_WORLD_X2   },
-		{SDLK_4,         EShortcut::ADVENTURE_VIEW_WORLD_X4   },
-		{SDLK_u,         EShortcut::ADVENTURE_TOGGLE_MAP_LEVEL},
-		{SDLK_k,         EShortcut::ADVENTURE_KINGDOM_OVERVIEW},
-		{SDLK_q,         EShortcut::ADVENTURE_QUEST_LOG       },
-		{SDLK_c,         EShortcut::ADVENTURE_CAST_SPELL      },
-		{SDLK_g,         EShortcut::ADVENTURE_THIEVES_GUILD   },
-		{SDLK_KP_PLUS,   EShortcut::ADVENTURE_ZOOM_IN         },
-		{SDLK_KP_MINUS,  EShortcut::ADVENTURE_ZOOM_OUT        },
-		{SDLK_BACKSPACE, EShortcut::ADVENTURE_ZOOM_RESET      },
-		{SDLK_q,         EShortcut::BATTLE_TOGGLE_QUEUE       },
-		{SDLK_f,         EShortcut::BATTLE_USE_CREATURE_SPELL },
-		{SDLK_s,         EShortcut::BATTLE_SURRENDER          },
-		{SDLK_r,         EShortcut::BATTLE_RETREAT            },
-		{SDLK_a,         EShortcut::BATTLE_AUTOCOMBAT         },
-		{SDLK_e,         EShortcut::BATTLE_END_WITH_AUTOCOMBAT},
-		{SDLK_c,         EShortcut::BATTLE_CAST_SPELL         },
-		{SDLK_w,         EShortcut::BATTLE_WAIT               },
-		{SDLK_d,         EShortcut::BATTLE_DEFEND             },
-		{SDLK_SPACE,     EShortcut::BATTLE_DEFEND             },
-		{SDLK_UP,        EShortcut::BATTLE_CONSOLE_UP         },
-		{SDLK_DOWN,      EShortcut::BATTLE_CONSOLE_DOWN       },
-		{SDLK_SPACE,     EShortcut::BATTLE_TACTICS_NEXT       },
-		{SDLK_RETURN,    EShortcut::BATTLE_TACTICS_END        },
-		{SDLK_KP_ENTER,  EShortcut::BATTLE_TACTICS_END        },
-		{SDLK_s,         EShortcut::BATTLE_SELECT_ACTION      },
-		{SDLK_i,         EShortcut::BATTLE_TOGGLE_HEROES_STATS},
-		{SDLK_t,         EShortcut::TOWN_OPEN_TAVERN          },
-		{SDLK_SPACE,     EShortcut::TOWN_SWAP_ARMIES          },
-		{SDLK_END,       EShortcut::RECRUITMENT_MAX           },
-		{SDLK_HOME,      EShortcut::RECRUITMENT_MIN           },
-		{SDLK_u,         EShortcut::RECRUITMENT_UPGRADE       },
-		{SDLK_a,         EShortcut::RECRUITMENT_UPGRADE_ALL   },
-		{SDLK_u,         EShortcut::RECRUITMENT_UPGRADE_ALL   },
-		{SDLK_h,         EShortcut::KINGDOM_HEROES_TAB        },
-		{SDLK_t,         EShortcut::KINGDOM_TOWNS_TAB         },
-		{SDLK_d,         EShortcut::HERO_DISMISS              },
-		{SDLK_c,         EShortcut::HERO_COMMANDER            },
-		{SDLK_l,         EShortcut::HERO_LOOSE_FORMATION      },
-		{SDLK_t,         EShortcut::HERO_TIGHT_FORMATION      },
-		{SDLK_b,         EShortcut::HERO_TOGGLE_TACTICS       },
-		{SDLK_a,         EShortcut::SPELLBOOK_TAB_ADVENTURE   },
-		{SDLK_c,         EShortcut::SPELLBOOK_TAB_COMBAT      }
-	};
+	const JsonNode config = JsonUtils::assembleFromFiles("config/shortcutsConfig");
+
+	for (auto const & entry : config["keyboard"].Struct())
+	{
+		std::string shortcutName = entry.first;
+		EShortcut shortcutID = findShortcut(shortcutName);
+
+		if (shortcutID == EShortcut::NONE)
+		{
+			logGlobal->warn("Unknown shortcut '%s' found when loading shortcuts config!", shortcutName);
+			continue;
+		}
 
-	auto range = keyToShortcut.equal_range(key);
+		if (entry.second.isString())
+		{
+			mappedShortcuts.emplace(entry.second.String(), shortcutID);
+		}
+
+		if (entry.second.isVector())
+		{
+			for (auto const & entryVector : entry.second.Vector())
+				mappedShortcuts.emplace(entryVector.String(), shortcutID);
+		}
+	}
+}
+
+std::vector<EShortcut> ShortcutHandler::translateKeycode(const std::string & key) const
+{
+	auto range = mappedShortcuts.equal_range(key);
 
 	// FIXME: some code expects calls to keyPressed / captureThisKey even without defined hotkeys
 	if (range.first == range.second)

+ 4 - 2
client/gui/ShortcutHandler.h

@@ -11,13 +11,15 @@
 #pragma once
 
 enum class EShortcut;
-using SDL_Keycode = int32_t;
 
 class ShortcutHandler
 {
+	std::multimap<std::string, EShortcut> mappedShortcuts;
 public:
+	ShortcutHandler();
+
 	/// returns list of shortcuts assigned to provided SDL keycode
-	std::vector<EShortcut> translateKeycode(SDL_Keycode key) const;
+	std::vector<EShortcut> translateKeycode(const std::string & key) const;
 
 	/// attempts to find shortcut by its unique identifier. Returns EShortcut::NONE on failure
 	EShortcut findShortcut(const std::string & identifier ) const;

+ 8 - 3
client/gui/WindowHandler.cpp

@@ -22,7 +22,9 @@
 
 void WindowHandler::popWindow(std::shared_ptr<IShowActivatable> top)
 {
-	assert(windowsStack.back() == top);
+	if (windowsStack.back() != top)
+		throw std::runtime_error("Attempt to pop non-top window from stack!");
+
 	top->deactivate();
 	disposed.push_back(top);
 	windowsStack.pop_back();
@@ -34,8 +36,11 @@ void WindowHandler::popWindow(std::shared_ptr<IShowActivatable> top)
 
 void WindowHandler::pushWindow(std::shared_ptr<IShowActivatable> newInt)
 {
-	assert(newInt);
-	assert(!vstd::contains(windowsStack, newInt)); // do not add same object twice
+	if (newInt == nullptr)
+		throw std::runtime_error("Attempt to push null window onto windows stack!");
+
+	if (vstd::contains(windowsStack, newInt))
+		throw std::runtime_error("Attempt to add already existing window to stack!");
 
 	//a new interface will be present, we'll need to use buffer surface (unless it's advmapint that will alter screenBuf on activate anyway)
 	screenBuf = screen2;

+ 1 - 1
client/lobby/CBonusSelection.cpp

@@ -119,7 +119,7 @@ CBonusSelection::CBonusSelection()
 	if (!getCampaign()->getMusic().empty())
 		CCS->musich->playMusic( getCampaign()->getMusic(), true, false);
 
-	if(settings["general"]["enableUiEnhancements"].Bool())
+	if(CSH->getState() != EClientState::GAMEPLAY && settings["general"]["enableUiEnhancements"].Bool())
 	{
 		tabExtraOptions = std::make_shared<ExtraOptionsTab>();
 		tabExtraOptions->recActions = UPDATE | SHOWALL | LCLICK | RCLICK_POPUP;

+ 1 - 1
client/lobby/SelectionTab.cpp

@@ -155,12 +155,12 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
 	OBJ_CONSTRUCTION;
 		
 	generalSortingBy = getSortBySelectionScreen(tabType);
+	sortingBy = _format;
 
 	bool enableUiEnhancements = settings["general"]["enableUiEnhancements"].Bool();
 
 	if(tabType != ESelectionScreen::campaignList)
 	{
-		sortingBy = _format;
 		background = std::make_shared<CPicture>(ImagePath::builtin("SCSELBCK.bmp"), 0, 6);
 		pos = background->pos;
 		inputName = std::make_shared<CTextInput>(inputNameRect, Point(-32, -25), ImagePath::builtin("GSSTRIP.bmp"), 0);

+ 5 - 0
client/mainmenu/CMainMenu.cpp

@@ -241,6 +241,11 @@ std::shared_ptr<CButton> CMenuEntry::createButton(CMenuScreen * parent, const Js
 
 	EShortcut shortcut = GH.shortcuts().findShortcut(button["shortcut"].String());
 
+	if (shortcut == EShortcut::NONE && !button["shortcut"].String().empty())
+	{
+		logGlobal->warn("Unknown shortcut '%s' found when loading main menu config!", button["shortcut"].String());
+	}
+
 	auto result = std::make_shared<CButton>(Point(posx, posy), AnimationPath::fromJson(button["name"]), help, command, shortcut);
 
 	if (button["center"].Bool())

+ 1 - 0
client/mainmenu/CPrologEpilogVideo.cpp

@@ -74,6 +74,7 @@ void CPrologEpilogVideo::show(Canvas & to)
 void CPrologEpilogVideo::clickPressed(const Point & cursorPosition)
 {
 	close();
+	CCS->soundh->resetCallback(voiceSoundHandle); // reset callback to avoid memory corruption since 'this' will be destroyed
 	CCS->soundh->stopSound(voiceSoundHandle);
 	CCS->soundh->stopSound(videoSoundHandle);
 	if(exitCb)

+ 7 - 1
client/mapView/MapRenderer.cpp

@@ -353,7 +353,10 @@ void MapRendererFow::renderTile(IMapRendererContext & context, Canvas & target,
 		size_t pseudorandomNumber = ((coordinates.x * 997) ^ (coordinates.y * 1009)) / 101;
 		size_t imageIndex = pseudorandomNumber % fogOfWarFullHide->size();
 
-		target.draw(fogOfWarFullHide->getImage(imageIndex), Point(0, 0));
+		if (context.showSpellRange(coordinates))
+			target.drawColor(Rect(0,0,32,32), Colors::BLACK);
+		else
+			target.draw(fogOfWarFullHide->getImage(imageIndex), Point(0, 0));
 	}
 	else
 	{
@@ -363,6 +366,9 @@ void MapRendererFow::renderTile(IMapRendererContext & context, Canvas & target,
 
 uint8_t MapRendererFow::checksum(IMapRendererContext & context, const int3 & coordinates)
 {
+	if (context.showSpellRange(coordinates))
+		return 0xff - 2;
+
 	const NeighborTilesInfo neighborInfo(context, coordinates);
 	int retBitmapID = neighborInfo.getBitmapID();
 	if(retBitmapID < 0)

+ 1 - 1
client/mapView/mapHandler.h

@@ -64,7 +64,7 @@ public:
 	void removeMapObserver(IMapObjectObserver * observer);
 
 	/// returns string description for terrain interaction
-	std::string getTerrainDescr(const int3 & pos, bool rightClick) const;
+	std::string getTerrainDescr(const int3 & pos, bool rightClick) const; //TODO: possible to get info about invisible tiles from client without serverside validation
 
 	/// determines if the map is ready to handle new hero movement (not available during fading animations)
 	bool hasOngoingAnimations();

+ 1 - 1
client/renderSDL/ScreenHandler.cpp

@@ -294,7 +294,7 @@ void ScreenHandler::initializeWindow()
 
 	SDL_RendererInfo info;
 	SDL_GetRendererInfo(mainRenderer, &info);
-	SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, "best");
+	SDL_SetHint(SDL_HINT_RENDER_SCALE_QUALITY, settings["video"]["scalingMode"].String().c_str());
 	logGlobal->info("Created renderer %s", info.name);
 }
 

+ 6 - 0
client/widgets/Buttons.cpp

@@ -216,6 +216,12 @@ void CButton::setActOnDown(bool on)
 	actOnDown = on;
 }
 
+void CButton::setHelp(const std::pair<std::string, std::string> & help)
+{
+	hoverTexts[0] = help.first;
+	helpBox = help.second;
+}
+
 void CButton::block(bool on)
 {
 	if(on || getState() == EButtonState::BLOCKED) //dont change button state if unblock requested, but it's not blocked

+ 1 - 0
client/widgets/Buttons.h

@@ -98,6 +98,7 @@ public:
 	void setHoverable(bool on);
 	void setSoundDisabled(bool on);
 	void setActOnDown(bool on);
+	void setHelp(const std::pair<std::string, std::string> & help);
 
 	/// State modifiers
 	bool isBlocked();

+ 2 - 1
client/widgets/CArtifactsOfHeroAltar.cpp

@@ -34,7 +34,8 @@ CArtifactsOfHeroAltar::CArtifactsOfHeroAltar(const Point & position)
 	rightBackpackRoll->moveBy(Point(2, -1));
 };
 
-CArtifactsOfHeroAltar::~CArtifactsOfHeroAltar()
+void CArtifactsOfHeroAltar::deactivate()
 {
 	putBackPickedArtifact();
+	CArtifactsOfHeroBase::deactivate();
 }

+ 1 - 1
client/widgets/CArtifactsOfHeroAltar.h

@@ -17,5 +17,5 @@ class CArtifactsOfHeroAltar : public CArtifactsOfHeroBase
 {
 public:
 	CArtifactsOfHeroAltar(const Point & position);
-	~CArtifactsOfHeroAltar();
+	void deactivate() override;
 };

+ 7 - 9
client/widgets/CArtifactsOfHeroBackpack.cpp

@@ -43,12 +43,17 @@ CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack()
 }
 
 void CArtifactsOfHeroBackpack::onSliderMoved(int newVal)
+{
+	backpackPos += newVal;
+	updateBackpackSlots();
+}
+
+void CArtifactsOfHeroBackpack::updateBackpackSlots()
 {
 	if(backpackListBox)
 		backpackListBox->resize(getActiveSlotRowsNum());
-	backpackPos += newVal;
 	auto slot = ArtifactPosition::BACKPACK_START + backpackPos;
-	for(auto artPlace : backpack)
+	for(const auto & artPlace : backpack)
 	{
 		setSlotData(artPlace, slot);
 		slot = slot + 1;
@@ -56,13 +61,6 @@ void CArtifactsOfHeroBackpack::onSliderMoved(int newVal)
 	redraw();
 }
 
-void CArtifactsOfHeroBackpack::updateBackpackSlots()
-{
-	if(backpackListBox)
-		backpackListBox->resize(getActiveSlotRowsNum());
-	CArtifactsOfHeroBase::updateBackpackSlots();
-}
-
 size_t CArtifactsOfHeroBackpack::getActiveSlotRowsNum()
 {
 	return (curHero->artifactsInBackpack.size() + slotsColumnsMax - 1) / slotsColumnsMax;

+ 2 - 1
client/widgets/CArtifactsOfHeroKingdom.cpp

@@ -46,7 +46,8 @@ CArtifactsOfHeroKingdom::CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vecto
 	setRedrawParent(true);
 }
 
-CArtifactsOfHeroKingdom::~CArtifactsOfHeroKingdom()
+void CArtifactsOfHeroKingdom::deactivate()
 {
 	putBackPickedArtifact();
+	CArtifactsOfHeroBase::deactivate();
 }

+ 2 - 1
client/widgets/CArtifactsOfHeroKingdom.h

@@ -23,5 +23,6 @@ public:
 	CArtifactsOfHeroKingdom() = delete;
 	CArtifactsOfHeroKingdom(ArtPlaceMap ArtWorn, std::vector<ArtPlacePtr> Backpack,
 		std::shared_ptr<CButton> leftScroll, std::shared_ptr<CButton> rightScroll);
-	~CArtifactsOfHeroKingdom();
+
+	void deactivate() override;
 };

+ 2 - 1
client/widgets/CArtifactsOfHeroMain.cpp

@@ -26,7 +26,8 @@ CArtifactsOfHeroMain::CArtifactsOfHeroMain(const Point & position)
 	addGestureCallback(std::bind(&CArtifactsOfHeroBase::gestureArtPlace, this, _1, _2));
 }
 
-CArtifactsOfHeroMain::~CArtifactsOfHeroMain()
+void CArtifactsOfHeroMain::deactivate()
 {
 	putBackPickedArtifact();
+	CArtifactsOfHeroBase::deactivate();
 }

+ 1 - 1
client/widgets/CArtifactsOfHeroMain.h

@@ -21,5 +21,5 @@ class CArtifactsOfHeroMain : public CArtifactsOfHeroBase
 {
 public:
 	CArtifactsOfHeroMain(const Point & position);
-	~CArtifactsOfHeroMain();
+	void deactivate() override;
 };

+ 3 - 21
client/widgets/CArtifactsOfHeroMarket.cpp

@@ -12,7 +12,7 @@
 
 #include "../../lib/mapObjects/CGHeroInstance.h"
 
-CArtifactsOfHeroMarket::CArtifactsOfHeroMarket(const Point & position)
+CArtifactsOfHeroMarket::CArtifactsOfHeroMarket(const Point & position, const int selectionWidth)
 {
 	init(
 		std::bind(&CArtifactsOfHeroBase::clickPrassedArtPlace, this, _1, _2),
@@ -21,25 +21,7 @@ CArtifactsOfHeroMarket::CArtifactsOfHeroMarket(const Point & position)
 		std::bind(&CArtifactsOfHeroBase::scrollBackpack, this, _1));
 
 	for(const auto & [slot, artPlace] : artWorn)
-		artPlace->setSelectionWidth(2);
+		artPlace->setSelectionWidth(selectionWidth);
 	for(auto artPlace : backpack)
-		artPlace->setSelectionWidth(2);
+		artPlace->setSelectionWidth(selectionWidth);
 };
-
-void CArtifactsOfHeroMarket::scrollBackpack(bool left)
-{
-	CArtifactsOfHeroBase::scrollBackpack(left);
-
-	// We may have highlight on one of backpack artifacts
-	if(selectArtCallback)
-	{
-		for(const auto & artPlace : backpack)
-		{
-			if(artPlace->isSelected())
-			{
-				selectArtCallback(artPlace.get());
-				break;
-			}
-		}
-	}
-}

+ 2 - 3
client/widgets/CArtifactsOfHeroMarket.h

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

+ 0 - 6
client/widgets/CWindowWithArtifacts.cpp

@@ -287,13 +287,7 @@ void CWindowWithArtifacts::artifactMoved(const ArtifactLocation & srcLoc, const
 		// Transition state. Nothing to do here. Just skip. Need to wait for final state.
 		return;
 
-	// When moving one artifact onto another it leads to two art movements: dst->TRANSITION_POS; src->dst
-	// However after first movement we pick the art from TRANSITION_POS and the second movement coming when
-	// we have a different artifact may look surprising... but it's valid.
-
 	auto pickedArtInst = std::get<const CArtifactInstance*>(curState.value());
-	assert(!pickedArtInst || destLoc.artHolder == std::get<const CGHeroInstance*>(curState.value())->id);
-
 	auto artifactMovedBody = [this, withRedraw, &destLoc, &pickedArtInst](auto artSetWeak) -> void
 	{
 		auto artSetPtr = artSetWeak.lock();

+ 2 - 2
client/widgets/MiscWidgets.cpp

@@ -20,7 +20,7 @@
 #include "../PlayerLocalState.h"
 #include "../gui/WindowHandler.h"
 #include "../eventsSDL/InputHandler.h"
-#include "../windows/CTradeWindow.h"
+#include "../windows/CMarketWindow.h"
 #include "../widgets/CGarrisonInt.h"
 #include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../widgets/TextControls.h"
@@ -474,7 +474,7 @@ void CInteractableTownTooltip::init(const CGTownInstance * town)
 		{
 			if(town->builtBuildings.count(BuildingID::MARKETPLACE))
 			{
-				GH.windows().createAndPushWindow<CMarketplaceWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
+				GH.windows().createAndPushWindow<CMarketWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
 				return;
 			}
 		}

+ 1 - 1
client/widgets/Slider.cpp

@@ -175,7 +175,7 @@ bool CSlider::receiveEvent(const Point &position, int eventType) const
 	return testTarget.isInside(position);
 }
 
-CSlider::CSlider(Point position, int totalw, const std::function<void(int)> & Moved, int Capacity, int Amount, int Value, Orientation orientation, CSlider::EStyle style)
+CSlider::CSlider(Point position, int totalw, const SliderMovingFunctor & Moved, int Capacity, int Amount, int Value, Orientation orientation, CSlider::EStyle style)
 	: Scrollable(LCLICK | DRAG, position, orientation ),
 	capacity(Capacity),
 	amount(Amount),

+ 3 - 1
client/widgets/Slider.h

@@ -75,13 +75,15 @@ public:
 	void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override;
 	void showAll(Canvas & to) override;
 
+	using SliderMovingFunctor = std::function<void(int)>;
+
 	 /// @param position coordinates of slider
 	 /// @param length length of slider ribbon, including left/right buttons
 	 /// @param Moved function that will be called whenever slider moves
 	 /// @param Capacity maximal number of visible at once elements
 	 /// @param Amount total amount of elements, including not visible
 	 /// @param Value starting position
-	CSlider(Point position, int length, const std::function<void(int)> & Moved, int Capacity, int Amount,
+	CSlider(Point position, int length, const SliderMovingFunctor & Moved, int Capacity, int Amount,
 		int Value, Orientation orientation, EStyle style = BROWN);
 	~CSlider();
 };

+ 13 - 0
client/widgets/TextControls.cpp

@@ -97,6 +97,19 @@ void CLabel::setText(const std::string & Txt)
 	}
 }
 
+void CLabel::clear()
+{
+	text.clear();
+
+	if(autoRedraw)
+	{
+		if(background || !parent)
+			redraw();
+		else
+			parent->redraw();
+	}
+}
+
 void CLabel::setMaxWidth(int width)
 {
 	maxWidth = width;

+ 1 - 0
client/widgets/TextControls.h

@@ -57,6 +57,7 @@ public:
 	virtual void setText(const std::string & Txt);
 	virtual void setMaxWidth(int width);
 	virtual void setColor(const ColorRGBA & Color);
+	void clear();
 	size_t getWidth();
 
 	CLabel(int x = 0, int y = 0, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT,

+ 91 - 72
client/widgets/markets/CAltarArtifacts.cpp

@@ -26,21 +26,19 @@
 #include "../../../lib/mapObjects/CGMarket.h"
 
 CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance * hero)
-	: CTradeBase(market, hero)
+	: CMarketBase(market, hero)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
 
-	assert(market);
+	assert(dynamic_cast<const CGArtifactsAltar*>(market));
 	auto altarObj = dynamic_cast<const CGArtifactsAltar*>(market);
 	altarId = altarObj->id;
 	altarArtifacts = altarObj;
 
-	deal = std::make_shared<CButton>(dealButtonPos, AnimationPath::builtin("ALTSACR.DEF"),
+	deal = std::make_shared<CButton>(Point(269, 520), AnimationPath::builtin("ALTSACR.DEF"),
 		CGI->generaltexth->zelp[585], [this]() {CAltarArtifacts::makeDeal(); });
-	labels.emplace_back(std::make_shared<CLabel>(450, 34, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477]));
-	labels.emplace_back(std::make_shared<CLabel>(302, 423, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478]));
-	selectedCost = std::make_shared<CLabel>(302, 500, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
-	selectedArt = std::make_shared<CArtPlace>(Point(280, 442));
+	labels.emplace_back(std::make_shared<CLabel>(450, 32, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[477]));
+	labels.emplace_back(std::make_shared<CLabel>(302, 424, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[478]));
 
 	sacrificeAllButton = std::make_shared<CButton>(Point(393, 520), AnimationPath::builtin("ALTFILL.DEF"),
 		CGI->generaltexth->zelp[571], std::bind(&CExperienceAltar::sacrificeAll, this));
@@ -50,50 +48,63 @@ CAltarArtifacts::CAltarArtifacts(const IMarket * market, const CGHeroInstance *
 		CGI->generaltexth->zelp[570], std::bind(&CAltarArtifacts::sacrificeBackpack, this));
 	sacrificeBackpackButton->block(hero->artifactsInBackpack.empty());
 
+	// Hero's artifacts
 	heroArts = std::make_shared<CArtifactsOfHeroAltar>(Point(-365, -11));
 	heroArts->setHero(hero);
 
-	int slotNum = 0;
-	for(auto & altarSlotPos : posSlotsAltar)
-	{
-		auto altarSlot = std::make_shared<CTradeableItem>(Rect(altarSlotPos, Point(44, 44)), EType::ARTIFACT_PLACEHOLDER, -1, false, slotNum);
-		altarSlot->clickPressedCallback = std::bind(&CAltarArtifacts::onSlotClickPressed, this, _1, hRight);
-		altarSlot->subtitle.clear();
-		items.front().emplace_back(altarSlot);
-		slotNum++;
-	}
+	// Altar
+	offerTradePanel = std::make_shared<ArtifactsAltarPanel>([this](const std::shared_ptr<CTradeableItem> & altarSlot)
+		{
+			CAltarArtifacts::onSlotClickPressed(altarSlot, offerTradePanel);
+		});
+	offerTradePanel->updateSlotsCallback = std::bind(&CAltarArtifacts::updateAltarSlots, this);
+	offerTradePanel->moveTo(pos.topLeft() + Point(315, 52));
 
-	expForHero->setText(std::to_string(0));
-	CTradeBase::deselect();
+	CMarketBase::updateShowcases();
+	CAltarArtifacts::deselect();
 };
 
 TExpType CAltarArtifacts::calcExpAltarForHero()
 {
 	TExpType expOnAltar(0);
 	for(const auto & tradeSlot : tradeSlotsMap)
-		expOnAltar += calcExpCost(tradeSlot.first);
+		expOnAltar += calcExpCost(tradeSlot.second->getTypeId());
 	expForHero->setText(std::to_string(expOnAltar));
 	return expOnAltar;
 }
 
+void CAltarArtifacts::deselect()
+{
+	CMarketBase::deselect();
+	CExperienceAltar::deselect();
+	tradeSlotsMap.clear();
+	// The event for removing artifacts from the altar will not be triggered. Therefore, we clean the altar immediately.
+	for(const auto & slot : offerTradePanel->slots)
+		slot->clear();
+	offerTradePanel->showcaseSlot->clear();
+}
+
+void CAltarArtifacts::update()
+{
+	CMarketBase::update();
+	CExperienceAltar::update();
+	if(const auto art = hero->getArt(ArtifactPosition::TRANSITION_POS))
+		offerQty = calcExpCost(art->getTypeId());
+	else
+		offerQty = 0;
+	updateShowcases();
+	redraw();
+}
+
 void CAltarArtifacts::makeDeal()
 {
 	std::vector<TradeItemSell> positions;
-	for(const auto & [artInst, altarSlot] : tradeSlotsMap)
+	for(const auto & [altarSlot, artInst] : tradeSlotsMap)
 	{
 		positions.push_back(artInst->getId());
 	}
 	LOCPLINT->cb->trade(market, EMarketMode::ARTIFACT_EXP, positions, std::vector<TradeItemBuy>(), std::vector<ui32>(), hero);
-
-	tradeSlotsMap.clear();
-	// The event for removing artifacts from the altar will not be triggered. Therefore, we clean the altar immediately.
-	for(auto item : items[0])
-	{
-		item->setID(-1);
-		item->subtitle.clear();
-	}
-	calcExpAltarForHero();
-	deal->block(tradeSlotsMap.empty());
+	deselect();
 }
 
 void CAltarArtifacts::sacrificeAll()
@@ -106,63 +117,59 @@ void CAltarArtifacts::sacrificeBackpack()
 	LOCPLINT->cb->bulkMoveArtifacts(heroArts->getHero()->id, altarId, false, false, true);
 }
 
-void CAltarArtifacts::setSelectedArtifact(const CArtifactInstance * art)
-{
-	selectedArt->setArtifact(art);
-	selectedCost->setText(art == nullptr ? "" : std::to_string(calcExpCost(art)));
-}
-
 std::shared_ptr<CArtifactsOfHeroAltar> CAltarArtifacts::getAOHset() const
 {
 	return heroArts;
 }
 
-ObjectInstanceID CAltarArtifacts::getObjId() const
-{
-	return altarId;
-}
-
-void CAltarArtifacts::updateSlots()
+void CAltarArtifacts::updateAltarSlots()
 {
 	assert(altarArtifacts->artifactsInBackpack.size() <= GameConstants::ALTAR_ARTIFACTS_SLOTS);
 	assert(tradeSlotsMap.size() <= GameConstants::ALTAR_ARTIFACTS_SLOTS);
 	
-	auto slotsToAdd = tradeSlotsMap;
-	for(auto & altarSlot : items[0])
+	auto tradeSlotsMapNewArts = tradeSlotsMap;
+	for(const auto & altarSlot : offerTradePanel->slots)
 		if(altarSlot->id != -1)
 		{
-			if(tradeSlotsMap.find(altarSlot->getArtInstance()) == tradeSlotsMap.end())
+			if(tradeSlotsMap.find(altarSlot) == tradeSlotsMap.end())
 			{
 				altarSlot->setID(-1);
-				altarSlot->subtitle.clear();
+				altarSlot->subtitle->clear();
 			}
 			else
 			{
-				slotsToAdd.erase(altarSlot->getArtInstance());
+				tradeSlotsMapNewArts.erase(altarSlot);
 			}
 		}
 
-	for(auto & tradeSlot : slotsToAdd)
+	for(auto & tradeSlot : tradeSlotsMapNewArts)
 	{
-		assert(tradeSlot.second->id == -1);
-		assert(altarArtifacts->getSlotByInstance(tradeSlot.first) != ArtifactPosition::PRE_FIRST);
-		tradeSlot.second->setArtInstance(tradeSlot.first);
-		tradeSlot.second->subtitle = std::to_string(calcExpCost(tradeSlot.first));
+		assert(tradeSlot.first->id == -1);
+		assert(altarArtifacts->getSlotByInstance(tradeSlot.second) != ArtifactPosition::PRE_FIRST);
+		tradeSlot.first->setID(tradeSlot.second->getTypeId().num);
+		tradeSlot.first->subtitle->setText(std::to_string(calcExpCost(tradeSlot.second->getTypeId())));
 	}
-	for(auto & slotInfo : altarArtifacts->artifactsInBackpack)
+
+	auto newArtsFromBulkMove = altarArtifacts->artifactsInBackpack;
+	for(const auto & [altarSlot, art] : tradeSlotsMap)
 	{
-		if(tradeSlotsMap.find(slotInfo.artifact) == tradeSlotsMap.end())
-		{
-			for(auto & altarSlot : items[0])
-				if(altarSlot->id == -1)
-				{
-					altarSlot->setArtInstance(slotInfo.artifact);
-					altarSlot->subtitle = std::to_string(calcExpCost(slotInfo.artifact));
-					tradeSlotsMap.try_emplace(slotInfo.artifact, altarSlot);
-					break;
-				}
-		}
+		newArtsFromBulkMove.erase(std::remove_if(newArtsFromBulkMove.begin(), newArtsFromBulkMove.end(), [artForRemove = art](auto & slotInfo)
+			{
+				return slotInfo.artifact == artForRemove;
+			}));
 	}
+	for(const auto & slotInfo : newArtsFromBulkMove)
+	{
+		for(const auto & altarSlot : offerTradePanel->slots)
+			if(altarSlot->id == -1)
+			{
+				altarSlot->setID(slotInfo.artifact->getTypeId().num);
+				altarSlot->subtitle->setText(std::to_string(calcExpCost(slotInfo.artifact->getTypeId())));
+				tradeSlotsMap.try_emplace(altarSlot, slotInfo.artifact);
+				break;
+			}
+	}
+
 	calcExpAltarForHero();
 	deal->block(tradeSlotsMap.empty());
 }
@@ -175,7 +182,18 @@ void CAltarArtifacts::putBackArtifacts()
 		LOCPLINT->cb->bulkMoveArtifacts(altarId, heroArts->getHero()->id, false, true, true);
 }
 
-void CAltarArtifacts::onSlotClickPressed(const std::shared_ptr<CTradeableItem> & altarSlot, std::shared_ptr<CTradeableItem> & hCurSlot)
+CMarketBase::MarketShowcasesParams CAltarArtifacts::getShowcasesParams() const
+{
+	if(const auto art = hero->getArt(ArtifactPosition::TRANSITION_POS))
+		return MarketShowcasesParams
+		{
+			std::nullopt,
+			ShowcaseParams {std::to_string(offerQty), CGI->artifacts()->getByIndex(art->getTypeId())->getIconIndex()}
+		};
+	return MarketShowcasesParams {std::nullopt, std::nullopt};
+}
+
+void CAltarArtifacts::onSlotClickPressed(const std::shared_ptr<CTradeableItem> & altarSlot, std::shared_ptr<TradePanelBase> & curPanel)
 {
 	assert(altarSlot);
 
@@ -186,7 +204,7 @@ void CAltarArtifacts::onSlotClickPressed(const std::shared_ptr<CTradeableItem> &
 			if(pickedArtInst->artType->isTradable())
 			{
 				if(altarSlot->id == -1)
-					tradeSlotsMap.try_emplace(pickedArtInst, altarSlot);
+					tradeSlotsMap.try_emplace(altarSlot, pickedArtInst);
 				deal->block(false);
 
 				LOCPLINT->cb->swapArtifacts(ArtifactLocation(heroArts->getHero()->id, ArtifactPosition::TRANSITION_POS),
@@ -199,19 +217,20 @@ void CAltarArtifacts::onSlotClickPressed(const std::shared_ptr<CTradeableItem> &
 			}
 		}
 	}
-	else if(const CArtifactInstance * art = altarSlot->getArtInstance())
+	else if(altarSlot->id != -1)
 	{
-		const auto slot = altarArtifacts->getSlotByInstance(art);
+		assert(tradeSlotsMap.at(altarSlot));
+		const auto slot = altarArtifacts->getSlotByInstance(tradeSlotsMap.at(altarSlot));
 		assert(slot != ArtifactPosition::PRE_FIRST);
 		LOCPLINT->cb->swapArtifacts(ArtifactLocation(altarId, slot), ArtifactLocation(hero->id, ArtifactPosition::TRANSITION_POS));
-		tradeSlotsMap.erase(art);
+		tradeSlotsMap.erase(altarSlot);
 	}
 }
 
-TExpType CAltarArtifacts::calcExpCost(const CArtifactInstance * art)
+TExpType CAltarArtifacts::calcExpCost(ArtifactID id) const
 {
-	int dmp = 0;
+	int bidQty = 0;
 	int expOfArt = 0;
-	market->getOffer(art->getTypeId(), 0, dmp, expOfArt, EMarketMode::ARTIFACT_EXP);
+	market->getOffer(id, 0, bidQty, expOfArt, EMarketMode::ARTIFACT_EXP);
 	return hero->calculateXp(expOfArt);
 }

+ 8 - 21
client/widgets/markets/CAltarArtifacts.h

@@ -10,43 +10,30 @@
 #pragma once
 
 #include "../CArtifactsOfHeroAltar.h"
-#include "CTradeBase.h"
+#include "CMarketBase.h"
 
 class CAltarArtifacts : public CExperienceAltar
 {
 public:
 	CAltarArtifacts(const IMarket * market, const CGHeroInstance * hero);
 	TExpType calcExpAltarForHero() override;
+	void deselect() override;
 	void makeDeal() override;
+	void update() override;
 	void sacrificeAll() override;
 	void sacrificeBackpack();
-	void setSelectedArtifact(const CArtifactInstance * art);
 	std::shared_ptr<CArtifactsOfHeroAltar> getAOHset() const;
-	ObjectInstanceID getObjId() const;
-	void updateSlots();
 	void putBackArtifacts();
 
 private:
 	ObjectInstanceID altarId;
 	const CArtifactSet * altarArtifacts;
-	std::shared_ptr<CArtPlace> selectedArt;
-	std::shared_ptr<CLabel> selectedCost;
 	std::shared_ptr<CButton> sacrificeBackpackButton;
 	std::shared_ptr<CArtifactsOfHeroAltar> heroArts;
-	std::map<const CArtifactInstance*, std::shared_ptr<CTradeableItem>> tradeSlotsMap;
+	std::map<std::shared_ptr<CTradeableItem>, const CArtifactInstance*> tradeSlotsMap;
 
-	const std::vector<Point> posSlotsAltar =
-	{
-		Point(317, 53), Point(371, 53), Point(425, 53),
-		Point(479, 53), Point(533, 53), Point(317, 123),
-		Point(371, 123), Point(425, 123), Point(479, 123),
-		Point(533, 123), Point(317, 193), Point(371, 193),
-		Point(425, 193), Point(479, 193), Point(533, 193),
-		Point(317, 263), Point(371, 263), Point(425, 263),
-		Point(479, 263), Point(533, 263), Point(398, 333),
-		Point(452, 333)
-	};
-
-	void onSlotClickPressed(const std::shared_ptr<CTradeableItem> & altarSlot, std::shared_ptr<CTradeableItem> & hCurSlot) override;
-	TExpType calcExpCost(const CArtifactInstance * art);
+	void updateAltarSlots();
+	CMarketBase::MarketShowcasesParams getShowcasesParams() const override;
+	void onSlotClickPressed(const std::shared_ptr<CTradeableItem> & altarSlot, std::shared_ptr<TradePanelBase> & curPanel) override;
+	TExpType calcExpCost(ArtifactID id) const;
 };

+ 103 - 86
client/widgets/markets/CAltarCreatures.cpp

@@ -13,7 +13,6 @@
 
 #include "../../gui/CGuiHandler.h"
 #include "../../widgets/Buttons.h"
-#include "../../widgets/Slider.h"
 #include "../../widgets/TextControls.h"
 
 #include "../../CGameInfo.h"
@@ -24,23 +23,23 @@
 #include "../../../lib/CGeneralTextHandler.h"
 #include "../../../lib/mapObjects/CGHeroInstance.h"
 #include "../../../lib/mapObjects/CGMarket.h"
+#include "../../../lib/MetaString.h"
 
 CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance * hero)
-	: CTradeBase(market, hero)
+	: CMarketBase(market, hero)
+	, CMarketSlider(std::bind(&CAltarCreatures::onOfferSliderMoved, this, _1))
+	, CMarketTraderText(Point(28, 31), FONT_MEDIUM, Colors::YELLOW)
 {
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
 
-	deal = std::make_shared<CButton>(dealButtonPos, AnimationPath::builtin("ALTSACR.DEF"),
+	deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("ALTSACR.DEF"),
 		CGI->generaltexth->zelp[584], [this]() {CAltarCreatures::makeDeal();});
 	labels.emplace_back(std::make_shared<CLabel>(155, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW,
 		boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated())));
 	labels.emplace_back(std::make_shared<CLabel>(450, 30, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[479]));
 	texts.emplace_back(std::make_unique<CTextBox>(CGI->generaltexth->allTexts[480], Rect(320, 56, 256, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW));
-	lSubtitle = std::make_shared<CLabel>(180, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
-	rSubtitle = std::make_shared<CLabel>(426, 503, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
-
-	offerSlider = std::make_shared<CSlider>(Point(231, 481), 137, std::bind(&CAltarCreatures::onOfferSliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL);
-	maxUnits = std::make_shared<CButton>(Point(147, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[578], std::bind(&CSlider::scrollToMax, offerSlider));
+	offerSlider->moveTo(pos.topLeft() + Point(231, 481));
+	maxAmount->setHelp(CGI->generaltexth->zelp[578]);
 
 	unitsOnAltar.resize(GameConstants::ARMY_SIZE, 0);
 	expPerUnit.resize(GameConstants::ARMY_SIZE, 0);
@@ -48,39 +47,46 @@ CAltarCreatures::CAltarCreatures(const IMarket * market, const CGHeroInstance *
 		Point(393, 520), AnimationPath::builtin("ALTARMY.DEF"), CGI->generaltexth->zelp[579], std::bind(&CExperienceAltar::sacrificeAll, this));
 
 	// Hero creatures panel
-	assert(leftTradePanel);
-	leftTradePanel->moveBy(Point(45, 110));
-	leftTradePanel->updateSlotsCallback = std::bind(&CCreaturesSelling::updateSubtitle, this);
-	for(const auto & slot : leftTradePanel->slots)
-		slot->clickPressedCallback = [this](const std::shared_ptr<CTradeableItem> & heroSlot) {CAltarCreatures::onSlotClickPressed(heroSlot, hLeft);};
+	assert(bidTradePanel);
+	bidTradePanel->moveTo(pos.topLeft() + Point(45, 110));
+	bidTradePanel->showcaseSlot->moveTo(pos.topLeft() + Point(149, 422));
+	bidTradePanel->showcaseSlot->subtitle->moveBy(Point(0, 3));
+	for(const auto & slot : bidTradePanel->slots)
+		slot->clickPressedCallback = [this](const std::shared_ptr<CTradeableItem> & heroSlot) {CAltarCreatures::onSlotClickPressed(heroSlot, bidTradePanel);};
 
 	// Altar creatures panel
-	rightTradePanel = std::make_shared<CreaturesPanel>([this](const std::shared_ptr<CTradeableItem> & altarSlot)
+	offerTradePanel = std::make_shared<CreaturesPanel>([this](const std::shared_ptr<CTradeableItem> & altarSlot)
 		{
-			CAltarCreatures::onSlotClickPressed(altarSlot, hRight);
-		}, leftTradePanel->slots);
-	rightTradePanel->moveBy(Point(334, 110));
-
-	leftTradePanel->deleteSlotsCheck = rightTradePanel->deleteSlotsCheck = std::bind(&CCreaturesSelling::slotDeletingCheck, this, _1);
+			CAltarCreatures::onSlotClickPressed(altarSlot, offerTradePanel);
+		}, bidTradePanel->slots);
+	offerTradePanel->moveTo(pos.topLeft() + Point(334, 110));
+	offerTradePanel->showcaseSlot->moveTo(pos.topLeft() + Point(395, 422));
+	offerTradePanel->showcaseSlot->subtitle->moveBy(Point(0, 3));
+	offerTradePanel->updateSlotsCallback = [this]()
+	{
+		for(const auto & altarSlot : offerTradePanel->slots)
+			updateAltarSlot(altarSlot);
+	};
+	bidTradePanel->deleteSlotsCheck = offerTradePanel->deleteSlotsCheck = std::bind(&CCreaturesSelling::slotDeletingCheck, this, _1);
+	
 	readExpValues();
-	expForHero->setText(std::to_string(0));
 	CAltarCreatures::deselect();
 };
 
 void CAltarCreatures::readExpValues()
 {
-	int dump;
-	for(auto heroSlot : leftTradePanel->slots)
+	int bidQty = 0;
+	for(const auto & heroSlot : bidTradePanel->slots)
 	{
 		if(heroSlot->id >= 0)
-			market->getOffer(heroSlot->id, 0, dump, expPerUnit[heroSlot->serial], EMarketMode::CREATURE_EXP);
+			market->getOffer(heroSlot->id, 0, bidQty, expPerUnit[heroSlot->serial], EMarketMode::CREATURE_EXP);
 	}
 }
 
-void CAltarCreatures::updateControls()
+void CAltarCreatures::highlightingChanged()
 {
 	int sliderAmount = 0;
-	if(hLeft)
+	if(bidTradePanel->isHighlighted())
 	{
 		std::optional<SlotID> lastSlot;
 		for(auto slot = SlotID(0); slot.num < GameConstants::ARMY_SIZE; slot++)
@@ -98,44 +104,32 @@ void CAltarCreatures::updateControls()
 				}
 			}
 		}
-		sliderAmount = hero->getStackCount(SlotID(hLeft->serial));
-		if(lastSlot.has_value() && lastSlot.value() == SlotID(hLeft->serial))
+		sliderAmount = hero->getStackCount(SlotID(bidTradePanel->highlightedSlot->serial));
+		if(lastSlot.has_value() && lastSlot.value() == SlotID(bidTradePanel->highlightedSlot->serial))
 			sliderAmount--;
 	}
 	offerSlider->setAmount(sliderAmount);
 	offerSlider->block(!offerSlider->getAmount());
-	if(hLeft)
-		offerSlider->scrollTo(unitsOnAltar[hLeft->serial]);
-	maxUnits->block(offerSlider->getAmount() == 0);
+	if(bidTradePanel->isHighlighted())
+		offerSlider->scrollTo(unitsOnAltar[bidTradePanel->highlightedSlot->serial]);
+	maxAmount->block(offerSlider->getAmount() == 0);
+	updateShowcases();
+	CMarketTraderText::highlightingChanged();
 }
 
-void CAltarCreatures::updateSubtitlesForSelected()
+void CAltarCreatures::update()
 {
-	if(hLeft)
-		lSubtitle->setText(std::to_string(offerSlider->getValue()));
-	else
-		lSubtitle->setText("");
-	if(hRight)
-		rSubtitle->setText(hRight->subtitle);
-	else
-		rSubtitle->setText("");
-}
-
-void CAltarCreatures::updateSlots()
-{
-	rightTradePanel->deleteSlots();
-	leftTradePanel->deleteSlots();
-	assert(leftTradePanel->slots.size() == rightTradePanel->slots.size());
-	readExpValues();
-	leftTradePanel->updateSlots();
+	CMarketBase::update();
+	CExperienceAltar::update();
+	assert(bidTradePanel->slots.size() == offerTradePanel->slots.size());
 }
 
 void CAltarCreatures::deselect()
 {
-	CTradeBase::deselect();
-	offerSlider->block(true);
-	maxUnits->block(true);
-	updateSubtitlesForSelected();
+	CMarketBase::deselect();
+	CExperienceAltar::deselect();
+	CMarketSlider::deselect();
+	CMarketTraderText::deselect();
 }
 
 TExpType CAltarCreatures::calcExpAltarForHero()
@@ -151,10 +145,6 @@ TExpType CAltarCreatures::calcExpAltarForHero()
 
 void CAltarCreatures::makeDeal()
 {
-	deselect();
-	offerSlider->scrollTo(0);
-	expForHero->setText(std::to_string(0));
-
 	std::vector<TradeItemSell> ids;
 	std::vector<ui32> toSacrifice;
 
@@ -172,17 +162,29 @@ void CAltarCreatures::makeDeal()
 	for(int & units : unitsOnAltar)
 		units = 0;
 
-	for(auto heroSlot : rightTradePanel->slots)
+	for(auto heroSlot : offerTradePanel->slots)
 	{
 		heroSlot->setType(EType::CREATURE_PLACEHOLDER);
-		heroSlot->subtitle.clear();
+		heroSlot->subtitle->clear();
 	}
+	deselect();
+}
+
+CMarketBase::MarketShowcasesParams CAltarCreatures::getShowcasesParams() const
+{
+	std::optional<ShowcaseParams> bidSelected = std::nullopt;
+	std::optional<ShowcaseParams> offerSelected = std::nullopt;
+	if(bidTradePanel->isHighlighted())
+		bidSelected = ShowcaseParams {std::to_string(offerSlider->getValue()), CGI->creatures()->getByIndex(bidTradePanel->getSelectedItemId())->getIconIndex()};
+	if(offerTradePanel->isHighlighted() && offerSlider->getValue() > 0)
+		offerSelected = ShowcaseParams {offerTradePanel->highlightedSlot->subtitle->getText(), CGI->creatures()->getByIndex(offerTradePanel->getSelectedItemId())->getIconIndex()};
+	return MarketShowcasesParams {bidSelected, offerSelected};
 }
 
 void CAltarCreatures::sacrificeAll()
 {
 	std::optional<SlotID> lastSlot;
-	for(auto heroSlot : leftTradePanel->slots)
+	for(auto heroSlot : bidTradePanel->slots)
 	{
 		auto stackCount = hero->getStackCount(SlotID(heroSlot->serial));
 		if(stackCount > unitsOnAltar[heroSlot->serial])
@@ -192,49 +194,51 @@ void CAltarCreatures::sacrificeAll()
 			unitsOnAltar[heroSlot->serial] = stackCount;
 		}
 	}
-	assert(lastSlot.has_value());
-	unitsOnAltar[lastSlot.value().num]--;
+	if(hero->needsLastStack())
+	{
+		assert(lastSlot.has_value());
+		unitsOnAltar[lastSlot.value().num]--;
+	}
 
-	if(hRight)
-		offerSlider->scrollTo(unitsOnAltar[hRight->serial]);
-	for(auto altarSlot : rightTradePanel->slots)
-		updateAltarSlot(altarSlot);
-	updateSubtitlesForSelected();
+	if(offerTradePanel->isHighlighted())
+		offerSlider->scrollTo(unitsOnAltar[offerTradePanel->highlightedSlot->serial]);
+	offerTradePanel->update();
+	updateShowcases();
 
 	deal->block(calcExpAltarForHero() == 0);
 }
 
-void CAltarCreatures::updateAltarSlot(std::shared_ptr<CTradeableItem> slot)
+void CAltarCreatures::updateAltarSlot(const std::shared_ptr<CTradeableItem> & slot)
 {
 	auto units = unitsOnAltar[slot->serial];
 	slot->setType(units > 0 ? EType::CREATURE : EType::CREATURE_PLACEHOLDER);
-	slot->subtitle = units > 0 ?
-		boost::str(boost::format(CGI->generaltexth->allTexts[122]) % std::to_string(hero->calculateXp(units * expPerUnit[slot->serial]))) : "";
+	slot->subtitle->setText(units > 0 ?
+		boost::str(boost::format(CGI->generaltexth->allTexts[122]) % std::to_string(hero->calculateXp(units * expPerUnit[slot->serial]))) : "");
 }
 
 void CAltarCreatures::onOfferSliderMoved(int newVal)
 {
-	if(hLeft)
-		unitsOnAltar[hLeft->serial] = newVal;
-	if(hRight)
-		updateAltarSlot(hRight);
+	if(bidTradePanel->isHighlighted())
+		unitsOnAltar[bidTradePanel->highlightedSlot->serial] = newVal;
+	if(offerTradePanel->isHighlighted())
+		updateAltarSlot(offerTradePanel->highlightedSlot);
 	deal->block(calcExpAltarForHero() == 0);
-	updateControls();
-	updateSubtitlesForSelected();
+	highlightingChanged();
+	redraw();
 }
 
-void CAltarCreatures::onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<CTradeableItem> & hCurSide)
+void CAltarCreatures::onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<TradePanelBase> & curPanel)
 {
-	if(hCurSide == newSlot)
+	assert(newSlot);
+	assert(curPanel);
+	if(newSlot == curPanel->highlightedSlot)
 		return;
 
-	auto * oppositeSlot = &hLeft;
-	auto oppositePanel = leftTradePanel;
-	CTradeBase::onSlotClickPressed(newSlot, hCurSide);
-	if(hCurSide == hLeft)
+	auto oppositePanel = bidTradePanel;
+	curPanel->onSlotClickPressed(newSlot);
+	if(curPanel->highlightedSlot == bidTradePanel->highlightedSlot)
 	{
-		oppositeSlot = &hRight;
-		oppositePanel = rightTradePanel;
+		oppositePanel = offerTradePanel;
 	}
 	std::shared_ptr<CTradeableItem> oppositeNewSlot;
 	for(const auto & slot : oppositePanel->slots)
@@ -244,8 +248,21 @@ void CAltarCreatures::onSlotClickPressed(const std::shared_ptr<CTradeableItem> &
 			break;
 		}
 	assert(oppositeNewSlot);
-	CTradeBase::onSlotClickPressed(oppositeNewSlot, *oppositeSlot);
-	updateControls();
-	updateSubtitlesForSelected();
+	oppositePanel->onSlotClickPressed(oppositeNewSlot);
+	highlightingChanged();
 	redraw();
 }
+
+std::string CAltarCreatures::getTraderText()
+{
+	if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted())
+	{
+		MetaString message = MetaString::createFromTextID("core.genrltxt.484");
+		message.replaceNamePlural(CreatureID(bidTradePanel->getSelectedItemId()));
+		return message.toString();
+	}
+	else
+	{
+		return "";
+	}
+}

+ 10 - 11
client/widgets/markets/CAltarCreatures.h

@@ -9,29 +9,28 @@
  */
 #pragma once
 
-#include "CTradeBase.h"
+#include "CMarketBase.h"
 
-class CAltarCreatures : public CExperienceAltar, public CCreaturesSelling
+class CAltarCreatures :
+	public CExperienceAltar, public CCreaturesSelling, public CMarketSlider, public CMarketTraderText
 {
 public:
 	CAltarCreatures(const IMarket * market, const CGHeroInstance * hero);
-	void updateSlots();
+	void update() override;
 	void deselect() override;
 	TExpType calcExpAltarForHero() override;
 	void makeDeal() override;
 	void sacrificeAll() override;
-	void updateAltarSlot(std::shared_ptr<CTradeableItem> slot);
 
 private:
-	std::shared_ptr<CButton> maxUnits;
 	std::vector<int> unitsOnAltar;
 	std::vector<int> expPerUnit;
-	std::shared_ptr<CLabel> lSubtitle;
-	std::shared_ptr<CLabel> rSubtitle;
 
+	CMarketBase::MarketShowcasesParams getShowcasesParams() const override;
+	void updateAltarSlot(const std::shared_ptr<CTradeableItem> & slot);
 	void readExpValues();
-	void updateControls();
-	void updateSubtitlesForSelected();
-	void onOfferSliderMoved(int newVal);
-	void onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<CTradeableItem> & hCurSide) override;
+	void highlightingChanged() override;
+	void onOfferSliderMoved(int newVal) override;
+	void onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<TradePanelBase> & curPanel) override;
+	std::string getTraderText() override;
 };

+ 126 - 0
client/widgets/markets/CArtifactsBuying.cpp

@@ -0,0 +1,126 @@
+/*
+ * CArtifactsBuying.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "CArtifactsBuying.h"
+
+#include "../../gui/CGuiHandler.h"
+#include "../../widgets/Buttons.h"
+#include "../../widgets/TextControls.h"
+
+#include "../../CGameInfo.h"
+#include "../../CPlayerInterface.h"
+
+#include "../../../CCallback.h"
+
+#include "../../../lib/CGeneralTextHandler.h"
+#include "../../../lib/mapObjects/CGHeroInstance.h"
+#include "../../../lib/mapObjects/CGMarket.h"
+#include "../../../lib/mapObjects/CGTownInstance.h"
+
+CArtifactsBuying::CArtifactsBuying(const IMarket * market, const CGHeroInstance * hero)
+	: CMarketBase(market, hero)
+	, CResourcesSelling([this](const std::shared_ptr<CTradeableItem> & heroSlot){CArtifactsBuying::onSlotClickPressed(heroSlot, bidTradePanel);})
+{
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	std::string title;
+	if(auto townMarket = dynamic_cast<const CGTownInstance*>(market))
+		title = (*CGI->townh)[townMarket->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated();
+	else
+		title = CGI->generaltexth->allTexts[349];
+	labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, title));
+	deal = std::make_shared<CButton>(dealButtonPos, AnimationPath::builtin("TPMRKB.DEF"),
+		CGI->generaltexth->zelp[595], [this](){CArtifactsBuying::makeDeal();});
+	labels.emplace_back(std::make_shared<CLabel>(445, 148, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[168]));
+
+	// Player's resources
+	assert(bidTradePanel);
+	bidTradePanel->moveTo(pos.topLeft() + Point(39, 184));
+	bidTradePanel->showcaseSlot->image->moveTo(pos.topLeft() + Point(141, 454));
+
+	// Artifacts panel
+	offerTradePanel = std::make_shared<ArtifactsPanel>([this](const std::shared_ptr<CTradeableItem> & newSlot)
+		{
+			CArtifactsBuying::onSlotClickPressed(newSlot, offerTradePanel);
+		}, [this]()
+		{
+			CMarketBase::updateSubtitlesForBid(EMarketMode::RESOURCE_ARTIFACT, bidTradePanel->getSelectedItemId());
+		}, market->availableItemsIds(EMarketMode::RESOURCE_ARTIFACT));
+	offerTradePanel->deleteSlotsCheck = [this](const std::shared_ptr<CTradeableItem> & slot)
+	{
+		return vstd::contains(this->market->availableItemsIds(EMarketMode::RESOURCE_ARTIFACT), ArtifactID(slot->id)) ? false : true;
+	};
+	offerTradePanel->moveTo(pos.topLeft() + Point(328, 181));
+
+	CMarketBase::update();
+	CArtifactsBuying::deselect();
+}
+
+void CArtifactsBuying::deselect()
+{
+	CMarketBase::deselect();
+	CMarketTraderText::deselect();
+}
+
+void CArtifactsBuying::makeDeal()
+{
+	if(ArtifactID(offerTradePanel->getSelectedItemId()).toArtifact()->canBePutAt(hero))
+	{
+		LOCPLINT->cb->trade(market, EMarketMode::RESOURCE_ARTIFACT, GameResID(bidTradePanel->getSelectedItemId()),
+			ArtifactID(offerTradePanel->getSelectedItemId()), offerQty, hero);
+		CMarketTraderText::makeDeal();
+		deselect();
+	}
+	else
+	{
+		LOCPLINT->showInfoDialog(CGI->generaltexth->translate("core.genrltxt.326"));
+	}
+}
+
+CMarketBase::MarketShowcasesParams CArtifactsBuying::getShowcasesParams() const
+{
+	if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted())
+		return MarketShowcasesParams
+		{
+			ShowcaseParams {std::to_string(deal->isBlocked() ? 0 : bidQty), bidTradePanel->getSelectedItemId()},
+			ShowcaseParams {std::to_string(deal->isBlocked() ? 0 : offerQty), CGI->artifacts()->getByIndex(offerTradePanel->getSelectedItemId())->getIconIndex()}
+		};
+	else
+		return MarketShowcasesParams {std::nullopt, std::nullopt};
+}
+
+void CArtifactsBuying::highlightingChanged()
+{
+	if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted())
+	{
+		market->getOffer(bidTradePanel->getSelectedItemId(), offerTradePanel->getSelectedItemId(), bidQty, offerQty, EMarketMode::RESOURCE_ARTIFACT);
+		deal->block(LOCPLINT->cb->getResourceAmount(GameResID(bidTradePanel->getSelectedItemId())) >= bidQty ? false : true);
+	}
+	CMarketBase::highlightingChanged();
+	CMarketTraderText::highlightingChanged();
+}
+
+std::string CArtifactsBuying::getTraderText()
+{
+	if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted())
+	{
+		MetaString message = MetaString::createFromTextID("core.genrltxt.267");
+		message.replaceName(ArtifactID(offerTradePanel->getSelectedItemId()));
+		message.replaceNumber(bidQty);
+		message.replaceTextID(bidQty == 1 ? "core.genrltxt.161" : "core.genrltxt.160");
+		message.replaceName(GameResID(bidTradePanel->getSelectedItemId()));
+		return message.toString();
+	}
+	else
+	{
+		return madeTransaction ? CGI->generaltexth->allTexts[162] : CGI->generaltexth->allTexts[163];
+	}
+}

+ 25 - 0
client/widgets/markets/CArtifactsBuying.h

@@ -0,0 +1,25 @@
+/*
+ * CArtifactsBuying.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "CMarketBase.h"
+
+class CArtifactsBuying : public CResourcesSelling, public CMarketTraderText
+{
+public:
+	CArtifactsBuying(const IMarket * market, const CGHeroInstance * hero);
+	void deselect() override;
+	void makeDeal() override;
+
+private:
+	CMarketBase::MarketShowcasesParams getShowcasesParams() const override;
+	void highlightingChanged() override;
+	std::string getTraderText() override;
+};

+ 174 - 0
client/widgets/markets/CArtifactsSelling.cpp

@@ -0,0 +1,174 @@
+/*
+ * CArtifactsSelling.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "CArtifactsSelling.h"
+
+#include "../../gui/CGuiHandler.h"
+#include "../../widgets/Buttons.h"
+#include "../../widgets/TextControls.h"
+
+#include "../../CGameInfo.h"
+#include "../../CPlayerInterface.h"
+
+#include "../../../CCallback.h"
+
+#include "../../../lib/CArtifactInstance.h"
+#include "../../../lib/CGeneralTextHandler.h"
+#include "../../../lib/mapObjects/CGHeroInstance.h"
+#include "../../../lib/mapObjects/CGMarket.h"
+#include "../../../lib/mapObjects/CGTownInstance.h"
+
+CArtifactsSelling::CArtifactsSelling(const IMarket * market, const CGHeroInstance * hero)
+	: CMarketBase(market, hero)
+	, CResourcesBuying(
+		[this](const std::shared_ptr<CTradeableItem> & resSlot){CArtifactsSelling::onSlotClickPressed(resSlot, offerTradePanel);},
+		[this](){CArtifactsSelling::updateSubtitles();})
+{
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	std::string title;
+	if(const auto townMarket = dynamic_cast<const CGTownInstance*>(market))
+		title = (*CGI->townh)[townMarket->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated();
+	else if(const auto mapMarket = dynamic_cast<const CGMarket*>(market))
+		title = mapMarket->title;
+
+	labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, title));
+	labels.push_back(std::make_shared<CLabel>(155, 56, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->allTexts[271]) % hero->getNameTranslated())));
+	deal = std::make_shared<CButton>(dealButtonPos, AnimationPath::builtin("TPMRKB.DEF"),
+		CGI->generaltexth->zelp[595], [this](){CArtifactsSelling::makeDeal();});
+	bidSelectedSlot = std::make_shared<CTradeableItem>(Rect(Point(123, 470), Point(69, 66)), EType::ARTIFACT_TYPE, 0, 0);
+
+	// Market resources panel
+	assert(offerTradePanel);
+	offerTradePanel->moveTo(pos.topLeft() + Point(326, 184));
+	offerTradePanel->showcaseSlot->moveTo(pos.topLeft() + Point(409, 473));
+	offerTradePanel->showcaseSlot->subtitle->moveBy(Point(0, 1));
+	
+	// Hero's artifacts
+	heroArts = std::make_shared<CArtifactsOfHeroMarket>(Point(-361, 46), offerTradePanel->selectionWidth);
+	heroArts->setHero(hero);
+	heroArts->selectArtCallback = [this](const CArtPlace * artPlace)
+	{
+		assert(artPlace);
+		selectedHeroSlot = artPlace->slot;
+		CArtifactsSelling::highlightingChanged();
+		CIntObject::redraw();
+	};
+
+	CArtifactsSelling::updateShowcases();
+	CArtifactsSelling::deselect();
+}
+
+void CArtifactsSelling::deselect()
+{
+	CMarketBase::deselect();
+	CMarketTraderText::deselect();
+	selectedHeroSlot = ArtifactPosition::PRE_FIRST;
+	heroArts->unmarkSlots();
+	bidSelectedSlot->clear();
+}
+
+void CArtifactsSelling::makeDeal()
+{
+	const auto art = hero->getArt(selectedHeroSlot);
+	assert(art);
+	LOCPLINT->cb->trade(market, EMarketMode::ARTIFACT_RESOURCE, art->getId(), GameResID(offerTradePanel->getSelectedItemId()), offerQty, hero);
+	CMarketTraderText::makeDeal();
+}
+
+void CArtifactsSelling::updateShowcases()
+{
+	const auto art = hero->getArt(selectedHeroSlot);
+	if(art && offerTradePanel->isHighlighted())
+	{
+		bidSelectedSlot->image->enable();
+		bidSelectedSlot->setID(art->getTypeId().num);
+		bidSelectedSlot->image->setFrame(CGI->artifacts()->getByIndex(art->getTypeId())->getIconIndex());
+		bidSelectedSlot->subtitle->setText(std::to_string(bidQty));
+	}
+	else
+	{
+		bidSelectedSlot->clear();
+	}
+	CMarketBase::updateShowcases();
+}
+
+void CArtifactsSelling::update()
+{
+	CMarketBase::update();
+	if(selectedHeroSlot != ArtifactPosition::PRE_FIRST)
+	{
+		if(hero->getArt(selectedHeroSlot) == nullptr)
+		{
+			deselect();
+			selectedHeroSlot = ArtifactPosition::PRE_FIRST;
+		}
+		else
+		{
+			heroArts->getArtPlace(selectedHeroSlot)->selectSlot(true);
+		}
+		highlightingChanged();
+	}
+}
+
+std::shared_ptr<CArtifactsOfHeroMarket> CArtifactsSelling::getAOHset() const
+{
+	return heroArts;
+}
+
+CMarketBase::MarketShowcasesParams CArtifactsSelling::getShowcasesParams() const
+{
+	if(hero->getArt(selectedHeroSlot) && offerTradePanel->isHighlighted())
+		return MarketShowcasesParams
+		{
+			std::nullopt,
+			ShowcaseParams {std::to_string(offerQty), offerTradePanel->getSelectedItemId()}
+		};
+	else
+		return MarketShowcasesParams {std::nullopt, std::nullopt};
+}
+
+void CArtifactsSelling::updateSubtitles()
+{
+	const auto art = this->hero->getArt(selectedHeroSlot);
+	const int bidId = art == nullptr ? -1 : art->getTypeId().num;
+	CMarketBase::updateSubtitlesForBid(EMarketMode::ARTIFACT_RESOURCE, bidId);
+}
+
+void CArtifactsSelling::highlightingChanged()
+{
+	const auto art = hero->getArt(selectedHeroSlot);
+	if(art && offerTradePanel->isHighlighted())
+	{
+		market->getOffer(art->getTypeId(), offerTradePanel->getSelectedItemId(), bidQty, offerQty, EMarketMode::ARTIFACT_RESOURCE);
+		deal->block(false);
+	}
+	CMarketBase::highlightingChanged();
+	CMarketTraderText::highlightingChanged();
+}
+
+std::string CArtifactsSelling::getTraderText()
+{
+	const auto art = hero->getArt(selectedHeroSlot);
+	if(art && offerTradePanel->isHighlighted())
+	{
+		MetaString message = MetaString::createFromTextID("core.genrltxt.268");
+		message.replaceNumber(offerQty);
+		message.replaceRawString(offerQty == 1 ? CGI->generaltexth->allTexts[161] : CGI->generaltexth->allTexts[160]);
+		message.replaceName(GameResID(offerTradePanel->getSelectedItemId()));
+		message.replaceName(art->getTypeId());
+		return message.toString();
+	}
+	else
+	{
+		return madeTransaction ? CGI->generaltexth->allTexts[162] : CGI->generaltexth->allTexts[163];
+	}
+}

+ 35 - 0
client/widgets/markets/CArtifactsSelling.h

@@ -0,0 +1,35 @@
+/*
+ * CArtifactsSelling.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../CArtifactsOfHeroMarket.h"
+#include "CMarketBase.h"
+
+class CArtifactsSelling : public CResourcesBuying, public CMarketTraderText
+{
+public:
+	CArtifactsSelling(const IMarket * market, const CGHeroInstance * hero);
+	void deselect() override;
+	void makeDeal() override;
+	void updateShowcases() override;
+	void update() override;
+	std::shared_ptr<CArtifactsOfHeroMarket> getAOHset() const;
+
+private:
+	std::shared_ptr<CArtifactsOfHeroMarket> heroArts;
+	std::shared_ptr<CLabel> bidSelectedSubtitle;
+	std::shared_ptr<CTradeableItem> bidSelectedSlot;
+	ArtifactPosition selectedHeroSlot;
+
+	CMarketBase::MarketShowcasesParams getShowcasesParams() const override;
+	void updateSubtitles();
+	void highlightingChanged() override;
+	std::string getTraderText() override;
+};

+ 124 - 0
client/widgets/markets/CFreelancerGuild.cpp

@@ -0,0 +1,124 @@
+/*
+ * CFreelancerGuild.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "CFreelancerGuild.h"
+
+#include "../../gui/CGuiHandler.h"
+#include "../../widgets/Buttons.h"
+#include "../../widgets/TextControls.h"
+
+#include "../../CGameInfo.h"
+#include "../../CPlayerInterface.h"
+
+#include "../../../CCallback.h"
+
+#include "../../../lib/CGeneralTextHandler.h"
+#include "../../../lib/MetaString.h"
+#include "../../../lib/mapObjects/CGHeroInstance.h"
+#include "../../../lib/mapObjects/CGMarket.h"
+
+CFreelancerGuild::CFreelancerGuild(const IMarket * market, const CGHeroInstance * hero)
+	: CMarketBase(market, hero)
+	, CResourcesBuying(
+		[this](const std::shared_ptr<CTradeableItem> & heroSlot){CFreelancerGuild::onSlotClickPressed(heroSlot, offerTradePanel);},
+		[this](){CMarketBase::updateSubtitlesForBid(EMarketMode::CREATURE_RESOURCE, bidTradePanel->getSelectedItemId());})
+	, CMarketSlider([this](int newVal){CMarketSlider::onOfferSliderMoved(newVal);})
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW,
+		VLC->generaltexth->translate("object.core.freelancersGuild.name")));
+	labels.emplace_back(std::make_shared<CLabel>(155, 103, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE,
+		boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated())));
+	deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("TPMRKB.DEF"),
+		CGI->generaltexth->zelp[595], [this]() {CFreelancerGuild::makeDeal();});
+	offerSlider->moveTo(pos.topLeft() + Point(232, 489));
+
+	// Hero creatures panel
+	assert(bidTradePanel);
+	bidTradePanel->moveTo(pos.topLeft() + Point(45, 123));
+	bidTradePanel->showcaseSlot->subtitle->moveBy(Point(0, -1));
+	bidTradePanel->deleteSlotsCheck = std::bind(&CCreaturesSelling::slotDeletingCheck, this, _1);
+	std::for_each(bidTradePanel->slots.cbegin(), bidTradePanel->slots.cend(), [this](auto & slot)
+		{
+			slot->clickPressedCallback = [this](const std::shared_ptr<CTradeableItem> & heroSlot)
+			{
+				CFreelancerGuild::onSlotClickPressed(heroSlot, bidTradePanel);
+			};
+		});
+
+	CFreelancerGuild::deselect();
+}
+
+void CFreelancerGuild::deselect()
+{
+	CMarketBase::deselect();
+	CMarketSlider::deselect();
+	CMarketTraderText::deselect();
+}
+
+void CFreelancerGuild::makeDeal()
+{
+	if(auto toTrade = offerSlider->getValue(); toTrade != 0)
+	{
+		LOCPLINT->cb->trade(market, EMarketMode::CREATURE_RESOURCE, SlotID(bidTradePanel->highlightedSlot->serial), GameResID(offerTradePanel->getSelectedItemId()), bidQty * toTrade, hero);
+		CMarketTraderText::makeDeal();
+		deselect();
+	}
+}
+
+CMarketBase::MarketShowcasesParams CFreelancerGuild::getShowcasesParams() const
+{
+	if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted())
+		return MarketShowcasesParams
+		{
+			ShowcaseParams {std::to_string(bidQty * offerSlider->getValue()), CGI->creatures()->getByIndex(bidTradePanel->getSelectedItemId())->getIconIndex()},
+			ShowcaseParams {std::to_string(offerQty * offerSlider->getValue()), offerTradePanel->getSelectedItemId()}
+		};
+	else
+		return MarketShowcasesParams {std::nullopt, std::nullopt};
+}
+
+void CFreelancerGuild::highlightingChanged()
+{
+	if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted())
+	{
+		market->getOffer(bidTradePanel->getSelectedItemId(), offerTradePanel->getSelectedItemId(), bidQty, offerQty, EMarketMode::CREATURE_RESOURCE);
+		offerSlider->setAmount((hero->getStackCount(SlotID(bidTradePanel->highlightedSlot->serial)) - (hero->stacksCount() == 1 && hero->needsLastStack() ? 1 : 0)) / bidQty);
+		offerSlider->scrollTo(0);
+		offerSlider->block(false);
+		maxAmount->block(false);
+		deal->block(false);
+	}
+	CMarketBase::highlightingChanged();
+	CMarketTraderText::highlightingChanged();
+}
+
+std::string CFreelancerGuild::getTraderText()
+{
+	if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted())
+	{
+		MetaString message = MetaString::createFromTextID("core.genrltxt.269");
+		message.replaceNumber(offerQty);
+		message.replaceRawString(offerQty == 1 ? CGI->generaltexth->allTexts[161] : CGI->generaltexth->allTexts[160]);
+		message.replaceName(GameResID(offerTradePanel->getSelectedItemId()));
+		message.replaceNumber(bidQty);
+		if(bidQty == 1)
+			message.replaceNameSingular(bidTradePanel->getSelectedItemId());
+		else
+			message.replaceNamePlural(bidTradePanel->getSelectedItemId());
+		return message.toString();
+	}
+	else
+	{
+		return madeTransaction ? CGI->generaltexth->allTexts[162] : CGI->generaltexth->allTexts[163];
+	}
+}

+ 26 - 0
client/widgets/markets/CFreelancerGuild.h

@@ -0,0 +1,26 @@
+/*
+ * CFreelancerGuild.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "CMarketBase.h"
+
+class CFreelancerGuild :
+	public CCreaturesSelling , public CResourcesBuying, public CMarketSlider, public CMarketTraderText
+{
+public:
+	CFreelancerGuild(const IMarket * market, const CGHeroInstance * hero);
+	void deselect() override;
+	void makeDeal() override;
+
+private:
+	CMarketBase::MarketShowcasesParams getShowcasesParams() const override;
+	void highlightingChanged() override;
+	std::string getTraderText() override;
+};

+ 244 - 0
client/widgets/markets/CMarketBase.cpp

@@ -0,0 +1,244 @@
+/*
+ * CMarketBase.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#include "StdInc.h"
+#include "CMarketBase.h"
+
+#include "../MiscWidgets.h"
+
+#include "../Images.h"
+#include "../../gui/CGuiHandler.h"
+#include "../../widgets/Buttons.h"
+#include "../../widgets/TextControls.h"
+
+#include "../../CGameInfo.h"
+#include "../../CPlayerInterface.h"
+
+#include "../../../CCallback.h"
+
+#include "../../../lib/CGeneralTextHandler.h"
+#include "../../../lib/mapObjects/CGHeroInstance.h"
+#include "../../../lib/CHeroHandler.h"
+#include "../../../lib/mapObjects/CGMarket.h"
+
+CMarketBase::CMarketBase(const IMarket * market, const CGHeroInstance * hero)
+	: market(market)
+	, hero(hero)
+{
+}
+
+void CMarketBase::deselect()
+{
+	if(bidTradePanel && bidTradePanel->highlightedSlot)
+	{
+		bidTradePanel->highlightedSlot->selectSlot(false);
+		bidTradePanel->highlightedSlot.reset();
+	}
+	if(offerTradePanel && offerTradePanel->highlightedSlot)
+	{
+		offerTradePanel->highlightedSlot->selectSlot(false);
+		offerTradePanel->highlightedSlot.reset();
+	}
+	deal->block(true);
+	bidQty = 0;
+	offerQty = 0;
+	updateShowcases();
+}
+
+void CMarketBase::onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<TradePanelBase> & curPanel)
+{
+	assert(newSlot);
+	assert(curPanel);
+	if(newSlot == curPanel->highlightedSlot)
+		return;
+
+	curPanel->onSlotClickPressed(newSlot);
+	highlightingChanged();
+	redraw();
+}
+
+void CMarketBase::update()
+{
+	if(bidTradePanel)
+		bidTradePanel->update();
+	if(offerTradePanel)
+		offerTradePanel->update();
+}
+
+void CMarketBase::updateSubtitlesForBid(EMarketMode marketMode, int bidId)
+{
+	if(bidId == -1)
+	{
+		if(offerTradePanel)
+			offerTradePanel->clearSubtitles();
+	}
+	else
+	{
+		for(const auto & slot : offerTradePanel->slots)
+		{
+			int slotBidQty = 0;
+			int slotOfferQty = 0;
+			market->getOffer(bidId, slot->id, slotBidQty, slotOfferQty, marketMode);
+			offerTradePanel->updateOffer(*slot, slotBidQty, slotOfferQty);
+		}
+	}
+};
+
+void CMarketBase::updateShowcases()
+{
+	const auto updateSelectedBody = [](const std::shared_ptr<TradePanelBase> & tradePanel, const std::optional<const ShowcaseParams> & params)
+	{
+		if(params.has_value())
+		{
+			tradePanel->setShowcaseSubtitle(params.value().text);
+			tradePanel->showcaseSlot->image->enable();
+			tradePanel->showcaseSlot->image->setFrame(params.value().imageIndex);
+		}
+		else
+		{
+			tradePanel->showcaseSlot->clear();
+		}
+	};
+
+	const auto params = getShowcasesParams();
+	if(bidTradePanel)
+		updateSelectedBody(bidTradePanel, params.bidParams);
+	if(offerTradePanel)
+		updateSelectedBody(offerTradePanel, params.offerParams);
+}
+
+void CMarketBase::highlightingChanged()
+{
+	offerTradePanel->update();
+	updateShowcases();
+}
+
+CExperienceAltar::CExperienceAltar()
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	// Experience needed to reach next level
+	texts.emplace_back(std::make_shared<CTextBox>(CGI->generaltexth->allTexts[475], Rect(15, 415, 125, 50), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW));
+	// Total experience on the Altar
+	texts.emplace_back(std::make_shared<CTextBox>(CGI->generaltexth->allTexts[476], Rect(15, 495, 125, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW));
+	expToLevel = std::make_shared<CLabel>(76, 477, FONT_SMALL, ETextAlignment::CENTER);
+	expForHero = std::make_shared<CLabel>(76, 545, FONT_SMALL, ETextAlignment::CENTER);
+}
+
+void CExperienceAltar::deselect()
+{
+	expForHero->setText(std::to_string(0));
+}
+
+void CExperienceAltar::update()
+{
+	expToLevel->setText(std::to_string(CGI->heroh->reqExp(CGI->heroh->level(hero->exp) + 1) - hero->exp));
+}
+
+CCreaturesSelling::CCreaturesSelling()
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	assert(hero);
+	CreaturesPanel::slotsData slots;
+	for(auto slotId = SlotID(0); slotId.num < GameConstants::ARMY_SIZE; slotId++)
+	{
+		if(const auto & creature = hero->getCreature(slotId))
+			slots.emplace_back(std::make_tuple(creature->getId(), slotId, hero->getStackCount(slotId)));
+	}
+	bidTradePanel = std::make_shared<CreaturesPanel>(nullptr, slots);
+	bidTradePanel->updateSlotsCallback = std::bind(&CCreaturesSelling::updateSubtitles, this);
+}
+
+bool CCreaturesSelling::slotDeletingCheck(const std::shared_ptr<CTradeableItem> & slot) const
+{
+	return hero->getStackCount(SlotID(slot->serial)) == 0 ? true : false;
+}
+
+void CCreaturesSelling::updateSubtitles() const
+{
+	for(const auto & heroSlot : bidTradePanel->slots)
+		heroSlot->subtitle->setText(std::to_string(this->hero->getStackCount(SlotID(heroSlot->serial))));
+}
+
+CResourcesBuying::CResourcesBuying(const CTradeableItem::ClickPressedFunctor & clickPressedCallback,
+	const TradePanelBase::UpdateSlotsFunctor & updSlotsCallback)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	offerTradePanel = std::make_shared<ResourcesPanel>(clickPressedCallback, updSlotsCallback);
+	offerTradePanel->moveTo(pos.topLeft() + Point(327, 182));
+	labels.emplace_back(std::make_shared<CLabel>(445, 148, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[168]));
+}
+
+CResourcesSelling::CResourcesSelling(const CTradeableItem::ClickPressedFunctor & clickPressedCallback)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	bidTradePanel = std::make_shared<ResourcesPanel>(clickPressedCallback, std::bind(&CResourcesSelling::updateSubtitles, this));
+	labels.emplace_back(std::make_shared<CLabel>(156, 148, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[270]));
+}
+
+void CResourcesSelling::updateSubtitles() const
+{
+	for(const auto & slot : bidTradePanel->slots)
+		slot->subtitle->setText(std::to_string(LOCPLINT->cb->getResourceAmount(static_cast<EGameResID>(slot->serial))));
+}
+
+CMarketSlider::CMarketSlider(const CSlider::SliderMovingFunctor & movingCallback)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	offerSlider = std::make_shared<CSlider>(Point(230, 489), 137, movingCallback, 0, 0, 0, Orientation::HORIZONTAL);
+	maxAmount = std::make_shared<CButton>(Point(228, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[596],
+		[this]()
+		{
+			offerSlider->scrollToMax();
+		});
+}
+
+void CMarketSlider::deselect()
+{
+	maxAmount->block(true);
+	offerSlider->scrollTo(0);
+	offerSlider->block(true);
+}
+
+void CMarketSlider::onOfferSliderMoved(int newVal)
+{
+	if(bidTradePanel->highlightedSlot && offerTradePanel->highlightedSlot)
+	{
+		offerSlider->scrollTo(newVal);
+		updateShowcases();
+		redraw();
+	}
+}
+
+CMarketTraderText::CMarketTraderText(const Point & pos, const EFonts & font, const ColorRGBA & color)
+	: madeTransaction(false)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	traderText = std::make_shared<CTextBox>("", Rect(pos, traderTextDimensions), 0, font, ETextAlignment::CENTER, color);
+}
+
+void CMarketTraderText::deselect()
+{
+	highlightingChanged();
+}
+
+void CMarketTraderText::makeDeal()
+{
+	madeTransaction = true;
+}
+
+void CMarketTraderText::highlightingChanged()
+{
+	traderText->setText(getTraderText());
+}

+ 128 - 0
client/widgets/markets/CMarketBase.h

@@ -0,0 +1,128 @@
+/*
+ * CMarketBase.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "TradePanels.h"
+#include "../../widgets/Slider.h"
+#include "../../render/EFont.h"
+#include "../../render/Colors.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+class IMarket;
+
+VCMI_LIB_NAMESPACE_END
+
+class CMarketBase : public CIntObject
+{
+public:
+	struct ShowcaseParams
+	{
+		std::string text;
+		int imageIndex;
+	};
+	struct MarketShowcasesParams
+	{
+		std::optional<const ShowcaseParams> bidParams;
+		std::optional<const ShowcaseParams> offerParams;
+	};
+	using ShowcasesParamsFunctor = std::function<const MarketShowcasesParams()>;
+
+	const IMarket * market;
+	const CGHeroInstance * hero;
+
+	std::shared_ptr<TradePanelBase> bidTradePanel;
+	std::shared_ptr<TradePanelBase> offerTradePanel;
+
+	std::shared_ptr<CButton> deal;
+	std::vector<std::shared_ptr<CLabel>> labels;
+	std::vector<std::shared_ptr<CTextBox>> texts;
+	int bidQty;
+	int offerQty;
+	const Point dealButtonPos = Point(270, 520);
+	const Point titlePos = Point(299, 27);
+
+	CMarketBase(const IMarket * market, const CGHeroInstance * hero);
+	virtual void makeDeal() = 0;
+	virtual void deselect();
+	virtual void update();
+
+protected:
+	virtual void onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<TradePanelBase> & curPanel);
+	virtual void updateSubtitlesForBid(EMarketMode marketMode, int bidId);
+	virtual void updateShowcases();
+	virtual MarketShowcasesParams getShowcasesParams() const = 0;
+	virtual void highlightingChanged();
+};
+
+// Market subclasses
+class CExperienceAltar : virtual public CMarketBase
+{
+public:
+	std::shared_ptr<CLabel> expToLevel;
+	std::shared_ptr<CLabel> expForHero;
+	std::shared_ptr<CButton> sacrificeAllButton;
+
+	CExperienceAltar();
+	void deselect() override;
+	void update() override;
+	virtual void sacrificeAll() = 0;
+	virtual TExpType calcExpAltarForHero() = 0;
+};
+
+class CCreaturesSelling : virtual public CMarketBase
+{
+public:
+	CCreaturesSelling();
+	bool slotDeletingCheck(const std::shared_ptr<CTradeableItem> & slot) const;
+	void updateSubtitles() const;
+};
+
+class CResourcesBuying : virtual public CMarketBase
+{
+public:
+	CResourcesBuying(const CTradeableItem::ClickPressedFunctor & clickPressedCallback,
+		const TradePanelBase::UpdateSlotsFunctor & updSlotsCallback);
+};
+
+class CResourcesSelling : virtual public CMarketBase
+{
+public:
+	explicit CResourcesSelling(const CTradeableItem::ClickPressedFunctor & clickPressedCallback);
+	void updateSubtitles() const;
+};
+
+class CMarketSlider : virtual public CMarketBase
+{
+public:
+	std::shared_ptr<CSlider> offerSlider;
+	std::shared_ptr<CButton> maxAmount;
+	const Point dealButtonPosWithSlider = Point(306, 520);
+
+	explicit CMarketSlider(const CSlider::SliderMovingFunctor & movingCallback);
+	void deselect() override;
+	virtual void onOfferSliderMoved(int newVal);
+};
+
+class CMarketTraderText : virtual public CMarketBase
+{
+public:
+	CMarketTraderText(const Point & pos = Point(316, 48), const EFonts & font = EFonts::FONT_SMALL, const ColorRGBA & Color = Colors::WHITE);
+	void deselect() override;
+	void makeDeal() override;
+
+	const Point traderTextDimensions = Point(260, 75);
+	std::shared_ptr<CTextBox> traderText;
+	bool madeTransaction;
+
+protected:
+	void highlightingChanged() override;
+	virtual std::string getTraderText() = 0;
+};

+ 123 - 0
client/widgets/markets/CMarketResources.cpp

@@ -0,0 +1,123 @@
+/*
+ * CMarketResources.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "CMarketResources.h"
+
+#include "../../gui/CGuiHandler.h"
+#include "../../widgets/Buttons.h"
+#include "../../widgets/TextControls.h"
+
+#include "../../CGameInfo.h"
+#include "../../CPlayerInterface.h"
+
+#include "../../../CCallback.h"
+
+#include "../../../lib/CGeneralTextHandler.h"
+#include "../../../lib/MetaString.h"
+#include "../../../lib/mapObjects/CGMarket.h"
+
+CMarketResources::CMarketResources(const IMarket * market, const CGHeroInstance * hero)
+	: CMarketBase(market, hero)
+	, CResourcesSelling([this](const std::shared_ptr<CTradeableItem> & heroSlot){CMarketResources::onSlotClickPressed(heroSlot, bidTradePanel);})
+	, CResourcesBuying(
+		[this](const std::shared_ptr<CTradeableItem> & resSlot){CMarketResources::onSlotClickPressed(resSlot, offerTradePanel);},
+		[this](){CMarketResources::updateSubtitles();})
+	, CMarketSlider([this](int newVal){CMarketSlider::onOfferSliderMoved(newVal);})
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[158]));
+	deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("TPMRKB.DEF"),
+		CGI->generaltexth->zelp[595], [this]() {CMarketResources::makeDeal(); });
+
+	// Player's resources
+	assert(bidTradePanel);
+	bidTradePanel->moveTo(pos.topLeft() + Point(39, 182));
+
+	// Market resources panel
+	assert(offerTradePanel);
+
+	CMarketBase::update();
+	CMarketResources::deselect();
+}
+
+void CMarketResources::deselect()
+{
+	CMarketBase::deselect();
+	CMarketSlider::deselect();
+	CMarketTraderText::deselect();
+}
+
+void CMarketResources::makeDeal()
+{
+	if(auto toTrade = offerSlider->getValue(); toTrade != 0)
+	{
+		LOCPLINT->cb->trade(market, EMarketMode::RESOURCE_RESOURCE, GameResID(bidTradePanel->getSelectedItemId()),
+			GameResID(offerTradePanel->highlightedSlot->id), bidQty * toTrade, hero);
+		CMarketTraderText::makeDeal();
+		deselect();
+	}
+}
+
+CMarketBase::MarketShowcasesParams CMarketResources::getShowcasesParams() const
+{
+	if(bidTradePanel->highlightedSlot && offerTradePanel->highlightedSlot && bidTradePanel->getSelectedItemId() != offerTradePanel->getSelectedItemId())
+		return MarketShowcasesParams
+		{
+			ShowcaseParams {std::to_string(bidQty * offerSlider->getValue()), bidTradePanel->getSelectedItemId()},
+			ShowcaseParams {std::to_string(offerQty * offerSlider->getValue()), offerTradePanel->getSelectedItemId()}
+		};
+	else
+		return MarketShowcasesParams {std::nullopt, std::nullopt};
+}
+
+void CMarketResources::highlightingChanged()
+{
+	if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted())
+	{
+		market->getOffer(bidTradePanel->getSelectedItemId(), offerTradePanel->getSelectedItemId(), bidQty, offerQty, EMarketMode::RESOURCE_RESOURCE);
+		offerSlider->setAmount(LOCPLINT->cb->getResourceAmount(GameResID(bidTradePanel->getSelectedItemId())) / bidQty);
+		offerSlider->scrollTo(0);
+		const bool isControlsBlocked = bidTradePanel->getSelectedItemId() != offerTradePanel->getSelectedItemId() ? false : true;
+		offerSlider->block(isControlsBlocked);
+		maxAmount->block(isControlsBlocked);
+		deal->block(isControlsBlocked);
+	}
+	CMarketBase::highlightingChanged();
+	CMarketTraderText::highlightingChanged();
+}
+
+void CMarketResources::updateSubtitles()
+{
+	CMarketBase::updateSubtitlesForBid(EMarketMode::RESOURCE_RESOURCE, bidTradePanel->getSelectedItemId());
+	if(bidTradePanel->highlightedSlot)
+		offerTradePanel->slots[bidTradePanel->highlightedSlot->serial]->subtitle->setText(CGI->generaltexth->allTexts[164]); // n/a
+}
+
+std::string CMarketResources::getTraderText()
+{
+	if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted() &&
+		bidTradePanel->getSelectedItemId() != offerTradePanel->getSelectedItemId())
+	{
+		MetaString message = MetaString::createFromTextID("core.genrltxt.157");
+		message.replaceNumber(offerQty);
+		message.replaceRawString(offerQty == 1 ? CGI->generaltexth->allTexts[161] : CGI->generaltexth->allTexts[160]);
+		message.replaceName(GameResID(bidTradePanel->getSelectedItemId()));
+		message.replaceNumber(bidQty);
+		message.replaceRawString(bidQty == 1 ? CGI->generaltexth->allTexts[161] : CGI->generaltexth->allTexts[160]);
+		message.replaceName(GameResID(offerTradePanel->getSelectedItemId()));
+		return message.toString();
+	}
+	else
+	{
+		return madeTransaction ? CGI->generaltexth->allTexts[162] : CGI->generaltexth->allTexts[163];
+	}
+}

+ 27 - 0
client/widgets/markets/CMarketResources.h

@@ -0,0 +1,27 @@
+/*
+ * CMarketResources.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "CMarketBase.h"
+
+class CMarketResources :
+	public CResourcesSelling, public CResourcesBuying, public CMarketSlider, public CMarketTraderText
+{
+public:
+	CMarketResources(const IMarket * market, const CGHeroInstance * hero);
+	void deselect() override;
+	void makeDeal() override;
+
+private:
+	CMarketBase::MarketShowcasesParams getShowcasesParams() const override;
+	void highlightingChanged() override;
+	void updateSubtitles();
+	std::string getTraderText() override;
+};

+ 0 - 105
client/widgets/markets/CTradeBase.cpp

@@ -1,105 +0,0 @@
-/*
- * CTradeBase.cpp, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#include "StdInc.h"
-#include "CTradeBase.h"
-
-#include "../MiscWidgets.h"
-
-#include "../../gui/CGuiHandler.h"
-#include "../../widgets/Buttons.h"
-#include "../../widgets/TextControls.h"
-
-#include "../../CGameInfo.h"
-
-#include "../../../lib/CGeneralTextHandler.h"
-#include "../../../lib/mapObjects/CGHeroInstance.h"
-
-CTradeBase::CTradeBase(const IMarket * market, const CGHeroInstance * hero)
-	: market(market)
-	, hero(hero)
-{
-}
-
-void CTradeBase::removeItems(const std::set<std::shared_ptr<CTradeableItem>> & toRemove)
-{
-	for(auto item : toRemove)
-		removeItem(item);
-}
-
-void CTradeBase::removeItem(std::shared_ptr<CTradeableItem> item)
-{
-	rightTradePanel->slots.erase(std::remove(rightTradePanel->slots.begin(), rightTradePanel->slots.end(), item));
-
-	if(hRight == item)
-		hRight.reset();
-}
-
-void CTradeBase::getEmptySlots(std::set<std::shared_ptr<CTradeableItem>> & toRemove)
-{
-	for(auto item : leftTradePanel->slots)
-		if(!hero->getStackCount(SlotID(item->serial)))
-			toRemove.insert(item);
-}
-
-void CTradeBase::deselect()
-{
-	if(hLeft)
-		hLeft->selectSlot(false);
-	if(hRight)
-		hRight->selectSlot(false);
-	hLeft = hRight = nullptr;
-	deal->block(true);
-}
-
-void CTradeBase::onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<CTradeableItem> & hCurSlot)
-{
-	if(newSlot == hCurSlot)
-		return;
-
-	if(hCurSlot)
-		hCurSlot->selectSlot(false);
-	hCurSlot = newSlot;
-	newSlot->selectSlot(true);
-}
-
-CExperienceAltar::CExperienceAltar()
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
-
-	// Experience needed to reach next level
-	texts.emplace_back(std::make_shared<CTextBox>(CGI->generaltexth->allTexts[475], Rect(15, 415, 125, 50), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW));
-	// Total experience on the Altar
-	texts.emplace_back(std::make_shared<CTextBox>(CGI->generaltexth->allTexts[476], Rect(15, 495, 125, 40), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW));
-	expToLevel = std::make_shared<CLabel>(75, 477, FONT_SMALL, ETextAlignment::CENTER);
-	expForHero = std::make_shared<CLabel>(75, 545, FONT_SMALL, ETextAlignment::CENTER);
-}
-
-CCreaturesSelling::CCreaturesSelling()
-{
-	assert(hero);
-	CreaturesPanel::slotsData slots;
-	for(auto slotId = SlotID(0); slotId.num < GameConstants::ARMY_SIZE; slotId++)
-	{
-		if(const auto & creature = hero->getCreature(slotId))
-			slots.emplace_back(std::make_tuple(creature->getId(), slotId, hero->getStackCount(slotId)));
-	}
-	leftTradePanel = std::make_shared<CreaturesPanel>(nullptr, slots);
-}
-
-bool CCreaturesSelling::slotDeletingCheck(const std::shared_ptr<CTradeableItem> & slot)
-{
-	return hero->getStackCount(SlotID(slot->serial)) == 0 ? true : false;
-}
-
-void CCreaturesSelling::updateSubtitle()
-{
-	for(auto & heroSlot : leftTradePanel->slots)
-		heroSlot->subtitle = std::to_string(this->hero->getStackCount(SlotID(heroSlot->serial)));
-}

+ 0 - 74
client/widgets/markets/CTradeBase.h

@@ -1,74 +0,0 @@
-/*
- * CTradeBase.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#pragma once
-
-#include "TradePanels.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class IMarket;
-class CGHeroInstance;
-
-VCMI_LIB_NAMESPACE_END
-
-class CButton;
-class CSlider;
-
-class CTradeBase
-{
-public:
-	const IMarket * market;
-	const CGHeroInstance * hero;
-
-	//all indexes: 1 = left, 0 = right
-	std::array<std::vector<std::shared_ptr<CTradeableItem>>, 2> items;
-	std::shared_ptr<TradePanelBase> leftTradePanel;
-	std::shared_ptr<TradePanelBase> rightTradePanel;
-
-	//highlighted items (nullptr if no highlight)
-	std::shared_ptr<CTradeableItem> hLeft;
-	std::shared_ptr<CTradeableItem> hRight;
-	std::shared_ptr<CButton> deal;
-	std::shared_ptr<CSlider> offerSlider;
-
-	std::vector<std::shared_ptr<CLabel>> labels;
-	std::vector<std::shared_ptr<CButton>> buttons;
-	std::vector<std::shared_ptr<CTextBox>> texts;
-
-	CTradeBase(const IMarket * market, const CGHeroInstance * hero);
-	void removeItems(const std::set<std::shared_ptr<CTradeableItem>> & toRemove);
-	void removeItem(std::shared_ptr<CTradeableItem> item);
-	void getEmptySlots(std::set<std::shared_ptr<CTradeableItem>> & toRemove);
-	virtual void makeDeal() = 0;
-	virtual void deselect();
-	virtual void onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot, std::shared_ptr<CTradeableItem> & hCurSlot);
-};
-
-// Market subclasses
-class CExperienceAltar : virtual public CTradeBase, virtual public CIntObject
-{
-public:
-	std::shared_ptr<CLabel> expToLevel;
-	std::shared_ptr<CLabel> expForHero;
-	std::shared_ptr<CButton> sacrificeAllButton;
-	const Point dealButtonPos = Point(269, 520);
-
-	CExperienceAltar();
-	virtual void sacrificeAll() = 0;
-	virtual TExpType calcExpAltarForHero() = 0;
-};
-
-class CCreaturesSelling : virtual public CTradeBase, virtual public CIntObject
-{
-public:
-	CCreaturesSelling();
-	bool slotDeletingCheck(const std::shared_ptr<CTradeableItem> & slot);
-	void updateSubtitle();
-};

+ 111 - 0
client/widgets/markets/CTransferResources.cpp

@@ -0,0 +1,111 @@
+/*
+ * CTransferResources.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "CTransferResources.h"
+
+#include "../../gui/CGuiHandler.h"
+#include "../../widgets/Buttons.h"
+#include "../../widgets/TextControls.h"
+
+#include "../../CGameInfo.h"
+#include "../../CPlayerInterface.h"
+
+#include "../../../CCallback.h"
+
+#include "../../../lib/CGeneralTextHandler.h"
+#include "../../../lib/MetaString.h"
+
+CTransferResources::CTransferResources(const IMarket * market, const CGHeroInstance * hero)
+	: CMarketBase(market, hero)
+	, CResourcesSelling([this](const std::shared_ptr<CTradeableItem> & heroSlot){CTransferResources::onSlotClickPressed(heroSlot, bidTradePanel);})
+	, CMarketSlider([this](int newVal){CMarketSlider::onOfferSliderMoved(newVal);})
+	, CMarketTraderText(Point(28, 48))
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
+
+	labels.emplace_back(std::make_shared<CLabel>(titlePos.x, titlePos.y, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->allTexts[158]));
+	labels.emplace_back(std::make_shared<CLabel>(445, 56, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[169]));
+	deal = std::make_shared<CButton>(dealButtonPosWithSlider, AnimationPath::builtin("TPMRKB.DEF"),
+		CGI->generaltexth->zelp[595], [this](){CTransferResources::makeDeal();});
+
+	// Player's resources
+	assert(bidTradePanel);
+	bidTradePanel->moveTo(pos.topLeft() + Point(40, 183));
+
+	// Players panel
+	offerTradePanel = std::make_shared<PlayersPanel>([this](const std::shared_ptr<CTradeableItem> & heroSlot)
+		{
+			CTransferResources::onSlotClickPressed(heroSlot, offerTradePanel);
+		});
+	offerTradePanel->moveTo(pos.topLeft() + Point(333, 84));
+
+	CMarketBase::update();
+	CTransferResources::deselect();
+}
+
+void CTransferResources::deselect()
+{
+	CMarketBase::deselect();
+	CMarketSlider::deselect();
+	CMarketTraderText::deselect();
+}
+
+void CTransferResources::makeDeal()
+{
+	if(auto toTrade = offerSlider->getValue(); toTrade != 0)
+	{
+		LOCPLINT->cb->trade(market, EMarketMode::RESOURCE_PLAYER, GameResID(bidTradePanel->getSelectedItemId()),
+			PlayerColor(offerTradePanel->getSelectedItemId()), toTrade, hero);
+		CMarketTraderText::makeDeal();
+		deselect();
+	}
+}
+
+CMarketBase::MarketShowcasesParams CTransferResources::getShowcasesParams() const
+{
+	if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted())
+		return MarketShowcasesParams
+		{
+			ShowcaseParams {std::to_string(offerSlider->getValue()), bidTradePanel->getSelectedItemId()},
+			ShowcaseParams {CGI->generaltexth->capColors[offerTradePanel->getSelectedItemId()], offerTradePanel->getSelectedItemId()}
+		};
+	else
+		return MarketShowcasesParams {std::nullopt, std::nullopt};
+}
+
+void CTransferResources::highlightingChanged()
+{
+	if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted())
+	{
+		offerSlider->setAmount(LOCPLINT->cb->getResourceAmount(GameResID(bidTradePanel->getSelectedItemId())));
+		offerSlider->scrollTo(0);
+		offerSlider->block(false);
+		maxAmount->block(false);
+		deal->block(false);
+	}
+	CMarketBase::highlightingChanged();
+	CMarketTraderText::highlightingChanged();
+}
+
+std::string CTransferResources::getTraderText()
+{
+	if(bidTradePanel->isHighlighted() && offerTradePanel->isHighlighted())
+	{
+		MetaString message = MetaString::createFromTextID("core.genrltxt.165");
+		message.replaceName(GameResID(bidTradePanel->getSelectedItemId()));
+		message.replaceName(PlayerColor(offerTradePanel->getSelectedItemId()));
+		return message.toString();
+	}
+	else
+	{
+		return madeTransaction ? CGI->generaltexth->allTexts[166] : CGI->generaltexth->allTexts[167];
+	}
+}

+ 25 - 0
client/widgets/markets/CTransferResources.h

@@ -0,0 +1,25 @@
+/*
+ * CTransferResources.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "CMarketBase.h"
+
+class CTransferResources : public CResourcesSelling, public CMarketSlider, public CMarketTraderText
+{
+public:
+	CTransferResources(const IMarket * market, const CGHeroInstance * hero);
+	void deselect() override;
+	void makeDeal() override;
+
+private:
+	CMarketBase::MarketShowcasesParams getShowcasesParams() const override;
+	void highlightingChanged() override;
+	std::string getTraderText() override;
+};

+ 115 - 124
client/widgets/markets/TradePanels.cpp

@@ -23,14 +23,11 @@
 #include "../../../lib/CGeneralTextHandler.h"
 #include "../../../lib/mapObjects/CGHeroInstance.h"
 
-CTradeableItem::CTradeableItem(const Rect & area, EType Type, int ID, bool Left, int Serial)
+CTradeableItem::CTradeableItem(const Rect & area, EType Type, int ID, int Serial)
 	: SelectableSlot(area, Point(1, 1))
-	, artInstance(nullptr)
 	, type(EType(-1)) // set to invalid, will be corrected in setType
 	, id(ID)
 	, serial(Serial)
-	, left(Left)
-	, downSelection(false)
 {
 	OBJECT_CONSTRUCTION_CAPTURING(255 - DISPOSE);
 
@@ -38,6 +35,7 @@ CTradeableItem::CTradeableItem(const Rect & area, EType Type, int ID, bool Left,
 	addUsedEvents(HOVER);
 	addUsedEvents(SHOW_POPUP);
 	
+	subtitle = std::make_shared<CLabel>(0, 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
 	setType(Type);
 
 	this->pos.w = area.w;
@@ -60,6 +58,30 @@ void CTradeableItem::setType(EType newType)
 		{
 			image = std::make_shared<CAnimImage>(getFilename(), getIndex());
 		}
+
+		switch(type)
+		{
+		case EType::RESOURCE:
+			subtitle->moveTo(pos.topLeft() + Point(35, 55));
+			image->moveTo(pos.topLeft() + Point(19, 8));
+			break;
+		case EType::CREATURE_PLACEHOLDER:
+		case EType::CREATURE:
+			subtitle->moveTo(pos.topLeft() + Point(30, 77));
+			break;
+		case EType::PLAYER:
+			subtitle->moveTo(pos.topLeft() + Point(31, 76));
+			break;
+		case EType::ARTIFACT_PLACEHOLDER:
+		case EType::ARTIFACT_INSTANCE:
+			image->moveTo(pos.topLeft() + Point(0, 1));
+			subtitle->moveTo(pos.topLeft() + Point(21, 56));
+			break;
+		case EType::ARTIFACT_TYPE:
+			subtitle->moveTo(pos.topLeft() + Point(35, 57));
+			image->moveTo(pos.topLeft() + Point(13, 0));
+			break;
+		}
 	}
 }
 
@@ -82,6 +104,14 @@ void CTradeableItem::setID(int newID)
 	}
 }
 
+void CTradeableItem::clear()
+{
+	setID(-1);
+	image->setFrame(0);
+	image->disable();
+	subtitle->clear();
+}
+
 AnimationPath CTradeableItem::getFilename()
 {
 	switch(type)
@@ -122,69 +152,15 @@ int CTradeableItem::getIndex()
 	}
 }
 
-void CTradeableItem::showAll(Canvas & to)
-{
-	Point posToBitmap;
-	Point posToSubCenter;
-
-	switch(type)
-	{
-	case EType::RESOURCE:
-		posToBitmap = Point(19, 9);
-		posToSubCenter = Point(35, 57);
-		break;
-	case EType::CREATURE_PLACEHOLDER:
-	case EType::CREATURE:
-		posToSubCenter = Point(29, 77);
-		break;
-	case EType::PLAYER:
-		posToSubCenter = Point(31, 77);
-		break;
-	case EType::ARTIFACT_PLACEHOLDER:
-	case EType::ARTIFACT_INSTANCE:
-		posToSubCenter = Point(22, 51);
-		if (downSelection)
-			posToSubCenter.y += 8;
-		break;
-	case EType::ARTIFACT_TYPE:
-		posToSubCenter = Point(35, 57);
-		posToBitmap = Point(13, 0);
-		break;
-	}
-
-	if(image)
-	{
-		image->moveTo(pos.topLeft() + posToBitmap);
-		CIntObject::showAll(to);
-	}
-
-	to.drawText(pos.topLeft() + posToSubCenter, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, subtitle);
-}
-
 void CTradeableItem::clickPressed(const Point & cursorPosition)
 {
 	if(clickPressedCallback)
 		clickPressedCallback(shared_from_this());
 }
 
-void CTradeableItem::showAllAt(const Point & dstPos, const std::string & customSub, Canvas & to)
-{
-	Rect oldPos = pos;
-	std::string oldSub = subtitle;
-	downSelection = true;
-
-	moveTo(dstPos);
-	subtitle = customSub;
-	showAll(to);
-
-	downSelection = false;
-	moveTo(oldPos.topLeft());
-	subtitle = oldSub;
-}
-
 void CTradeableItem::hover(bool on)
 {
-	if(!on)
+	if(!on || id == -1)
 	{
 		GH.statusbar()->clear();
 		return;
@@ -196,12 +172,19 @@ void CTradeableItem::hover(bool on)
 	case EType::CREATURE_PLACEHOLDER:
 		GH.statusbar()->write(boost::str(boost::format(CGI->generaltexth->allTexts[481]) % CGI->creh->objects[id]->getNamePluralTranslated()));
 		break;
+	case EType::ARTIFACT_TYPE:
 	case EType::ARTIFACT_PLACEHOLDER:
 		if(id < 0)
 			GH.statusbar()->write(CGI->generaltexth->zelp[582].first);
 		else
 			GH.statusbar()->write(CGI->artifacts()->getByIndex(id)->getNameTranslated());
 		break;
+	case EType::RESOURCE:
+		GH.statusbar()->write(CGI->generaltexth->restypes[id]);
+		break;
+	case EType::PLAYER:
+		GH.statusbar()->write(CGI->generaltexth->capColors[id]);
+		break;
 	}
 }
 
@@ -221,51 +204,11 @@ void CTradeableItem::showPopupWindow(const Point & cursorPosition)
 	}
 }
 
-std::string CTradeableItem::getName(int number) const
-{
-	switch(type)
-	{
-	case EType::PLAYER:
-		return CGI->generaltexth->capColors[id];
-	case EType::RESOURCE:
-		return CGI->generaltexth->restypes[id];
-	case EType::CREATURE:
-		if (number == 1)
-			return CGI->creh->objects[id]->getNameSingularTranslated();
-		else
-			return CGI->creh->objects[id]->getNamePluralTranslated();
-	case EType::ARTIFACT_TYPE:
-	case EType::ARTIFACT_INSTANCE:
-		return CGI->artifacts()->getByIndex(id)->getNameTranslated();
-	}
-	logGlobal->error("Invalid trade item type: %d", (int)type);
-	return "";
-}
-
-const CArtifactInstance * CTradeableItem::getArtInstance() const
+void TradePanelBase::update()
 {
-	switch(type)
-	{
-	case EType::ARTIFACT_PLACEHOLDER:
-	case EType::ARTIFACT_INSTANCE:
-		return artInstance;
-	default:
-		return nullptr;
-	}
-}
-
-void CTradeableItem::setArtInstance(const CArtifactInstance * art)
-{
-	assert(type == EType::ARTIFACT_PLACEHOLDER || type == EType::ARTIFACT_INSTANCE);
-	artInstance = art;
-	if(art)
-		setID(art->getTypeId());
-	else
-		setID(-1);
-}
+	if(deleteSlotsCheck)
+		slots.erase(std::remove_if(slots.begin(), slots.end(), deleteSlotsCheck), slots.end());
 
-void TradePanelBase::updateSlots()
-{
 	if(updateSlotsCallback)
 		updateSlotsCallback();
 }
@@ -279,42 +222,68 @@ void TradePanelBase::deselect()
 void TradePanelBase::clearSubtitles()
 {
 	for(const auto & slot : slots)
-		slot->subtitle.clear();
+		slot->subtitle->clear();
 }
 
 void TradePanelBase::updateOffer(CTradeableItem & slot, int cost, int qty)
 {
-	slot.subtitle = std::to_string(qty);
+	std::string subtitle = std::to_string(qty);
 	if(cost != 1)
 	{
-		slot.subtitle.append("/");
-		slot.subtitle.append(std::to_string(cost));
+		subtitle.append("/");
+		subtitle.append(std::to_string(cost));
 	}
+	slot.subtitle->setText(subtitle);
 }
 
-void TradePanelBase::deleteSlots()
+void TradePanelBase::setShowcaseSubtitle(const std::string & text)
 {
-	if(deleteSlotsCheck)
-		slots.erase(std::remove_if(slots.begin(), slots.end(), deleteSlotsCheck), slots.end());
+	showcaseSlot->subtitle->setText(text);
 }
 
-ResourcesPanel::ResourcesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, UpdateSlotsFunctor updateSubtitles)
+int TradePanelBase::getSelectedItemId() const
+{
+	if(highlightedSlot)
+		return highlightedSlot->id;
+	else
+		return -1;
+}
+
+void TradePanelBase::onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot)
+{
+	assert(vstd::contains(slots, newSlot));
+	if(newSlot == highlightedSlot)
+		return;
+
+	if(highlightedSlot)
+		highlightedSlot->selectSlot(false);
+	highlightedSlot = newSlot;
+	newSlot->selectSlot(true);
+}
+
+bool TradePanelBase::isHighlighted() const
+{
+	return getSelectedItemId() != -1;
+}
+
+ResourcesPanel::ResourcesPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback,
+	const UpdateSlotsFunctor & updateSubtitles)
 {
 	assert(resourcesForTrade.size() == slotsPos.size());
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
 
 	for(const auto & res : resourcesForTrade)
 	{
-		auto slot = slots.emplace_back(std::make_shared<CTradeableItem>(Rect(slotsPos[res.num], slotDimension),
-			EType::RESOURCE, res.num, true, res.num));
+		auto slot = slots.emplace_back(std::make_shared<CTradeableItem>(Rect(slotsPos[res.num], slotDimension), EType::RESOURCE, res.num, res.num));
 		slot->clickPressedCallback = clickPressedCallback;
 		slot->setSelectionWidth(selectionWidth);
 	}
 	updateSlotsCallback = updateSubtitles;
+	showcaseSlot = std::make_shared<CTradeableItem>(Rect(selectedPos, slotDimension), EType::RESOURCE, 0, 0);
 }
 
-ArtifactsPanel::ArtifactsPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, UpdateSlotsFunctor updateSubtitles,
-	const std::vector<TradeItemBuy> & arts)
+ArtifactsPanel::ArtifactsPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback,
+	const UpdateSlotsFunctor & updateSubtitles, const std::vector<TradeItemBuy> & arts)
 {
 	assert(slotsForTrade == slotsPos.size());
 	assert(slotsForTrade == arts.size());
@@ -326,15 +295,17 @@ ArtifactsPanel::ArtifactsPanel(CTradeableItem::ClickPressedFunctor clickPressedC
 		if(artType != ArtifactID::NONE)
 		{
 			auto slot = slots.emplace_back(std::make_shared<CTradeableItem>(Rect(slotsPos[slotIdx], slotDimension),
-				EType::ARTIFACT_TYPE, artType, false, slotIdx));
+				EType::ARTIFACT_TYPE, artType, slotIdx));
 			slot->clickPressedCallback = clickPressedCallback;
 			slot->setSelectionWidth(selectionWidth);
 		}
 	}
 	updateSlotsCallback = updateSubtitles;
+	showcaseSlot = std::make_shared<CTradeableItem>(Rect(selectedPos, slotDimension), EType::ARTIFACT_TYPE, 0, 0);
+	showcaseSlot->subtitle->moveBy(Point(0, 1));
 }
 
-PlayersPanel::PlayersPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback)
+PlayersPanel::PlayersPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback)
 {
 	assert(PlayerColor::PLAYER_LIMIT_I <= slotsPos.size() + 1);
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
@@ -350,15 +321,16 @@ PlayersPanel::PlayersPanel(CTradeableItem::ClickPressedFunctor clickPressedCallb
 	int slotNum = 0;
 	for(auto & slot : slots)
 	{
-		slot = std::make_shared<CTradeableItem>(Rect(slotsPos[slotNum], slotDimension), EType::PLAYER, players[slotNum].num, false, slotNum);
+		slot = std::make_shared<CTradeableItem>(Rect(slotsPos[slotNum], slotDimension), EType::PLAYER, players[slotNum].num, slotNum);
 		slot->clickPressedCallback = clickPressedCallback;
 		slot->setSelectionWidth(selectionWidth);
-		slot->subtitle = CGI->generaltexth->capColors[players[slotNum].num];
+		slot->subtitle->setText(CGI->generaltexth->capColors[players[slotNum].num]);
 		slotNum++;
 	}
+	showcaseSlot = std::make_shared<CTradeableItem>(Rect(selectedPos, slotDimension), EType::PLAYER, 0, 0);
 }
 
-CreaturesPanel::CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, const slotsData & initialSlots)
+CreaturesPanel::CreaturesPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback, const slotsData & initialSlots)
 {
 	assert(initialSlots.size() <= GameConstants::ARMY_SIZE);
 	assert(slotsPos.size() <= GameConstants::ARMY_SIZE);
@@ -367,15 +339,16 @@ CreaturesPanel::CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedC
 	for(const auto & [creatureId, slotId, creaturesNum] : initialSlots)
 	{
 		auto slot = slots.emplace_back(std::make_shared<CTradeableItem>(Rect(slotsPos[slotId.num], slotDimension),
-			creaturesNum == 0 ? EType::CREATURE_PLACEHOLDER : EType::CREATURE, creatureId.num, true, slotId));
+			creaturesNum == 0 ? EType::CREATURE_PLACEHOLDER : EType::CREATURE, creatureId.num, slotId));
 		slot->clickPressedCallback = clickPressedCallback;
 		if(creaturesNum != 0)
-			slot->subtitle = std::to_string(creaturesNum);
+			slot->subtitle->setText(std::to_string(creaturesNum));
 		slot->setSelectionWidth(selectionWidth);
 	}
+	showcaseSlot = std::make_shared<CTradeableItem>(Rect(selectedPos, slotDimension), EType::CREATURE, 0, 0);
 }
 
-CreaturesPanel::CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback,
+CreaturesPanel::CreaturesPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback,
 	const std::vector<std::shared_ptr<CTradeableItem>> & srcSlots, bool emptySlots)
 {
 	assert(slots.size() <= GameConstants::ARMY_SIZE);
@@ -384,9 +357,27 @@ CreaturesPanel::CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedC
 	for(const auto & srcSlot : srcSlots)
 	{
 		auto slot = slots.emplace_back(std::make_shared<CTradeableItem>(Rect(slotsPos[srcSlot->serial], srcSlot->pos.dimensions()),
-			emptySlots ? EType::CREATURE_PLACEHOLDER : EType::CREATURE, srcSlot->id, true, srcSlot->serial));
+			emptySlots ? EType::CREATURE_PLACEHOLDER : EType::CREATURE, srcSlot->id, srcSlot->serial));
 		slot->clickPressedCallback = clickPressedCallback;
-		slot->subtitle = emptySlots ? "" : srcSlot->subtitle;
+		slot->subtitle->setText(emptySlots ? "" : srcSlot->subtitle->getText());
 		slot->setSelectionWidth(selectionWidth);
 	}
+	showcaseSlot = std::make_shared<CTradeableItem>(Rect(selectedPos, slotDimension), EType::CREATURE, 0, 0);
+}
+
+ArtifactsAltarPanel::ArtifactsAltarPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback)
+{
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	int slotNum = 0;
+	for(auto & altarSlotPos : slotsPos)
+	{
+		auto slot = slots.emplace_back(std::make_shared<CTradeableItem>(Rect(altarSlotPos, Point(44, 44)), EType::ARTIFACT_PLACEHOLDER, -1, slotNum));
+		slot->clickPressedCallback = clickPressedCallback;
+		slot->subtitle->clear();
+		slot->subtitle->moveBy(Point(0, -1));
+		slotNum++;
+	}
+	showcaseSlot = std::make_shared<CTradeableItem>(Rect(selectedPos, slotDimension), EType::ARTIFACT_TYPE, 0, 0);
+	showcaseSlot->subtitle->moveBy(Point(0, 3));
 }

+ 44 - 26
client/widgets/markets/TradePanels.h

@@ -27,30 +27,20 @@ public:
 	int getIndex();
 	using ClickPressedFunctor = std::function<void(const std::shared_ptr<CTradeableItem>&)>;
 
-	const CArtifactInstance * artInstance; //holds ptr to artifact instance id type artifact
 	EType type;
 	int id;
 	const int serial;
-	const bool left;
-	std::string subtitle;
+	std::shared_ptr<CLabel> subtitle;
 	ClickPressedFunctor clickPressedCallback;
 
 	void setType(EType newType);
 	void setID(int newID);
-
-	const CArtifactInstance * getArtInstance() const;
-	void setArtInstance(const CArtifactInstance * art);
-
-	bool downSelection;
-
-	void showAllAt(const Point & dstPos, const std::string & customSub, Canvas & to);
+	void clear();
 
 	void showPopupWindow(const Point & cursorPosition) override;
 	void hover(bool on) override;
-	void showAll(Canvas & to) override;
 	void clickPressed(const Point & cursorPosition) override;
-	std::string getName(int number = -1) const;
-	CTradeableItem(const Rect & area, EType Type, int ID, bool Left, int Serial);
+	CTradeableItem(const Rect & area, EType Type, int ID, int Serial);
 };
 
 class TradePanelBase : public CIntObject
@@ -62,14 +52,18 @@ public:
 	std::vector<std::shared_ptr<CTradeableItem>> slots;
 	UpdateSlotsFunctor updateSlotsCallback;
 	DeleteSlotsCheck deleteSlotsCheck;
-	std::shared_ptr<CTradeableItem> selected;
 	const int selectionWidth = 2;
+	std::shared_ptr<CTradeableItem> showcaseSlot;		// Separate slot that displays the contents for trading
+	std::shared_ptr<CTradeableItem> highlightedSlot;	// One of the slots highlighted by a frame
 
-	virtual void updateSlots();
+	virtual void update();
 	virtual void deselect();
 	virtual void clearSubtitles();
 	void updateOffer(CTradeableItem & slot, int, int);
-	void deleteSlots();
+	void setShowcaseSubtitle(const std::string & text);
+	int getSelectedItemId() const;
+	void onSlotClickPressed(const std::shared_ptr<CTradeableItem> & newSlot);
+	bool isHighlighted() const;
 };
 
 class ResourcesPanel : public TradePanelBase
@@ -87,25 +81,27 @@ class ResourcesPanel : public TradePanelBase
 		Point(83, 158)
 	};
 	const Point slotDimension = Point(69, 66);
+	const Point selectedPos = Point(83, 267);
 
 public:
-	ResourcesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, UpdateSlotsFunctor updateSubtitles);
+	ResourcesPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback, const UpdateSlotsFunctor & updateSubtitles);
 };
 
 class ArtifactsPanel : public TradePanelBase
 {
 	const std::vector<Point> slotsPos =
 	{
-		Point(0, 0), Point(83, 0), Point(166, 0),
-		Point(0, 79), Point(83, 79), Point(166, 79),
+		Point(0, 0), Point(83, 0), Point(165, 0),
+		Point(0, 79), Point(83, 79), Point(165, 79),
 		Point(83, 158)
 	};
 	const size_t slotsForTrade = 7;
-	const Point slotDimension = Point(69, 66);
+	const Point slotDimension = Point(69, 68);
+	const Point selectedPos = Point(83, 266);
 
 public:
-	ArtifactsPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, UpdateSlotsFunctor updateSubtitles,
-		const std::vector<TradeItemBuy> & arts);
+	ArtifactsPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback,
+		const UpdateSlotsFunctor & updateSubtitles, const std::vector<TradeItemBuy> & arts);
 };
 
 class PlayersPanel : public TradePanelBase
@@ -117,9 +113,10 @@ class PlayersPanel : public TradePanelBase
 		Point(83, 236)
 	};
 	const Point slotDimension = Point(58, 64);
+	const Point selectedPos = Point(83, 367);
 
 public:
-	explicit PlayersPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback);
+	explicit PlayersPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback);
 };
 
 class CreaturesPanel : public TradePanelBase
@@ -130,12 +127,33 @@ class CreaturesPanel : public TradePanelBase
 		Point(0, 98), Point(83, 98), Point(166, 98),
 		Point(83, 196)
 	};
-	const Point slotDimension = Point(58, 64);
+	const Point slotDimension = Point(59, 64);
+	const Point selectedPos = Point(83, 327);
 
 public:
 	using slotsData = std::vector<std::tuple<CreatureID, SlotID, int>>;
 
-	CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback, const slotsData & initialSlots);
-	CreaturesPanel(CTradeableItem::ClickPressedFunctor clickPressedCallback,
+	CreaturesPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback, const slotsData & initialSlots);
+	CreaturesPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback,
 		const std::vector<std::shared_ptr<CTradeableItem>> & srcSlots, bool emptySlots = true);
 };
+
+class ArtifactsAltarPanel : public TradePanelBase
+{
+	const std::vector<Point> slotsPos =
+	{
+		Point(0, 0), Point(54, 0), Point(108, 0),
+		Point(162, 0), Point(216, 0), Point(0, 70),
+		Point(54, 70), Point(108, 70), Point(162, 70),
+		Point(216, 70), Point(0, 140), Point(54, 140),
+		Point(108, 140), Point(162, 140), Point(216, 140),
+		Point(0, 210), Point(54, 210), Point(108, 210),
+		Point(162, 210), Point(216, 210), Point(81, 280),
+		Point(135, 280)
+	};
+	const Point slotDimension = Point(69, 66);
+	const Point selectedPos = Point(-48, 389);
+
+public:
+	explicit ArtifactsAltarPanel(const CTradeableItem::ClickPressedFunctor & clickPressedCallback);
+};

+ 0 - 147
client/windows/CAltarWindow.cpp

@@ -1,147 +0,0 @@
-/*
- * CAltarWindow.cpp, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-
-#include "StdInc.h"
-#include "CAltarWindow.h"
-
-#include "../gui/CGuiHandler.h"
-#include "../render/Canvas.h"
-#include "../gui/Shortcut.h"
-#include "../widgets/Buttons.h"
-#include "../widgets/TextControls.h"
-
-#include "../CGameInfo.h"
-
-#include "../lib/networkPacks/ArtifactLocation.h"
-#include "../../lib/CGeneralTextHandler.h"
-#include "../../lib/CHeroHandler.h"
-#include "../../lib/mapObjects/CGHeroInstance.h"
-
-CAltarWindow::CAltarWindow(const IMarket * market, const CGHeroInstance * hero, const std::function<void()> & onWindowClosed, EMarketMode mode)
-	: CWindowObject(PLAYER_COLORED, ImagePath::builtin(mode == EMarketMode::CREATURE_EXP ? "ALTARMON.bmp" : "ALTRART2.bmp"))
-	, hero(hero)
-	, windowClosedCallback(onWindowClosed)
-{
-	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
-
-	assert(mode == EMarketMode::ARTIFACT_EXP || mode == EMarketMode::CREATURE_EXP);
-	if(mode == EMarketMode::ARTIFACT_EXP)
-		createAltarArtifacts(market, hero);
-	else if(mode == EMarketMode::CREATURE_EXP)
-		createAltarCreatures(market, hero);
-
-	updateExpToLevel();
-	statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26));
-}
-
-void CAltarWindow::updateExpToLevel()
-{
-	altar->expToLevel->setText(std::to_string(CGI->heroh->reqExp(CGI->heroh->level(altar->hero->exp) + 1) - altar->hero->exp));
-}
-
-void CAltarWindow::updateGarrisons()
-{
-	if(auto altarCreatures = std::static_pointer_cast<CAltarCreatures>(altar))
-		altarCreatures->updateSlots();
-}
-
-bool CAltarWindow::holdsGarrison(const CArmedInstance * army)
-{
-	return hero == army;
-}
-
-const CGHeroInstance * CAltarWindow::getHero() const
-{
-	return hero;
-}
-
-void CAltarWindow::close()
-{
-	if(windowClosedCallback)
-		windowClosedCallback();
-
-	CWindowObject::close();
-}
-
-void CAltarWindow::createAltarArtifacts(const IMarket * market, const CGHeroInstance * hero)
-{
-	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
-
-	background = createBg(ImagePath::builtin("ALTRART2.bmp"), PLAYER_COLORED);
-
-	auto altarArtifacts = std::make_shared<CAltarArtifacts>(market, hero);
-	altar = altarArtifacts;
-	artSets.clear();
-	addSetAndCallbacks(altarArtifacts->getAOHset()); altarArtifacts->putBackArtifacts();
-
-	changeModeButton = std::make_shared<CButton>(Point(516, 421), AnimationPath::builtin("ALTSACC.DEF"),
-		CGI->generaltexth->zelp[572], std::bind(&CAltarWindow::createAltarCreatures, this, market, hero));
-	if(altar->hero->getAlignment() == EAlignment::GOOD)
-		changeModeButton->block(true);
-	quitButton = std::make_shared<CButton>(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"),
-		CGI->generaltexth->zelp[568], [this, altarArtifacts]()
-		{
-			altarArtifacts->putBackArtifacts();
-			CAltarWindow::close();
-		}, EShortcut::GLOBAL_RETURN);
-	altar->setRedrawParent(true);
-	redraw();
-}
-
-void CAltarWindow::createAltarCreatures(const IMarket * market, const CGHeroInstance * hero)
-{
-	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
-
-	background = createBg(ImagePath::builtin("ALTARMON.bmp"), PLAYER_COLORED);
-
-	altar = std::make_shared<CAltarCreatures>(market, hero);
-
-	changeModeButton = std::make_shared<CButton>(Point(516, 421), AnimationPath::builtin("ALTART.DEF"),
-		CGI->generaltexth->zelp[580], std::bind(&CAltarWindow::createAltarArtifacts, this, market, hero));
-	if(altar->hero->getAlignment() == EAlignment::EVIL)
-		changeModeButton->block(true);
-	quitButton = std::make_shared<CButton>(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"),
-		CGI->generaltexth->zelp[568], std::bind(&CAltarWindow::close, this), EShortcut::GLOBAL_RETURN);
-	altar->setRedrawParent(true);
-	redraw();
-}
-
-void CAltarWindow::artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw)
-{
-	if(!getState().has_value())
-		return;
-
-	if(auto altarArtifacts = std::static_pointer_cast<CAltarArtifacts>(altar))
-	{
-		if(srcLoc.artHolder == altarArtifacts->getObjId() || destLoc.artHolder == altarArtifacts->getObjId())
-			altarArtifacts->updateSlots();
-
-		if(const auto pickedArt = getPickedArtifact())
-			altarArtifacts->setSelectedArtifact(pickedArt);
-		else
-			altarArtifacts->setSelectedArtifact(nullptr);
-	}
-	CWindowWithArtifacts::artifactMoved(srcLoc, destLoc, withRedraw);
-}
-
-void CAltarWindow::showAll(Canvas & to)
-{
-	// This func is temporary workaround for compliance with CTradeWindow
-	CWindowObject::showAll(to);
-
-	if(altar->hRight)
-	{
-		altar->hRight->showAllAt(altar->pos.topLeft() + Point(396, 423), "", to);
-	}
-	if(altar->hLeft)
-	{
-		altar->hLeft->showAllAt(altar->pos.topLeft() + Point(150, 423), "", to);
-	}
-}

+ 0 - 40
client/windows/CAltarWindow.h

@@ -1,40 +0,0 @@
-/*
- * CAltarWindow.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#pragma once
-
-#include "../widgets/markets/CAltarArtifacts.h"
-#include "../widgets/markets/CAltarCreatures.h"
-#include "../widgets/CWindowWithArtifacts.h"
-#include "CWindowObject.h"
-
-class CAltarWindow : public CWindowObject, public CWindowWithArtifacts, public IGarrisonHolder
-{
-public:
-	CAltarWindow(const IMarket * market, const CGHeroInstance * hero, const std::function<void()> & onWindowClosed, EMarketMode mode);
-	void updateExpToLevel();
-	void updateGarrisons() override;
-	bool holdsGarrison(const CArmedInstance * army) override;
-	const CGHeroInstance * getHero() const;
-	void close() override;
-
-	void artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) override;
-	void showAll(Canvas & to) override;
-
-private:
-	const CGHeroInstance * hero;
-	std::shared_ptr<CExperienceAltar> altar;
-	std::shared_ptr<CButton> changeModeButton;
-	std::shared_ptr<CButton> quitButton;
-	std::function<void()> windowClosedCallback;
-	std::shared_ptr<CGStatusBar> statusBar;
-
-	void createAltarArtifacts(const IMarket * market, const CGHeroInstance * hero);
-	void createAltarCreatures(const IMarket * market, const CGHeroInstance * hero);
-};

+ 7 - 7
client/windows/CCastleInterface.cpp

@@ -11,7 +11,7 @@
 #include "CCastleInterface.h"
 
 #include "CHeroWindow.h"
-#include "CTradeWindow.h"
+#include "CMarketWindow.h"
 #include "InfoWindows.h"
 #include "GUIClasses.h"
 #include "QuickRecruitmentWindow.h"
@@ -685,7 +685,7 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil
 	logGlobal->trace("You've clicked on %d", (int)building.toEnum());
 	const CBuilding *b = town->town->buildings.find(building)->second;
 
-	if(building >= BuildingID::DWELL_FIRST)
+	if (building >= BuildingID::DWELL_FIRST)
 	{
 		enterDwelling((building-BuildingID::DWELL_FIRST)%GameConstants::CREATURES_PER_TOWN);
 	}
@@ -728,7 +728,7 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil
 		case BuildingID::MARKETPLACE:
 				// can't use allied marketplace
 				if (town->getOwner() == LOCPLINT->playerID)
-					GH.windows().createAndPushWindow<CMarketplaceWindow>(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE);
+					GH.windows().createAndPushWindow<CMarketWindow>(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_RESOURCE);
 				else
 					enterBuilding(building);
 				break;
@@ -744,7 +744,7 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil
 		case BuildingID::SPECIAL_1:
 		case BuildingID::SPECIAL_2:
 		case BuildingID::SPECIAL_3:
-				switch(subID)
+				switch (subID)
 				{
 				case BuildingSubID::NONE:
 						enterBuilding(building);
@@ -756,7 +756,7 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil
 
 				case BuildingSubID::ARTIFACT_MERCHANT:
 						if(town->visitingHero)
-							GH.windows().createAndPushWindow<CMarketplaceWindow>(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_ARTIFACT);
+							GH.windows().createAndPushWindow<CMarketWindow>(town, town->visitingHero, nullptr, EMarketMode::RESOURCE_ARTIFACT);
 						else
 							LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s.
 						break;
@@ -767,7 +767,7 @@ void CCastleBuildings::buildingClicked(BuildingID building, BuildingSubID::EBuil
 
 				case BuildingSubID::FREELANCERS_GUILD:
 						if(getHero())
-							GH.windows().createAndPushWindow<CMarketplaceWindow>(town, getHero(), nullptr, EMarketMode::CREATURE_RESOURCE);
+							GH.windows().createAndPushWindow<CMarketWindow>(town, getHero(), nullptr, EMarketMode::CREATURE_RESOURCE);
 						else
 							LOCPLINT->showInfoDialog(boost::str(boost::format(CGI->generaltexth->allTexts[273]) % b->getNameTranslated())); //Only visiting heroes may use the %s.
 						break;
@@ -1338,7 +1338,7 @@ void CCastleInterface::recreateIcons()
 		{
 			if(town->builtBuildings.count(BuildingID::MARKETPLACE))
 			{
-				GH.windows().createAndPushWindow<CMarketplaceWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
+				GH.windows().createAndPushWindow<CMarketWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
 				return;
 			}
 		}

+ 4 - 4
client/windows/CKingdomInterface.cpp

@@ -26,7 +26,7 @@
 #include "../widgets/MiscWidgets.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/ObjectLists.h"
-#include "../windows/CTradeWindow.h"
+#include "../windows/CMarketWindow.h"
 
 #include "../../CCallback.h"
 
@@ -821,11 +821,11 @@ CTownItem::CTownItem(const CGTownInstance * Town)
 		available.push_back(std::make_shared<CCreaInfo>(Point(48+37*(int)i, 78), town, (int)i, true, false));
 	}
 
-	fastTownHall = std::make_shared<CButton>(Point(69, 31), AnimationPath::builtin("castleInterfaceQuickAccessz"), CButton::tooltip(), [this]() { std::make_shared<CCastleBuildings>(town)->enterTownHall(); });
+	fastTownHall = std::make_shared<CButton>(Point(69, 31), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this]() { std::make_shared<CCastleBuildings>(town)->enterTownHall(); });
 	fastTownHall->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("ITMTL"), town->hallLevel()));
 
 	int imageIndex = town->fortLevel() == CGTownInstance::EFortLevel::NONE ? 3 : town->fortLevel() - 1;
-	fastArmyPurchase = std::make_shared<CButton>(Point(111, 31), AnimationPath::builtin("castleInterfaceQuickAccessz"), CButton::tooltip(), [this]() { std::make_shared<CCastleBuildings>(town)->enterToTheQuickRecruitmentWindow(); });
+	fastArmyPurchase = std::make_shared<CButton>(Point(111, 31), AnimationPath::builtin("castleInterfaceQuickAccess"), CButton::tooltip(), [this]() { std::make_shared<CCastleBuildings>(town)->enterToTheQuickRecruitmentWindow(); });
 	fastArmyPurchase->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("itmcl"), imageIndex));
 
 	fastTavern = std::make_shared<LRClickableArea>(Rect(5, 6, 58, 64), [&]()
@@ -840,7 +840,7 @@ CTownItem::CTownItem(const CGTownInstance * Town)
 		{
 			if(town->builtBuildings.count(BuildingID::MARKETPLACE))
 			{
-				GH.windows().createAndPushWindow<CMarketplaceWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
+				GH.windows().createAndPushWindow<CMarketWindow>(town, nullptr, nullptr, EMarketMode::RESOURCE_RESOURCE);
 				return;
 			}
 		}

+ 263 - 0
client/windows/CMarketWindow.cpp

@@ -0,0 +1,263 @@
+/*
+ * CMarketWindow.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+
+#include "StdInc.h"
+#include "CMarketWindow.h"
+
+#include "../gui/CGuiHandler.h"
+#include "../gui/Shortcut.h"
+
+#include "../widgets/Buttons.h"
+#include "../widgets/TextControls.h"
+#include "../widgets/markets/CAltarArtifacts.h"
+#include "../widgets/markets/CAltarCreatures.h"
+#include "../widgets/markets/CArtifactsBuying.h"
+#include "../widgets/markets/CArtifactsSelling.h"
+#include "../widgets/markets/CFreelancerGuild.h"
+#include "../widgets/markets/CMarketResources.h"
+#include "../widgets/markets/CTransferResources.h"
+
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+
+#include "../../lib/CGeneralTextHandler.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/mapObjects/CGMarket.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+
+CMarketWindow::CMarketWindow(const IMarket * market, const CGHeroInstance * hero, const std::function<void()> & onWindowClosed, EMarketMode mode)
+	: CStatusbarWindow(PLAYER_COLORED)
+	, windowClosedCallback(onWindowClosed)
+{
+	assert(mode == EMarketMode::RESOURCE_RESOURCE || mode == EMarketMode::RESOURCE_PLAYER || mode == EMarketMode::CREATURE_RESOURCE ||
+		mode == EMarketMode::RESOURCE_ARTIFACT || mode == EMarketMode::ARTIFACT_RESOURCE || mode == EMarketMode::ARTIFACT_EXP ||
+		mode == EMarketMode::CREATURE_EXP);
+	
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	if(mode == EMarketMode::RESOURCE_RESOURCE)
+		createMarketResources(market, hero);
+	else if(mode == EMarketMode::RESOURCE_PLAYER)
+		createTransferResources(market, hero);
+	else if(mode == EMarketMode::CREATURE_RESOURCE)
+		createFreelancersGuild(market, hero);
+	else if(mode == EMarketMode::RESOURCE_ARTIFACT)
+		createArtifactsBuying(market, hero);
+	else if(mode == EMarketMode::ARTIFACT_RESOURCE)
+		createArtifactsSelling(market, hero);
+	else if(mode == EMarketMode::ARTIFACT_EXP)
+		createAltarArtifacts(market, hero);
+	else if(mode == EMarketMode::CREATURE_EXP)
+		createAltarCreatures(market, hero);
+
+	statusbar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26));
+}
+
+void CMarketWindow::updateArtifacts()
+{
+	assert(marketWidget);
+	marketWidget->update();
+}
+
+void CMarketWindow::updateGarrisons()
+{
+	assert(marketWidget);
+	marketWidget->update();
+}
+
+void CMarketWindow::updateResource()
+{
+	assert(marketWidget);
+	marketWidget->update();
+}
+
+void CMarketWindow::updateHero()
+{
+	assert(marketWidget);
+	marketWidget->update();
+}
+
+void CMarketWindow::close()
+{
+	if(windowClosedCallback)
+		windowClosedCallback();
+
+	CWindowObject::close();
+}
+
+bool CMarketWindow::holdsGarrison(const CArmedInstance * army)
+{
+	assert(marketWidget);
+	return marketWidget->hero == army;
+}
+
+void CMarketWindow::artifactRemoved(const ArtifactLocation & artLoc)
+{
+	marketWidget->update();
+	CWindowWithArtifacts::artifactRemoved(artLoc);
+}
+
+void CMarketWindow::artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw)
+{
+	if(!getState().has_value())
+		return;
+	CWindowWithArtifacts::artifactMoved(srcLoc, destLoc, withRedraw);
+	assert(marketWidget);
+	marketWidget->update();
+}
+
+void CMarketWindow::createChangeModeButtons(EMarketMode currentMode, const IMarket * market, const CGHeroInstance * hero)
+{
+	auto isButtonVisible = [currentMode, market, hero](EMarketMode modeButton) -> bool
+	{
+		if(currentMode == modeButton)
+			return false;
+
+		if(!market->allowsTrade(modeButton))
+			return false;
+
+		if(modeButton == EMarketMode::RESOURCE_RESOURCE || modeButton == EMarketMode::RESOURCE_PLAYER)
+		{
+			if(const auto town = dynamic_cast<const CGTownInstance*>(market))
+				return town->getOwner() == LOCPLINT->playerID;
+			else
+				return true;
+		}
+		else
+		{
+			return hero != nullptr;
+		}
+	};
+
+	changeModeButtons.clear();
+	auto buttonPos = Point(18, 520);
+
+	auto addButton = [this, &buttonPos](const AnimationPath & picPath, const std::pair<std::string, std::string> & buttonHelpContainer,
+		const std::function<void()> & pressButtonFunctor)
+	{
+		changeModeButtons.emplace_back(std::make_shared<CButton>(buttonPos, picPath, buttonHelpContainer, pressButtonFunctor));
+		buttonPos -= Point(0, buttonHeightWithMargin);
+	};
+
+	if(isButtonVisible(EMarketMode::RESOURCE_PLAYER))
+		addButton(AnimationPath::builtin("TPMRKBU1.DEF"), CGI->generaltexth->zelp[612], std::bind(&CMarketWindow::createTransferResources, this, market, hero));
+	if(isButtonVisible(EMarketMode::ARTIFACT_RESOURCE))
+		addButton(AnimationPath::builtin("TPMRKBU3.DEF"), CGI->generaltexth->zelp[613], std::bind(&CMarketWindow::createArtifactsSelling, this, market, hero));
+	if(isButtonVisible(EMarketMode::RESOURCE_ARTIFACT))
+		addButton(AnimationPath::builtin("TPMRKBU2.DEF"), CGI->generaltexth->zelp[598], std::bind(&CMarketWindow::createArtifactsBuying, this, market, hero));
+
+	buttonPos = Point(516, 520 - buttonHeightWithMargin);
+	if(isButtonVisible(EMarketMode::CREATURE_RESOURCE))
+		addButton(AnimationPath::builtin("TPMRKBU4.DEF"), CGI->generaltexth->zelp[599], std::bind(&CMarketWindow::createFreelancersGuild, this, market, hero));
+	if(isButtonVisible(EMarketMode::RESOURCE_RESOURCE))
+		addButton(AnimationPath::builtin("TPMRKBU5.DEF"), CGI->generaltexth->zelp[605], std::bind(&CMarketWindow::createMarketResources, this, market, hero));
+	
+	buttonPos = Point(516, 421);
+	if(isButtonVisible(EMarketMode::CREATURE_EXP))
+	{
+		addButton(AnimationPath::builtin("ALTSACC.DEF"), CGI->generaltexth->zelp[572], std::bind(&CMarketWindow::createAltarCreatures, this, market, hero));
+		if(marketWidget->hero->getAlignment() == EAlignment::GOOD)
+			changeModeButtons.back()->block(true);
+	}
+	if(isButtonVisible(EMarketMode::ARTIFACT_EXP))
+	{
+		addButton(AnimationPath::builtin("ALTART.DEF"), CGI->generaltexth->zelp[580], std::bind(&CMarketWindow::createAltarArtifacts, this, market, hero));
+		if(marketWidget->hero->getAlignment() == EAlignment::EVIL)
+			changeModeButtons.back()->block(true);
+	}
+}
+
+void CMarketWindow::initWidgetInternals(const EMarketMode mode, const std::pair<std::string, std::string> & quitButtonHelpContainer)
+{
+	background->center();
+	pos = background->pos;
+	marketWidget->setRedrawParent(true);
+	marketWidget->moveTo(pos.topLeft());
+
+	createChangeModeButtons(mode, marketWidget->market, marketWidget->hero);
+	quitButton = std::make_shared<CButton>(quitButtonPos, AnimationPath::builtin("IOK6432.DEF"),
+		quitButtonHelpContainer, [this](){close();}, EShortcut::GLOBAL_RETURN);
+	redraw();
+}
+
+void CMarketWindow::createArtifactsBuying(const IMarket * market, const CGHeroInstance * hero)
+{
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	background = createBg(ImagePath::builtin("TPMRKABS.bmp"), PLAYER_COLORED);
+	marketWidget = std::make_shared<CArtifactsBuying>(market, hero);
+	initWidgetInternals(EMarketMode::RESOURCE_ARTIFACT, CGI->generaltexth->zelp[600]);
+}
+
+void CMarketWindow::createArtifactsSelling(const IMarket * market, const CGHeroInstance * hero)
+{
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	background = createBg(ImagePath::builtin("TPMRKASS.bmp"), PLAYER_COLORED);
+	// Create image that copies part of background containing slot MISC_1 into position of slot MISC_5
+	artSlotBack = std::make_shared<CPicture>(background->getSurface(), Rect(20, 187, 47, 47), 0, 0);
+	artSlotBack->moveTo(Point(358, 443));
+	auto artsSellingMarket = std::make_shared<CArtifactsSelling>(market, hero);
+	artSets.clear();
+	addSetAndCallbacks(artsSellingMarket->getAOHset());
+	marketWidget = artsSellingMarket;
+	initWidgetInternals(EMarketMode::ARTIFACT_RESOURCE, CGI->generaltexth->zelp[600]);	
+}
+
+void CMarketWindow::createMarketResources(const IMarket * market, const CGHeroInstance * hero)
+{
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	background = createBg(ImagePath::builtin("TPMRKRES.bmp"), PLAYER_COLORED);
+	marketWidget = std::make_shared<CMarketResources>(market, hero);
+	initWidgetInternals(EMarketMode::RESOURCE_RESOURCE, CGI->generaltexth->zelp[600]);
+}
+
+void CMarketWindow::createFreelancersGuild(const IMarket * market, const CGHeroInstance * hero)
+{
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	background = createBg(ImagePath::builtin("TPMRKCRS.bmp"), PLAYER_COLORED);
+	marketWidget = std::make_shared<CFreelancerGuild>(market, hero);
+	initWidgetInternals(EMarketMode::CREATURE_RESOURCE, CGI->generaltexth->zelp[600]);
+}
+
+void CMarketWindow::createTransferResources(const IMarket * market, const CGHeroInstance * hero)
+{
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	background = createBg(ImagePath::builtin("TPMRKPTS.bmp"), PLAYER_COLORED);
+	marketWidget = std::make_shared<CTransferResources>(market, hero);
+	initWidgetInternals(EMarketMode::RESOURCE_PLAYER, CGI->generaltexth->zelp[600]);
+}
+
+void CMarketWindow::createAltarArtifacts(const IMarket * market, const CGHeroInstance * hero)
+{
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	background = createBg(ImagePath::builtin("ALTRART2.bmp"), PLAYER_COLORED);
+	auto altarArtifacts = std::make_shared<CAltarArtifacts>(market, hero);
+	marketWidget = altarArtifacts;
+	artSets.clear();
+	addSetAndCallbacks(altarArtifacts->getAOHset());
+	initWidgetInternals(EMarketMode::ARTIFACT_EXP, CGI->generaltexth->zelp[568]);
+	updateHero();
+	quitButton->addCallback([altarArtifacts](){altarArtifacts->putBackArtifacts();});
+}
+
+void CMarketWindow::createAltarCreatures(const IMarket * market, const CGHeroInstance * hero)
+{
+	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255 - DISPOSE);
+
+	background = createBg(ImagePath::builtin("ALTARMON.bmp"), PLAYER_COLORED);
+	marketWidget = std::make_shared<CAltarCreatures>(market, hero);
+	initWidgetInternals(EMarketMode::CREATURE_EXP, CGI->generaltexth->zelp[568]);
+	updateHero();
+}

+ 50 - 0
client/windows/CMarketWindow.h

@@ -0,0 +1,50 @@
+/*
+ * CMarketWindow.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "../widgets/markets/CMarketBase.h"
+#include "../widgets/CWindowWithArtifacts.h"
+#include "CWindowObject.h"
+
+class CMarketWindow : public CStatusbarWindow, public CWindowWithArtifacts, public IGarrisonHolder
+{
+public:
+	CMarketWindow(const IMarket * market, const CGHeroInstance * hero, const std::function<void()> & onWindowClosed, EMarketMode mode);
+	void updateResource();
+	void updateArtifacts();
+	void updateGarrisons() override;
+	void updateHero();
+	void close() override;
+	bool holdsGarrison(const CArmedInstance * army) override;
+	void artifactRemoved(const ArtifactLocation & artLoc) override;
+	void artifactMoved(const ArtifactLocation & srcLoc, const ArtifactLocation & destLoc, bool withRedraw) override;
+
+private:
+	void createChangeModeButtons(EMarketMode currentMode, const IMarket * market, const CGHeroInstance * hero);
+	void initWidgetInternals(const EMarketMode mode, const std::pair<std::string, std::string> & quitButtonHelpContainer);
+
+	void createArtifactsBuying(const IMarket * market, const CGHeroInstance * hero);
+	void createArtifactsSelling(const IMarket * market, const CGHeroInstance * hero);
+	void createMarketResources(const IMarket * market, const CGHeroInstance * hero);
+	void createFreelancersGuild(const IMarket * market, const CGHeroInstance * hero);
+	void createTransferResources(const IMarket * market, const CGHeroInstance * hero);
+	void createAltarArtifacts(const IMarket * market, const CGHeroInstance * hero);
+	void createAltarCreatures(const IMarket * market, const CGHeroInstance * hero);
+
+	const int buttonHeightWithMargin = 32 + 3;
+	std::vector<std::shared_ptr<CButton>> changeModeButtons;
+	std::shared_ptr<CButton> quitButton;
+	std::function<void()> windowClosedCallback;
+	const Point quitButtonPos = Point(516, 520);
+	std::shared_ptr<CMarketBase> marketWidget;
+
+	// This is workaround for bug in H3 files where this slot for ragdoll on this screen is missing
+	std::shared_ptr<CPicture> artSlotBack;
+};

+ 19 - 5
client/windows/CSpellWindow.cpp

@@ -40,6 +40,7 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/spells/CSpellHandler.h"
+#include "../../lib/spells/ISpellMechanics.h"
 #include "../../lib/spells/Problem.h"
 #include "../../lib/GameConstants.h"
 
@@ -653,12 +654,25 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition)
 				owner->myInt->localState->spellbookSettings.spellbookLastPageAdvmap = owner->currentPage;
 			});
 
-			if(mySpell->getTargetType() == spells::AimType::LOCATION)
-				adventureInt->enterCastingMode(mySpell);
-			else if(mySpell->getTargetType() == spells::AimType::NO_TARGET)
-				owner->myInt->cb->castSpell(h, mySpell->id);
+			spells::detail::ProblemImpl problem;
+			if (mySpell->getAdventureMechanics().canBeCast(problem, LOCPLINT->cb.get(), owner->myHero))
+			{
+				if(mySpell->getTargetType() == spells::AimType::LOCATION)
+					adventureInt->enterCastingMode(mySpell);
+				else if(mySpell->getTargetType() == spells::AimType::NO_TARGET)
+					owner->myInt->cb->castSpell(h, mySpell->id);
+				else
+					logGlobal->error("Invalid spell target type");
+			}
 			else
-				logGlobal->error("Invalid spell target type");
+			{
+				std::vector<std::string> texts;
+				problem.getAll(texts);
+				if(!texts.empty())
+					LOCPLINT->showInfoDialog(texts.front());
+				else
+					LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.adventureMap.spellUnknownProblem"));
+			}
 		}
 	}
 }

+ 0 - 679
client/windows/CTradeWindow.cpp

@@ -1,679 +0,0 @@
-/*
- * CTradeWindow.cpp, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#include "StdInc.h"
-#include "CTradeWindow.h"
-
-#include "../gui/CGuiHandler.h"
-#include "../gui/CursorHandler.h"
-#include "../render/Canvas.h"
-#include "../gui/Shortcut.h"
-#include "../gui/WindowHandler.h"
-#include "../widgets/Buttons.h"
-#include "../widgets/Slider.h"
-#include "../widgets/TextControls.h"
-
-#include "../CGameInfo.h"
-#include "../CPlayerInterface.h"
-
-#include "../../CCallback.h"
-
-#include "../../lib/CGeneralTextHandler.h"
-#include "../../lib/CHeroHandler.h"
-#include "../../lib/mapObjects/CGHeroInstance.h"
-#include "../../lib/mapObjects/CGTownInstance.h"
-#include "../../lib/mapObjects/CGMarket.h"
-
-CTradeWindow::CTradeWindow(const ImagePath & bgName, const IMarket *Market, const CGHeroInstance *Hero, const std::function<void()> & onWindowClosed, EMarketMode Mode):
-	CTradeBase(Market, Hero),
-	CWindowObject(PLAYER_COLORED, bgName),
-	onWindowClosed(onWindowClosed),
-	readyToTrade(false)
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-
-	mode = Mode;
-	initTypes();
-}
-
-void CTradeWindow::initTypes()
-{
-	switch(mode)
-	{
-	case EMarketMode::RESOURCE_RESOURCE:
-		itemsType[1] = EType::RESOURCE;
-		itemsType[0] = EType::RESOURCE;
-		break;
-	case EMarketMode::RESOURCE_PLAYER:
-		itemsType[1] = EType::RESOURCE;
-		itemsType[0] = EType::PLAYER;
-		break;
-	case EMarketMode::CREATURE_RESOURCE:
-		itemsType[1] = EType::CREATURE;
-		itemsType[0] = EType::RESOURCE;
-		break;
-	case EMarketMode::RESOURCE_ARTIFACT:
-		itemsType[1] = EType::RESOURCE;
-		itemsType[0] = EType::ARTIFACT_TYPE;
-		break;
-	case EMarketMode::ARTIFACT_RESOURCE:
-		itemsType[1] = EType::ARTIFACT_INSTANCE;
-		itemsType[0] = EType::RESOURCE;
-		break;
-	}
-}
-
-void CTradeWindow::initItems(bool Left)
-{
-	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);
-
-	if(Left && (itemsType[1] == EType::ARTIFACT_TYPE || itemsType[1] == EType::ARTIFACT_INSTANCE))
-	{
-		if(mode == EMarketMode::ARTIFACT_RESOURCE)
-		{
-			auto item = std::make_shared<CTradeableItem>(Rect(Point(137, 469), Point()), itemsType[Left], -1, 1, 0);
-			item->recActions &= ~(UPDATE | SHOWALL);
-			items[Left].push_back(item);
-		}
-	}
-	else
-	{
-		auto updRightSub = [this](EMarketMode marketMode)
-		{
-			if(hLeft)
-				for(const auto & slot : rightTradePanel->slots)
-				{
-					int h1, h2; //hlp variables for getting offer
-					market->getOffer(hLeft->id, slot->id, h1, h2, marketMode);
-
-					rightTradePanel->updateOffer(*slot, h1, h2);
-				}
-			else
-				rightTradePanel->clearSubtitles();
-		};
-
-		auto clickPressedTradePanel = [this](const std::shared_ptr<CTradeableItem> & newSlot, bool left)
-		{
-			CTradeBase::onSlotClickPressed(newSlot, left ? hLeft : hRight);
-			selectionChanged(left);
-		};
-
-		if(Left && mode == EMarketMode::CREATURE_RESOURCE)
-		{
-			CreaturesPanel::slotsData slots;
-			for(auto slotId = SlotID(0); slotId.num < GameConstants::ARMY_SIZE; slotId++)
-			{
-				if(const auto & creature = hero->getCreature(slotId))
-					slots.emplace_back(std::make_tuple(creature->getId(), slotId, hero->getStackCount(slotId)));
-			}
-			leftTradePanel = std::make_shared<CreaturesPanel>(std::bind(clickPressedTradePanel, _1, true), slots);
-			leftTradePanel->moveBy(Point(45, 123));
-			leftTradePanel->deleteSlotsCheck = [this](const std::shared_ptr<CTradeableItem> & slot)
-			{
-				return this->hero->getStackCount(SlotID(slot->serial)) == 0 ? true : false;
-			};
-		}
-		else if(Left && (mode == EMarketMode::RESOURCE_RESOURCE || mode == EMarketMode::RESOURCE_ARTIFACT || mode == EMarketMode::RESOURCE_PLAYER))
-		{
-			leftTradePanel = std::make_shared<ResourcesPanel>(
-				[clickPressedTradePanel](const std::shared_ptr<CTradeableItem> & newSlot)
-				{
-					clickPressedTradePanel(newSlot, true);
-				},
-				[this]()
-				{
-					for(const auto & slot : leftTradePanel->slots)
-						slot->subtitle = std::to_string(LOCPLINT->cb->getResourceAmount(static_cast<EGameResID>(slot->serial)));
-				});
-			leftTradePanel->moveBy(Point(39, 182));
-			leftTradePanel->updateSlots();
-		}
-		else if(!Left && mode == EMarketMode::RESOURCE_RESOURCE)
-		{
-			rightTradePanel = std::make_shared<ResourcesPanel>(
-				[clickPressedTradePanel](const std::shared_ptr<CTradeableItem> & newSlot)
-				{
-					clickPressedTradePanel(newSlot, false);
-				},
-				[this, updRightSub]()
-				{
-					updRightSub(EMarketMode::RESOURCE_RESOURCE);
-					if(hLeft)
-						rightTradePanel->slots[hLeft->serial]->subtitle = CGI->generaltexth->allTexts[164]; // n/a
-				});
-			rightTradePanel->moveBy(Point(327, 181));
-		}
-		else if(!Left && (mode == EMarketMode::ARTIFACT_RESOURCE || mode == EMarketMode::CREATURE_RESOURCE))
-		{
-			rightTradePanel = std::make_shared<ResourcesPanel>(std::bind(clickPressedTradePanel, _1, false),
-				std::bind(updRightSub, EMarketMode::ARTIFACT_RESOURCE));
-			rightTradePanel->moveBy(Point(327, 181));
-		}
-		else if(!Left && mode == EMarketMode::RESOURCE_ARTIFACT)
-		{
-			rightTradePanel = std::make_shared<ArtifactsPanel>(std::bind(clickPressedTradePanel, _1, false),
-				std::bind(updRightSub, EMarketMode::RESOURCE_ARTIFACT), market->availableItemsIds(mode));
-			rightTradePanel->moveBy(Point(327, 181));
-			rightTradePanel->deleteSlotsCheck = [this](const std::shared_ptr<CTradeableItem> & slot)
-			{
-				return vstd::contains(market->availableItemsIds(EMarketMode::RESOURCE_ARTIFACT), ArtifactID(slot->id)) ? false : true;
-			};
-		}
-		else if(!Left && mode == EMarketMode::RESOURCE_PLAYER)
-		{
-			rightTradePanel = std::make_shared<PlayersPanel>(std::bind(clickPressedTradePanel, _1, false));
-			rightTradePanel->moveBy(Point(333, 83));
-		}
-	}
-}
-
-void CTradeWindow::initSubs(bool Left)
-{
-	if(itemsType[Left] == EType::RESOURCE || itemsType[Left] == EType::ARTIFACT_TYPE)
-	{ 
-		if(Left)
-			leftTradePanel->updateSlots();
-		else
-			rightTradePanel->updateSlots();
-		return;
-	}
-}
-
-void CTradeWindow::showAll(Canvas & to)
-{
-	CWindowObject::showAll(to);
-
-	if(readyToTrade)
-	{
-		if(hLeft)
-			hLeft->showAllAt(pos.topLeft() + selectionOffset(true), updateSlotSubtitle(true), to);
-		if(hRight)
-			hRight->showAllAt(pos.topLeft() + selectionOffset(false), updateSlotSubtitle(false), to);
-	}
-}
-
-void CTradeWindow::close()
-{
-	if (onWindowClosed)
-		onWindowClosed();
-
-	CWindowObject::close();
-}
-
-void CTradeWindow::setMode(EMarketMode Mode)
-{
-	const IMarket *m = market;
-	const CGHeroInstance *h = hero;
-	const auto functor = onWindowClosed;
-
-	onWindowClosed = nullptr; // don't call on closing of this window - pass it to next window
-	close();
-
-	switch(Mode)
-	{
-	case EMarketMode::CREATURE_EXP:
-	case EMarketMode::ARTIFACT_EXP:
-		break;
-	default:
-		GH.windows().createAndPushWindow<CMarketplaceWindow>(m, h, functor, Mode);
-		break;
-	}
-}
-
-void CTradeWindow::artifactSelected(CArtPlace * slot)
-{
-	assert(mode == EMarketMode::ARTIFACT_RESOURCE);
-	items[1][0]->setArtInstance(slot->getArt());
-	if(slot->getArt())
-		hLeft = items[1][0];
-	else
-		hLeft = nullptr;
-
-	selectionChanged(true);
-}
-
-ImagePath CMarketplaceWindow::getBackgroundForMode(EMarketMode mode)
-{
-	switch(mode)
-	{
-	case EMarketMode::RESOURCE_RESOURCE:
-		return ImagePath::builtin("TPMRKRES.bmp");
-	case EMarketMode::RESOURCE_PLAYER:
-		return ImagePath::builtin("TPMRKPTS.bmp");
-	case EMarketMode::CREATURE_RESOURCE:
-		return ImagePath::builtin("TPMRKCRS.bmp");
-	case EMarketMode::RESOURCE_ARTIFACT:
-		return ImagePath::builtin("TPMRKABS.bmp");
-	case EMarketMode::ARTIFACT_RESOURCE:
-		return ImagePath::builtin("TPMRKASS.bmp");
-	}
-	assert(0);
-	return {};
-}
-
-CMarketplaceWindow::CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function<void()> & onWindowClosed, EMarketMode Mode)
-	: CTradeWindow(getBackgroundForMode(Mode), Market, Hero, onWindowClosed, Mode)
-{
-	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
-
-	madeTransaction = false;
-	bool sliderNeeded = (mode != EMarketMode::RESOURCE_ARTIFACT && mode != EMarketMode::ARTIFACT_RESOURCE);
-
-	statusBar = CGStatusBar::create(std::make_shared<CPicture>(background->getSurface(), Rect(8, pos.h - 26, pos.w - 16, 19), 8, pos.h - 26));
-
-	std::string title;
-
-	if(auto * o = dynamic_cast<const CGTownInstance *>(market))
-	{
-		switch (mode)
-		{
-		case EMarketMode::CREATURE_RESOURCE:
-			title = (*CGI->townh)[ETownType::STRONGHOLD]->town->buildings[BuildingID::FREELANCERS_GUILD]->getNameTranslated();
-			break;
-		case EMarketMode::RESOURCE_ARTIFACT:
-			title = (*CGI->townh)[o->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated();
-			break;
-		case EMarketMode::ARTIFACT_RESOURCE:
-			title = (*CGI->townh)[o->getFaction()]->town->buildings[BuildingID::ARTIFACT_MERCHANT]->getNameTranslated();
-
-			// create image that copies part of background containing slot MISC_1 into position of slot MISC_5
-			// this is workaround for bug in H3 files where this slot for ragdoll on this screen is missing
-			images.push_back(std::make_shared<CPicture>(background->getSurface(), Rect(20, 187, 47, 47), 18, 339 ));
-			break;
-		default:
-			title = CGI->generaltexth->allTexts[158];
-			break;
-		}
-	}
-	else if(auto * o = dynamic_cast<const CGMarket *>(market))
-	{
-		title = o->title;
-	}
-
-	titleLabel = std::make_shared<CLabel>(300, 27, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, title);
-	if(mode == EMarketMode::ARTIFACT_RESOURCE)
-	{
-		arts = std::make_shared<CArtifactsOfHeroMarket>(Point(-361, 46));
-		arts->selectArtCallback = std::bind(&CTradeWindow::artifactSelected, this, _1);
-		arts->setHero(hero);
-		addSetAndCallbacks(arts);
-	}
-	initItems(false);
-	initItems(true);
-
-	ok = std::make_shared<CButton>(Point(516, 520), AnimationPath::builtin("IOK6432.DEF"), CGI->generaltexth->zelp[600], [&](){ close(); }, EShortcut::GLOBAL_RETURN);
-	deal = std::make_shared<CButton>(Point(307, 520), AnimationPath::builtin("TPMRKB.DEF"), CGI->generaltexth->zelp[595], [&](){ makeDeal(); } );
-	deal->block(true);
-
-	if(sliderNeeded)
-	{
-		slider = std::make_shared<CSlider>(Point(231, 490), 137, std::bind(&CMarketplaceWindow::sliderMoved, this, _1), 0, 0, 0, Orientation::HORIZONTAL);
-		max = std::make_shared<CButton>(Point(229, 520), AnimationPath::builtin("IRCBTNS.DEF"), CGI->generaltexth->zelp[596], [&](){ setMax(); });
-		max->block(true);
-	}
-	else
-	{
-		deal->moveBy(Point(-30, 0));
-	}
-
-	//left side
-	switch(Mode)
-	{
-	case EMarketMode::RESOURCE_RESOURCE:
-	case EMarketMode::RESOURCE_PLAYER:
-	case EMarketMode::RESOURCE_ARTIFACT:
-		labels.push_back(std::make_shared<CLabel>(154, 148, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[270]));
-		break;
-	case EMarketMode::CREATURE_RESOURCE:
-		//%s's Creatures
-		labels.push_back(std::make_shared<CLabel>(152, 102, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->allTexts[272]) % hero->getNameTranslated())));
-		break;
-	case EMarketMode::ARTIFACT_RESOURCE:
-		//%s's Artifacts
-		labels.push_back(std::make_shared<CLabel>(152, 56, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, boost::str(boost::format(CGI->generaltexth->allTexts[271]) % hero->getNameTranslated())));
-		break;
-	}
-
-	Rect traderTextRect;
-
-	//right side
-	switch(Mode)
-	{
-	case EMarketMode::RESOURCE_RESOURCE:
-	case EMarketMode::CREATURE_RESOURCE:
-	case EMarketMode::RESOURCE_ARTIFACT:
-	case EMarketMode::ARTIFACT_RESOURCE:
-		labels.push_back(std::make_shared<CLabel>(445, 148, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[168]));
-		traderTextRect = Rect(316, 48, 260, 75);
-		break;
-	case EMarketMode::RESOURCE_PLAYER:
-		labels.push_back(std::make_shared<CLabel>(445, 55, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, CGI->generaltexth->allTexts[169]));
-		traderTextRect = Rect(28, 48, 260, 75);
-		break;
-	}
-
-	traderText = std::make_shared<CTextBox>("", traderTextRect, 0, FONT_SMALL, ETextAlignment::CENTER);
-	int specialOffset = mode == EMarketMode::ARTIFACT_RESOURCE ? 35 : 0; //in selling artifacts mode we need to move res-res and art-res buttons down
-
-	if(printButtonFor(EMarketMode::RESOURCE_PLAYER))
-		buttons.push_back(std::make_shared<CButton>(Point(18, 520),AnimationPath::builtin("TPMRKBU1.DEF"), CGI->generaltexth->zelp[612], [&](){ setMode(EMarketMode::RESOURCE_PLAYER);}));
-	if(printButtonFor(EMarketMode::RESOURCE_RESOURCE))
-		buttons.push_back(std::make_shared<CButton>(Point(516, 450 + specialOffset),AnimationPath::builtin("TPMRKBU5.DEF"), CGI->generaltexth->zelp[605], [&](){ setMode(EMarketMode::RESOURCE_RESOURCE);}));
-	if(printButtonFor(EMarketMode::CREATURE_RESOURCE))
-		buttons.push_back(std::make_shared<CButton>(Point(516, 485),AnimationPath::builtin("TPMRKBU4.DEF"), CGI->generaltexth->zelp[599], [&](){ setMode(EMarketMode::CREATURE_RESOURCE);}));
-	if(printButtonFor(EMarketMode::RESOURCE_ARTIFACT))
-		buttons.push_back(std::make_shared<CButton>(Point(18, 450 + specialOffset),AnimationPath::builtin("TPMRKBU2.DEF"), CGI->generaltexth->zelp[598], [&](){ setMode(EMarketMode::RESOURCE_ARTIFACT);}));
-	if(printButtonFor(EMarketMode::ARTIFACT_RESOURCE))
-		buttons.push_back(std::make_shared<CButton>(Point(18, 485),AnimationPath::builtin("TPMRKBU3.DEF"), CGI->generaltexth->zelp[613], [&](){ setMode(EMarketMode::ARTIFACT_RESOURCE);}));
-
-	updateTraderText();
-}
-
-CMarketplaceWindow::~CMarketplaceWindow() = default;
-
-void CMarketplaceWindow::setMax()
-{
-	slider->scrollToMax();
-}
-
-void CMarketplaceWindow::makeDeal()
-{
-	int sliderValue = 0;
-	if(slider)
-		sliderValue = slider->getValue();
-	else
-		sliderValue = !deal->isBlocked(); //should always be 1
-
-	if(!sliderValue)
-		return;
-
-	bool allowDeal = true;
-	int leftIdToSend = hLeft->id;
-	switch (mode)
-	{
-		case EMarketMode::CREATURE_RESOURCE:
-			leftIdToSend = hLeft->serial;
-			break;
-		case EMarketMode::ARTIFACT_RESOURCE:
-			leftIdToSend = hLeft->getArtInstance()->getId().getNum();
-			break;
-		case EMarketMode::RESOURCE_ARTIFACT:
-			if(!ArtifactID(hRight->id).toArtifact()->canBePutAt(hero))
-			{
-				LOCPLINT->showInfoDialog(CGI->generaltexth->translate("core.genrltxt.326"));
-				allowDeal = false;
-			}
-			break;
-		default:
-			break;
-	}
-
-	if(allowDeal)
-	{
-		switch(mode)
-		{
-		case EMarketMode::RESOURCE_RESOURCE:
-			LOCPLINT->cb->trade(market, mode, GameResID(leftIdToSend), GameResID(hRight->id), slider->getValue() * r1, hero);
-			slider->scrollTo(0);
-			break;
-		case EMarketMode::CREATURE_RESOURCE:
-			LOCPLINT->cb->trade(market, mode, SlotID(leftIdToSend), GameResID(hRight->id), slider->getValue() * r1, hero);
-			slider->scrollTo(0);
-			break;
-		case EMarketMode::RESOURCE_PLAYER:
-			LOCPLINT->cb->trade(market, mode, GameResID(leftIdToSend), PlayerColor(hRight->id), slider->getValue() * r1, hero);
-			slider->scrollTo(0);
-			break;
-
-		case EMarketMode::RESOURCE_ARTIFACT:
-			LOCPLINT->cb->trade(market, mode, GameResID(leftIdToSend), ArtifactID(hRight->id), r2, hero);
-			break;
-		case EMarketMode::ARTIFACT_RESOURCE:
-			LOCPLINT->cb->trade(market, mode, ArtifactInstanceID(leftIdToSend), GameResID(hRight->id), r2, hero);
-			break;
-		}
-	}
-
-	madeTransaction = true;
-	hLeft = nullptr;
-	hRight = nullptr;
-	if(leftTradePanel)
-		leftTradePanel->deselect();
-	assert(rightTradePanel);
-	rightTradePanel->deselect();
-	selectionChanged(true);
-}
-
-void CMarketplaceWindow::sliderMoved( int to )
-{
-	redraw();
-}
-
-void CMarketplaceWindow::selectionChanged(bool side)
-{
-	readyToTrade = hLeft && hRight;
-	if(mode == EMarketMode::RESOURCE_RESOURCE)
-		readyToTrade = readyToTrade && (hLeft->id != hRight->id); //for resource trade, two DIFFERENT resources must be selected
-
-	if(mode == EMarketMode::ARTIFACT_RESOURCE && !hLeft)
-		arts->unmarkSlots();
-
-	if(readyToTrade)
-	{
-		int soldItemId = hLeft->id;
-		market->getOffer(soldItemId, hRight->id, r1, r2, mode);
-
-		if(slider)
-		{
-			int newAmount = -1;
-			if(itemsType[1] == EType::RESOURCE)
-				newAmount = LOCPLINT->cb->getResourceAmount(static_cast<EGameResID>(soldItemId));
-			else if(itemsType[1] == EType::CREATURE)
-				newAmount = hero->getStackCount(SlotID(hLeft->serial)) - (hero->stacksCount() == 1  &&  hero->needsLastStack());
-			else
-				assert(0);
-
-			slider->setAmount(newAmount / r1);
-			slider->scrollTo(0);
-			max->block(false);
-			deal->block(false);
-		}
-		else if(itemsType[1] == EType::RESOURCE) //buying -> check if we can afford transaction
-		{
-			deal->block(LOCPLINT->cb->getResourceAmount(static_cast<EGameResID>(soldItemId)) < r1);
-		}
-		else
-			deal->block(false);
-	}
-	else
-	{
-		if(slider)
-		{
-			max->block(true);
-			slider->setAmount(0);
-			slider->scrollTo(0);
-		}
-		deal->block(true);
-	}
-
-	if(side && itemsType[0] != EType::PLAYER) //items[1] selection changed, recalculate offers
-		initSubs(false);
-
-	updateTraderText();
-	redraw();
-}
-
-bool CMarketplaceWindow::printButtonFor(EMarketMode M) const
-{
-	if (!market->allowsTrade(M))
-		return false;
-
-	if (M == mode)
-		return false;
-
-	if ( M == EMarketMode::RESOURCE_RESOURCE || M == EMarketMode::RESOURCE_PLAYER)
-	{
-		auto * town = dynamic_cast<const CGTownInstance *>(market);
-
-		if (town)
-			return town->getOwner() == LOCPLINT->playerID;
-		else
-			return true;
-	}
-	else
-	{
-		return hero != nullptr;
-	}
-}
-
-void CMarketplaceWindow::updateGarrison()
-{
-	if(mode != EMarketMode::CREATURE_RESOURCE)
-		return;
-
-	leftTradePanel->deleteSlots();
-	leftTradePanel->updateSlots();
-}
-
-void CMarketplaceWindow::artifactsChanged(bool Left)
-{
-	assert(!Left);
-	if(mode != EMarketMode::RESOURCE_ARTIFACT)
-		return;
-	
-	rightTradePanel->deleteSlots();
-	redraw();
-}
-
-std::string CMarketplaceWindow::updateSlotSubtitle(bool Left) const
-{
-	if(Left)
-	{
-		switch(itemsType[1])
-		{
-		case EType::RESOURCE:
-		case EType::CREATURE:
-			{
-				int val = slider
-					? slider->getValue() * r1
-					: (((deal->isBlocked())) ? 0 : r1);
-
-				return std::to_string(val);
-			}
-		case EType::ARTIFACT_INSTANCE:
-			return ((deal->isBlocked()) ? "0" : "1");
-		}
-	}
-	else
-	{
-		switch(itemsType[0])
-		{
-		case EType::RESOURCE:
-			if(slider)
-				return std::to_string( slider->getValue() * r2 );
-			else
-				return std::to_string(r2);
-		case EType::ARTIFACT_TYPE:
-			return ((deal->isBlocked()) ? "0" : "1");
-		case EType::PLAYER:
-			return (hRight ? CGI->generaltexth->capColors[hRight->id] : "");
-		}
-	}
-
-	return "???";
-}
-
-Point CMarketplaceWindow::selectionOffset(bool Left) const
-{
-	if(Left)
-	{
-		switch(itemsType[1])
-		{
-		case EType::RESOURCE:
-			return Point(122, 448);
-		case EType::CREATURE:
-			return Point(128, 450);
-		case EType::ARTIFACT_INSTANCE:
-			return Point(134, 469);
-		}
-	}
-	else
-	{
-		switch(itemsType[0])
-		{
-		case EType::RESOURCE:
-			if(mode == EMarketMode::ARTIFACT_RESOURCE)
-				return Point(410, 471);
-			else
-				return Point(410, 448);
-		case EType::ARTIFACT_TYPE:
-			return Point(411, 449);
-		case EType::PLAYER:
-			return Point(417, 451);
-		}
-	}
-
-	assert(0);
-	return Point(0,0);
-}
-
-void CMarketplaceWindow::resourceChanged()
-{
-	initSubs(true);
-}
-
-void CMarketplaceWindow::updateTraderText()
-{
-	if(readyToTrade)
-	{
-		if(mode == EMarketMode::RESOURCE_PLAYER)
-		{
-			//I can give %s to the %s player.
-			traderText->setText(boost::str(boost::format(CGI->generaltexth->allTexts[165]) % hLeft->getName() % hRight->getName()));
-		}
-		else if(mode == EMarketMode::RESOURCE_ARTIFACT)
-		{
-			//I can offer you the %s for %d %s of %s.
-			traderText->setText(boost::str(boost::format(CGI->generaltexth->allTexts[267]) % hRight->getName() % r1 % CGI->generaltexth->allTexts[160 + (r1==1)] % hLeft->getName()));
-		}
-		else if(mode == EMarketMode::RESOURCE_RESOURCE)
-		{
-			//I can offer you %d %s of %s for %d %s of %s.
-			traderText->setText(boost::str(boost::format(CGI->generaltexth->allTexts[157]) % r2 % CGI->generaltexth->allTexts[160 + (r2==1)] % hRight->getName() % r1 % CGI->generaltexth->allTexts[160 + (r1==1)] % hLeft->getName()));
-		}
-		else if(mode == EMarketMode::CREATURE_RESOURCE)
-		{
-			//I can offer you %d %s of %s for %d %s.
-			traderText->setText(boost::str(boost::format(CGI->generaltexth->allTexts[269]) % r2 % CGI->generaltexth->allTexts[160 + (r2==1)] % hRight->getName() % r1 % hLeft->getName(r1)));
-		}
-		else if(mode == EMarketMode::ARTIFACT_RESOURCE)
-		{
-			//I can offer you %d %s of %s for your %s.
-			traderText->setText(boost::str(boost::format(CGI->generaltexth->allTexts[268]) % r2 % CGI->generaltexth->allTexts[160 + (r2==1)] % hRight->getName() % hLeft->getName(r1)));
-		}
-		return;
-	}
-
-	int gnrtxtnr = -1;
-	if(madeTransaction)
-	{
-		if(mode == EMarketMode::RESOURCE_PLAYER)
-			gnrtxtnr = 166; //Are there any other resources you'd like to give away?
-		else
-			gnrtxtnr = 162; //You have received quite a bargain.  I expect to make no profit on the deal.  Can I interest you in any of my other wares?
-	}
-	else
-	{
-		if(mode == EMarketMode::RESOURCE_PLAYER)
-			gnrtxtnr = 167; //If you'd like to give any of your resources to another player, click on the item you wish to give and to whom.
-		else
-			gnrtxtnr = 163; //Please inspect our fine wares.  If you feel like offering a trade, click on the items you wish to trade with and for.
-	}
-	traderText->setText(CGI->generaltexth->allTexts[gnrtxtnr]);
-}

+ 0 - 80
client/windows/CTradeWindow.h

@@ -1,80 +0,0 @@
-/*
- * CTradeWindow.h, part of VCMI engine
- *
- * Authors: listed in file AUTHORS in main folder
- *
- * License: GNU General Public License v2.0 or later
- * Full text of license available in license.txt file, in main folder
- *
- */
-#pragma once
-
-#include "../widgets/markets/CTradeBase.h"
-#include "../widgets/CWindowWithArtifacts.h"
-#include "CWindowObject.h"
-
-class CSlider;
-class CGStatusBar;
-
-class CTradeWindow : public CTradeBase, public CWindowObject, public CWindowWithArtifacts //base for markets and altar of sacrifice
-{
-public:
-	EType itemsType[2];
-
-	EMarketMode mode;
-	std::shared_ptr<CButton> ok;
-	std::shared_ptr<CButton> max;
-
-	std::shared_ptr<CSlider> slider; //for choosing amount to be exchanged
-	bool readyToTrade;
-
-	CTradeWindow(const ImagePath & bgName, const IMarket * Market, const CGHeroInstance * Hero, const std::function<void()> & onWindowClosed, EMarketMode Mode); //c
-
-	void showAll(Canvas & to) override;
-	void close() override;
-
-	void initSubs(bool Left);
-	void initTypes();
-	void initItems(bool Left);
-	void setMode(EMarketMode Mode); //mode setter
-
-	void artifactSelected(CArtPlace * slot); //used when selling artifacts -> called when user clicked on artifact slot
-	virtual void selectionChanged(bool side) = 0; //true == left
-	virtual Point selectionOffset(bool Left) const = 0;
-	virtual std::string updateSlotSubtitle(bool Left) const = 0;
-	virtual void updateGarrison() = 0;
-	virtual void artifactsChanged(bool left) = 0;
-protected:
-	std::function<void()> onWindowClosed;
-	std::shared_ptr<CGStatusBar> statusBar;
-	std::vector<std::shared_ptr<CPicture>> images;
-};
-
-class CMarketplaceWindow : public CTradeWindow
-{
-	std::shared_ptr<CLabel> titleLabel;
-	std::shared_ptr<CArtifactsOfHeroMarket> arts;
-
-	bool printButtonFor(EMarketMode M) const;
-
-	ImagePath getBackgroundForMode(EMarketMode mode);
-public:
-	int r1, r2; //suggested amounts of traded resources
-	bool madeTransaction; //if player made at least one transaction
-	std::shared_ptr<CTextBox> traderText;
-
-	void setMax();
-	void sliderMoved(int to);
-	void makeDeal() override;
-	void selectionChanged(bool side) override; //true == left
-	CMarketplaceWindow(const IMarket * Market, const CGHeroInstance * Hero, const std::function<void()> & onWindowClosed, EMarketMode Mode);
-	~CMarketplaceWindow();
-
-	Point selectionOffset(bool Left) const override;
-	std::string updateSlotSubtitle(bool Left) const override;
-
-	void updateGarrison() override; //removes creatures with count 0 from the list (apparently whole stack has been sold)
-	void artifactsChanged(bool left) override;
-	void resourceChanged();
-	void updateTraderText();
-};

+ 4 - 3
client/windows/InfoWindows.cpp

@@ -54,10 +54,7 @@ CSelWindow::CSelWindow( const std::string & Text, PlayerColor player, int charpe
 	text = std::make_shared<CTextBox>(Text, Rect(0, 0, 250, 100), 0, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
 
 	if(buttons.size() > 1 && askID.getNum() >= 0) //cancel button functionality
-	{
 		buttons.back()->addCallback([askID](){LOCPLINT->cb->selectionMade(0, askID);});
-		//buttons.back()->addCallback(std::bind(&CCallback::selectionMade, LOCPLINT->cb.get(), 0, askID));
-	}
 
 	if(buttons.size() == 1)
 		buttons.front()->assignedKey = EShortcut::GLOBAL_RETURN;
@@ -69,7 +66,11 @@ CSelWindow::CSelWindow( const std::string & Text, PlayerColor player, int charpe
 	}
 
 	if(!comps.empty())
+	{
 		components = std::make_shared<CComponentBox>(comps, Rect(0,0,0,0));
+		for (auto & comp : comps)
+			comp->onChoose = [this](){ madeChoiceAndClose(); };
+	}
 
 	CMessage::drawIWindow(this, Text, player);
 }

+ 7 - 7
config/battlefields.json

@@ -79,14 +79,14 @@
 				"type" : "MORALE",
 				"val" : 1,
 				"valueType" : "BASE_NUMBER",
-				"description" : "Creatures of good town alignment on Holly Ground",
+				"description" : "core.arraytxt.123",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["good"] }]
 			},
 			{
 				"type" : "MORALE",
 				"val" : -1,
 				"valueType" : "BASE_NUMBER",
-				"description" : "Creatures of evil town alignment on Holly Ground",
+				"description" : "core.arraytxt.124",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["evil"] }]
 			}
 		]
@@ -99,7 +99,7 @@
 				"type" : "LUCK",
 				"val" : 2,
 				"valueType" : "BASE_NUMBER",
-				"description" : "Creatures of neutral town alignment on Clover Field",
+				"description" : "core.arraytxt.83",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["neutral"] }]
 			}
 		]
@@ -112,14 +112,14 @@
 				"type" : "MORALE",
 				"val" : -1,
 				"valueType" : "BASE_NUMBER",
-				"description" : "Creatures of good town alignment on Evil Fog",
+				"description" : "core.arraytxt.126",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["good"] }]
 			},
 			{
 				"type" : "MORALE",
 				"val" : 1,
 				"valueType" : "BASE_NUMBER",
-				"description" : "Creatures of evil town alignment on Evil Fog",
+				"description" : "core.arraytxt.125",
 				"limiters": [{ "type" : "CREATURE_ALIGNMENT_LIMITER", "parameters" : ["evil"] }]
 			}
 		]
@@ -132,13 +132,13 @@
 				"type" : "NO_MORALE",
 				"val" : 0,
 				"valueType" : "INDEPENDENT_MIN",
-				"description" : "Creatures on Cursed Ground"
+				"description" : "core.arraytxt.112"
 			},
 			{
 				"type" : "NO_LUCK",
 				"val" : 0,
 				"valueType" : "INDEPENDENT_MIN",
-				"description" : "Creatures on Cursed Ground"
+				"description" : "core.arraytxt.81"
 			},
 			{
 				"type" : "BLOCK_MAGIC_ABOVE",

+ 689 - 0
config/biomes.json

@@ -0,0 +1,689 @@
+{
+	"templateSet2":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "crater"
+		},
+		"templates" : ["AVLct1d0", "AVLct2d0", "AVLct3d0", "AVLct4d0", "AVLct5d0", "AVLctrd0"]
+	},
+	"dirtRedFlowers":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLfl1d0", "AVLfl6d0", "AVLfl7d0"]
+	},
+	"dirtLightFlowers":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLfl4d0", "AVLfl5d0"]
+	},
+	"dirtYellowFlowers":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLfl3d0", "AVLfl8d0"]
+	},
+	"dirtPurpleFlowers":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLfl2d0", "AVLfl9d0"]
+	},
+	"templateSet5":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "lake"
+		},
+		"templates" : ["AVLlk1d0", "AVLlk2d0", "AVLlk3d0"]
+	},
+	"templateSet7":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "rock"
+		},
+		"templates" : ["AVLmd1d0", "AVLmd2d0"]
+	},
+	"templateSet8":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "mountain"
+		},
+		"templates" : ["avlmtdr1", "avlmtdr2", "avlmtdr3", "avlmtdr4", "avlmtdr5", "avlmtdr6", "avlmtdr7", "avlmtdr8"]
+	},
+	"templateSet9":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "tree"
+		},
+		"templates" : ["avlautr0", "avlautr1", "AVLAUTR2", "AVLAUTR3", "AVLAUTR4", "AVLAUTR5", "AVLautr6", "AVLautr7"]
+	},
+	"templateSet10":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "rock"
+		},
+		"templates" : ["AVLoc1d0", "AVLoc2d0", "AVLoc3d0"]
+	},
+	"templateSet11":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLPNTR0", "AVLPNTR1", "AVLPNTR2", "AVLPNTR3", "AVLPNTR4", "AVLPNTR5", "AVLpntr6", "AVLpntr7"]
+	},
+	"templateSet13":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "rock"
+		},
+		"templates" : ["AvLRD01", "AvLRD02", "AvLRD04", "AVLrk3d0", "AVLrk5d0"]
+	},
+	"templateSet14":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLsh1d0", "AVLsh2d0", "AVLsh3d0", "AVLsh4d0", "AVLsh5d0", "AVLsh6d0", "AVLsh7d0", "AVLsh8d0"]
+	},
+	"dirtStumps":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "plant"
+		},
+		"templates" : ["AvLdlog", "AvLStm1", "AvLStm2", "AvLStm3"]
+	},
+	"templateSet16":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLtr1d0", "AVLtr2d0", "AVLtr3d0"]
+	},
+	"templateSet17":{
+		"biome":{
+			"terrain" : "dirt",
+			"objectType" : "other"
+		},
+		"templates" : ["avlxdt00", "avlxdt01", "avlxdt02", "avlxdt03", "avlxdt04", "avlxdt05", "avlxdt06", "avlxdt07", "avlxdt08", "avlxdt09", "avlxdt10", "avlxdt11"]
+	},
+	"cactus":{
+		"biome":{
+			"terrain" : "sand",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLca010", "AVLca020", "AVLca030", "AVLca040", "AVLca050", "AVLca060", "AVLca070", "AVLca080", "AVLca090", "AVLca100", "AVLca110", "AVLca120", "AVLca130"]
+	},
+	"sandCraters":{
+		"biome":{
+			"terrain" : "sand",
+			"objectType" : "crater"
+		},
+		"templates" : ["AVLctds0", "AVLspit0"]
+	},
+	"templateSet32":{
+		"biome":{
+			"terrain" : "sand",
+			"objectType" : "mountain"
+		},
+		"templates" : ["AVLmtds1", "AVLmtds2", "AVLmtds3", "AVLmtds4", "AVLmtds5", "AVLmtds6"]
+	},
+	"templateSet34":{
+		"biome":{
+			"terrain" : "sand",
+			"objectType" : "rock"
+		},
+		"templates" : ["AVLdun10", "AVLdun20", "AVLdun30"]
+	},
+	"templateSet36":{
+		"biome":{
+			"terrain" : "sand",
+			"objectType" : "animal"
+		},
+		"templates" : ["AVLskul0"]
+	},
+	"sandPalms":{
+		"biome":{
+			"terrain" : "sand",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLplm10", "AVLplm20", "AVLplm30", "AVLplm40", "AVLplm50"]
+	},
+	"sandYucca":{
+		"biome":{
+			"terrain" : "sand",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLyuc10", "AVLyuc20", "AVLyuc30"]
+	},
+	"templateSet38":{
+		"biome":{
+			"terrain" : "sand",
+			"objectType" : "other"
+		},
+		"templates" : ["avlxds01", "avlxds02", "avlxds03", "avlxds04", "avlxds05", "avlxds06", "avlxds07", "avlxds08", "avlxds09", "avlxds10", "avlxds11", "avlxds12"]
+	},
+	"templateSet50":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "crater"
+		},
+		"templates" : ["AVLct1g0", "AVLct2g0", "AVLct3g0", "AVLct4g0", "AVLct5g0", "AVLct6g0", "AVLctrg0"]
+	},
+	"grassRedFlowers":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLf01g0", "AVLf02g0", "AVLf07g0"]
+	},
+	"grassPurpleFlowers":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLf03g0", "AVLf08g0", "AVLf12g0"]
+	},
+	"grassYellowFlowers":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLf04g0", "AVLf05g0", "AVLf09g0", "AVLf10g0", "AVLf11g0"]
+	},
+	"grassWhiteFlowers":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLf06g0"]
+	},
+	"templateSet53":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "lake"
+		},
+		"templates" : ["AVLlk1g0", "AVLlk2g0", "AVLlk3g0"]
+	},
+	"templateSet54":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "plant"
+		},
+		"templates" : ["AvLdlog"]
+	},
+	"templateSet55":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "rock"
+		},
+		"templates" : ["AVLmd1g0", "AVLmd2g0"]
+	},
+	"greyMountains":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "mountain"
+		},
+		"templates" : ["AVLmtgn0", "AVLmtgn1", "AVLmtgn2", "AVLmtgn3", "AVLmtgn4", "AVLmtgn5"]
+	},
+	"brownMountains":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "mountain"
+		},
+		"templates" : ["AVLmtgr1", "AVLmtgr2", "AVLmtgr3", "AVLmtgr4", "AVLmtgr5", "AVLmtgr6"]
+	},
+	"greenOakTrees":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLSPTR0", "AVLSPTR1", "AVLSPTR2", "AVLSPTR3", "AVLSPTR4", "AVLSPTR5", "AVLSPTR6", "AVLsptr7", "AVLsptr8"]
+	},
+	"autumnOakTrees":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "tree"
+		},
+		"templates" : ["avlautr0", "avlautr1", "AVLAUTR2", "AVLAUTR3", "AVLAUTR4", "AVLAUTR5", "AVLautr6", "AVLautr7"]
+	},
+	"templateSet58":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "rock"
+		},
+		"templates" : ["AVLoc1g0", "AVLoc2g0", "AVLoc3g0"]
+	},
+	"templateSet59":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLPNTR0", "AVLPNTR1", "AVLPNTR2", "AVLPNTR3", "AVLPNTR4", "AVLPNTR5", "AVLpntr6", "AVLpntr7"]
+	},
+	"templateSet61":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "rock"
+		},
+		"templates" : ["AvLRG01", "AvLRG02", "AvLRG03", "AvLRG04", "AvLRG05", "AvLRG06", "AvLRG07", "AvLRG08", "AvLRG09", "AvLRG10", "AvLRG11"]
+	},
+	"templateSet62":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLsh1g0", "AVLsh2g0", "AVLsh3g0", "AVLsh4g0", "AVLsh5g0", "AVLsh6g0"]
+	},
+	"templateSet63":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "plant"
+		},
+		"templates" : ["AvLStm1", "AvLStm2", "AvLStm3"]
+	},
+	"swampTreesOnGrass":{
+		"biome":{
+			"terrain" : "grass",
+			"faction" : "fortress",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLswmp0", "AVLswmp1", "AVLswmp2", "AVLswmp3", "AVLswmp4", "AVLswmp5", "AVLswmp6", "AVLswmp7", "AVLtr1d0", "AVLtr2d0", "AVLtr3d0", "AVLwlw10", "AVLwlw20", "AVLwlw30"]
+	},
+	"templateSet65":{
+		"biome":{
+			"terrain" : "grass",
+			"objectType" : "other"
+		},
+		"templates" : ["avlxgr01", "avlxgr02", "avlxgr03", "avlxgr04", "avlxgr05", "avlxgr06", "avlxgr07", "avlxgr08", "avlxgr09", "avlxgr10", "avlxgr11", "avlxgr12"]
+	},
+	"templateSet77":{
+		"biome":{
+			"terrain" : "snow",
+			"objectType" : "crater"
+		},
+		"templates" : ["AVLctsn0"]
+	},
+	"templateSet78":{
+		"biome":{
+			"terrain" : "snow",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLd1sn0", "AVLd2sn0", "AVLd3sn0", "AVLd4sn0", "AVLd5sn0", "AVLd6sn0", "AVLd7sn0", "AVLd8sn0", "AVLd9sn0", "AVLddsn0", "AVLddsn1", "AVLddsn2", "AVLddsn3", "AVLddsn4", "AVLddsn5", "AVLddsn6", "AVLddsn7"]
+	},
+	"templateSet79":{
+		"biome":{
+			"terrain" : "snow",
+			"objectType" : "lake"
+		},
+		"templates" : ["AVLflk10", "AVLflk20", "AVLflk30"]
+	},
+	"templateSet81":{
+		"biome":{
+			"terrain" : "snow",
+			"objectType" : "mountain"
+		},
+		"templates" : ["AVLmtsn1", "AVLmtsn2", "AVLmtsn3", "AVLmtsn4", "AVLmtsn5", "AVLmtsn6"]
+	},
+	"templateSet82":{
+		"biome":{
+			"terrain" : "snow",
+			"objectType" : "rock"
+		},
+		"templates" : ["AVLo1sn0", "AVLo2sn0", "AVLo3sn0"]
+	},
+	"templateSet83":{
+		"biome":{
+			"terrain" : "snow",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLSNTR0", "AVLSNTR1", "AVLSNTR2", "AVLSNTR3", "AVLSNTR4", "AVLSNTR5", "AVLsntr6", "AVLsntr7"]
+	},
+	"templateSet85":{
+		"biome":{
+			"terrain" : "snow",
+			"objectType" : "rock"
+		},
+		"templates" : ["AVLr1sn0", "AVLr2sn0", "AVLr3sn0", "AVLr4sn0", "AVLr5sn0", "AVLr6sn0", "AVLr7sn0", "AVLr8sn0"]
+	},
+	"templateSet86":{
+		"biome":{
+			"terrain" : "snow",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLs1sn0", "AVLs2sn0", "AVLs3sn0"]
+	},
+	"templateSet87":{
+		"biome":{
+			"terrain" : "snow",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLp1sn0", "AVLp2sn0"]
+	},
+	"templateSet99":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "crater"
+		},
+		"templates" : ["AVLctrs0"]
+	},
+	"templateSet100":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLdead0", "AVLdead1", "AVLdead2", "AVLdead3", "AVLdead4", "AVLdead5", "AVLdead6", "AVLdead7", "AVLdt1s0", "AVLdt2s0", "AVLdt3s0", "AVLswp60", "AVLswp70"]
+	},
+	"templateSet102":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "lake"
+		},
+		"templates" : ["AVLlk1s0", "AVLlk2s0", "AVLlk3s0", "AVLswp50"]
+	},
+	"templateSet103":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLman10", "AVLman20", "AVLman30", "AVLman40", "AVLman50"]
+	},
+	"templateSet104":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLmoss0"]
+	},
+	"templateSet105":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "mountain"
+		},
+		"templates" : ["AVLmtsw1", "AVLmtsw2", "AVLmtsw3", "AVLmtsw4", "AVLmtsw5", "AVLmtsw6"]
+	},
+	"swampTrees":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLSPTR0", "AVLSPTR1", "AVLSPTR2", "AVLSPTR3", "AVLSPTR4", "AVLSPTR5", "AVLSPTR6", "AVLsptr7", "AVLsptr8"]
+	},
+	"templateSet108":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "rock"
+		},
+		"templates" : ["AVLrk1s0", "AVLrk2s0", "AVLrk3s0", "AVLrk4s0"]
+	},
+	"templateSet109":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLs01s0", "AVLs02s0", "AVLs03s0", "AVLs04s0", "AVLs05s0", "AVLs06s0", "AVLs07s0", "AVLs08s0", "AVLs09s0", "AVLs10s0", "AVLs11s0", "AVLswp10", "AVLswp20", "AVLswp30", "AVLswp40"]
+	},
+	"floodedPalms":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLswmp0", "AVLswmp1", "AVLswmp2", "AVLswmp3", "AVLswmp4", "AVLswmp5", "AVLswmp6", "AVLswmp7"]
+	},
+	"swampTrees2":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLtr1d0", "AVLtr2d0", "AVLtr3d0", "AVLwlw10", "AVLwlw20", "AVLwlw30"]
+	},
+	"swampPalms":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "tree"
+		},
+		"templates" : ["avlswtr0", "avlswtr1", "avlswtr2", "avlswtr3", "avlswtr4", "avlswtr5", "avlswtr6", "avlswtr7", "avlswtr8", "avlswtr9"]
+	},
+	"swampSinglePalms":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "plant"
+		},
+		"templates" : ["avlswt00", "avlswt01", "avlswt02", "avlswt03", "avlswt04", "avlswt05", "avlswt06", "avlswt07", "avlswt08", "avlswt09", "avlswt10", "avlswt11", "avlswt12", "avlswt13", "avlswt14", "avlswt15", "avlswt16", "avlswt17", "avlswt18", "avlswt19"]
+	},
+	"templateSet112":{
+		"biome":{
+			"terrain" : "swamp",
+			"objectType" : "other"
+		},
+		"templates" : ["avlxsw01", "avlxsw02", "avlxsw03", "avlxsw04", "avlxsw05", "avlxsw06", "avlxsw07", "avlxsw08", "avlxsw09", "avlxsw10", "avlxsw11"]
+	},
+	"templateSet124":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLca1r0", "AVLca2r0"]
+	},
+	"roughCraters":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "crater"
+		},
+		"templates" : ["AVLglly0", "AVLct1r0", "AVLct2r0", "AVLct3r0", "AVLct4r0", "AVLct5r0", "AVLct6r0", "AVLct7r0", "AVLct8r0", "AVLct9r0", "AVLctrr0"]
+	},
+	"templateSet129":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "rock"
+		},
+		"templates" : ["AVLmd1r0", "AVLmd2r0", "AVLmd3r0"]
+	},
+	"templateSet130":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "mountain"
+		},
+		"templates" : ["avlmtrf1", "avlmtrf2", "avlmtrf3", "avlmtrf4", "avlmtrf5", "avlmtrf6"]
+	},
+	"templateSet131":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "rock"
+		},
+		"templates" : ["AVLoc1r0", "AVLoc2r0", "AVLoc3r0", "AVLoc4r0"]
+	},
+	"templateSet133":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "rock"
+		},
+		"templates" : ["avlbuzr0", "AVLr02r0", "AVLr03r0", "AVLr04r0", "AVLr06r0", "AVLr07r0", "AVLr08r0", "AVLr09r0", "AVLr10r0", "AVLr11r0", "AVLr12r0", "AVLr13r0", "AVLr14r0", "AVLr15r0", "AvLRR01", "AvLRR05"]
+	},
+	"templateSet134":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLsh1r0", "AVLsh2r0", "AVLsh3r0", "avlsh4r0", "avlsh5r0", "avlsh6r0", "avlsh7r0", "avlsh8r0", "avlsh9r0"]
+	},
+	"templateSet135":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "animal"
+		},
+		"templates" : ["AVLskul0"]
+	},
+	"roughStumps":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "plant"
+		},
+		"templates" : ["AvLdlog", "AvLStm1", "AvLStm2", "AvLStm3"]
+	},
+	"roughSmallTree":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLroug0", "AVLroug1", "AVLroug2"]
+	},
+	"roughYucca":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLyuc10", "AVLyuc20", "AVLyuc30"]
+	},
+	"roughLake":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "lake"
+		},
+		"templates" : ["AVLlk1r"]
+	},
+	"templateSet139":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "other"
+		},
+		"templates" : ["AVLtRo00", "AVLtRo01", "AVLtRo02", "AVLtRo03", "AVLtRo04", "AVLtRo05", "AVLtRo06", "AVLtRo07", "AVLtRo08", "AVLtRo09", "AVLtRo10", "AVLtRo11", "AVLtRo12", "AVLtrRo0", "AVLtrRo1", "AVLtrRo2", "AVLtrRo3", "AVLtrRo4", "AVLtrRo5", "AVLtrRo6", "AVLtrRo7"]
+	},
+	"templateSet140":{
+		"biome":{
+			"terrain" : "rough",
+			"objectType" : "other"
+		},
+		"templates" : ["avlxro01", "avlxro02", "avlxro03", "avlxro04", "avlxro05", "avlxro06", "avlxro07", "avlxro08", "avlxro09", "avlxro10", "avlxro11", "avlxro12"]
+	},
+	"templateSet152":{
+		"biome":{
+			"terrain" : "subterra",
+			"objectType" : "crater"
+		},
+		"templates" : ["AVLct1u0", "AVLct2u0", "AVLct3u0", "AVLct4u0", "AVLct5u0"]
+	},
+	"templateSet153":{
+		"biome":{
+			"terrain" : "subterra",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLdead0", "AVLdead1", "AVLdead2", "AVLdead3", "AVLdead4", "AVLdead5", "AVLdead6", "AVLdead7"]
+	},
+	"templateSet155":{
+		"biome":{
+			"terrain" : "subterra",
+			"objectType" : "lake"
+		},
+		"templates" : ["AVLlk1u0", "AVLlk2u0", "AVLlk3u0"]
+	},
+	"templateSet156":{
+		"biome":{
+			"terrain" : "subterra",
+			"objectType" : "lake"
+		},
+		"templates" : ["AVLlv1u0", "AVLlv2u0", "AVLlv3u0"]
+	},
+	"templateSet157":{
+		"biome":{
+			"terrain" : "subterra",
+			"objectType" : "lake"
+		},
+		"templates" : ["AVLllk10", "AVLllk20"]
+	},
+	"templateSet158":{
+		"biome":{
+			"terrain" : "subterra",
+			"objectType" : "plant"
+		},
+		"templates" : ["AVLms010", "AVLms020", "AVLms030", "AVLms040", "AVLms050", "AVLms060", "AVLms070", "AVLms080", "AVLms090", "AVLms100", "AVLms110", "AVLms120"]
+	},
+	"templateSet159":{
+		"biome":{
+			"terrain" : "subterra",
+			"objectType" : "mountain"
+		},
+		"templates" : ["AVLmtsb0", "AVLmtsb1", "AVLmtsb2", "AVLmtsb3", "AVLmtsb4", "AVLmtsb5"]
+	},
+	"templateSet160":{
+		"biome":{
+			"terrain" : "subterra",
+			"objectType" : "rock"
+		},
+		"templates" : ["AVLoc1u0", "AVLoc2u0", "AVLoc3u0", "AVLoc4u0"]
+	},
+	"templateSet162":{
+		"biome":{
+			"terrain" : "subterra",
+			"objectType" : "rock"
+		},
+		"templates" : ["AVLr01u0", "AVLr02u0", "AVLr03u0", "AVLr04u0", "AVLr05u0", "AVLr06u0", "AVLr07u0", "AVLr08u0", "AVLr09u0", "AVLr10u0", "AVLr11u0", "AVLr12u0", "AVLr13u0", "AVLr14u0", "AVLr15u0", "AVLr16u0", "AVLstg10", "AVLstg20", "AVLstg30", "AVLstg40", "AVLstg50", "AVLstg60"]
+	},
+	"templateSet163":{
+		"biome":{
+			"terrain" : "subterra",
+			"objectType" : "other"
+		},
+		"templates" : ["avlxsu01", "avlxsu02", "avlxsu03", "avlxsu04", "avlxsu05", "avlxsu06", "avlxsu07", "avlxsu08", "avlxsu09", "avlxsu10", "avlxsu11", "avlxsu12"]
+	},
+	"lavaChasm":{
+		"biome":{
+			"terrain" : "lava",
+			"objectType" : "crater"
+		},
+		"templates" : ["AVLc10l0", "AVLc11l0", "AVLc12l0", "AVLc13l0", "AVLc14l0", "AVLct1l0", "AVLct2l0", "AVLct3l0", "AVLct4l0", "AVLct5l0", "AVLct6l0", "AVLct7l0", "AVLct8l0", "AVLct9l0", "AVLctrl0"]
+	},
+	"lavaDeadTree":{
+		"biome":{
+			"terrain" : "lava",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLdead0", "AVLdead1", "AVLdead2", "AVLdead3", "AVLdead4", "AVLdead5", "AVLdead6", "AVLdead7"]
+	},
+	"lavaLake":{
+		"biome":{
+			"terrain" : "lava",
+			"objectType" : "lake"
+		},
+		"templates" : ["AVLlav10", "AVLlav20", "AVLlav30", "AVLlav40", "AVLlav50", "AVLlav60", "AVLlav70", "AVLlav80", "AVLlav90", "AVLlv100", "AVLlv110", "AVLlv120", "AVLlv130", "AVLlv140", "AVLlv150", "AVLlv160", "AVLlv170", "AVLlv180", "AVLlv190", "AVLlv200", "AVLlv210", "AVLlv220", "AVLlv230", "AVLlv240", "AVLlv250", "AVLlv260"]
+	},
+	"templateSet180":{
+		"biome":{
+			"terrain" : "lava",
+			"objectType" : "mountain"
+		},
+		"templates" : ["AVLmtvo1", "AVLmtvo2", "AVLmtvo3", "AVLmtvo4", "AVLmtvo5", "AVLmtvo6"]
+	},
+	"volcanos":{
+		"biome":{
+			"terrain" : "lava",
+			"objectType" : "mountain"
+		},
+		"templates" : ["AVLvol10", "AVLvol20", "AVLvol30", "AVLvol40", "AVLvol50"]
+	},
+	"templateSet192":{
+		"biome":{
+			"terrain" : "water",
+			"objectType" : "tree"
+		},
+		"templates" : ["AVLklp10", "AVLklp20"]
+	},
+	"templateSet193":{
+		"biome":{
+			"terrain" : "water",
+			"objectType" : "rock"
+		},
+		"templates" : ["AVLrk1w0", "AVLrk2w0", "AVLrk3w0", "AVLrk4w0"]
+	},
+	"templateSet194":{
+		"biome":{
+			"terrain" : "water",
+			"objectType" : "mountain"
+		},
+		"templates" : ["AVLref10", "AVLref20", "AVLref30", "AVLref40", "AVLref50", "AVLref60"]
+	}
+}

+ 20 - 1
config/gameConfig.json

@@ -67,6 +67,11 @@
 		"config/objects/witchHut.json"
 	],
 
+	"biomes" :
+	[
+		"config/biomes.json"
+	],
+
 	"artifacts" :
 	[
 		"config/artifacts.json"
@@ -312,7 +317,7 @@
 			// defines dice size of a luck roll, based on creature's luck
 			"goodLuckDice" : [ 24, 12, 8 ],
 			"badLuckDice" : [],
-			
+
 			// every 1 attack point damage influence in battle when attack points > defense points during creature attack
 			"attackPointDamageFactor": 0.05, 
 			// limit of damage increase that can be achieved by overpowering attack points
@@ -388,6 +393,20 @@
 			// if enabled flying will work like in original game, otherwise nerf similar to HotA flying is applied
 			"originalFlyRules" : false
 		},
+
+		"spells":
+		{
+			// if enabled, dimension work doesn't work into tiles under Fog of War
+			"dimensionDoorOnlyToUncoveredTiles" : false,
+			// if enabled, dimension door will hint regarding tile being incompatible terrain type, unlike H3 (water/land)
+			"dimensionDoorExposesTerrainType" : false,
+			// if enabled, attempt to use dimension door on incompatible terrain (water/land) will result in spending of mana, movement and casts per day (H3 behavior)
+			"dimensionDoorFailureSpendsPoints" : true,
+			// if enabled, dimension door will initiate a fight upon landing on tile adjacent to neutral creature
+			"dimensionDoorTriggersGuards" : false,
+			// if enabled, dimension door can be used 1x per day, exception being 2x per day for XL+U or bigger maps (41472 tiles) + hero having expert air magic
+			"dimensionDoorTournamentRulesLimit" : false
+		},
 		
 		"bonuses" : 
 		{

+ 1 - 1
config/heroes/conflux.json

@@ -176,7 +176,7 @@
 			"bonuses" : {
 				"damage" : {
 					"type" : "CREATURE_DAMAGE",
-					"subtype" : "creatureDamageMin",
+					"subtype" : "creatureDamageBoth",
 					"val" : 5
 				},
 				"attack" : {

+ 3 - 3
config/mainmenu.json

@@ -21,9 +21,9 @@
 				"name" : "main",
 				"buttons":
 				[
-					{"x": 644, "y":  70, "center" : true, "name":"MMENUNG", "shortcut" : "mainMenuNew", "help": 3, "command": "to new"},
-					{"x": 645, "y": 192, "center" : true, "name":"MMENULG", "shortcut" : "mainMenuLoad", "help": 4, "command": "to load"},
-					{"x": 643, "y": 296, "center" : true, "name":"MMENUHS", "shortcut" : "mainMenuScores", "help": 5, "command": "highscores"},
+					{"x": 644, "y":  70, "center" : true, "name":"MMENUNG", "shortcut" : "mainMenuNewGame", "help": 3, "command": "to new"},
+					{"x": 645, "y": 192, "center" : true, "name":"MMENULG", "shortcut" : "mainMenuLoadGame", "help": 4, "command": "to load"},
+					{"x": 643, "y": 296, "center" : true, "name":"MMENUHS", "shortcut" : "mainMenuHighScores", "help": 5, "command": "highscores"},
 					{"x": 643, "y": 414, "center" : true, "name":"MMENUCR", "shortcut" : "mainMenuCredits", "help": 6, "command": "to credits"},
 					{"x": 643, "y": 520, "center" : true, "name":"MMENUQT", "shortcut" : "mainMenuQuit", "help": 7, "command": "exit"}
 				]

+ 67 - 0
config/schemas/biome.json

@@ -0,0 +1,67 @@
+{
+	"type" : "object",
+	"$schema" : "http://json-schema.org/draft-04/schema",
+	"title" : "VCMI map obstacle set format",
+	"description" : "Description of map object set, used only as sub-schema of object",
+	"required" : ["biome", "templates"],
+	"additionalProperties" : true, // may have type-dependant properties
+	"properties" : {
+		"biome" : {
+			"type" : "object",
+			"properties": {
+				"objectType" : {
+					"type" : "string",
+					"enmum": ["mountain", "tree", "lake", "crater", "rock", "plant", "structure", "animal", "other"],
+					"description" : "Type of the obstacle set"
+				},
+				"terrain" : {
+					"anyOf": [
+						{
+							"type" : "string",
+							"description" : "Terrain of the obstacle set"
+						},
+						{
+							"type" : "array",
+							"items" : { "type" : "string" },
+							"description" : "Terrains of the obstacle set"
+						}
+					]
+					
+				},
+				"faction" : {
+					"anyOf": [
+						{
+							"type" : "string",
+							"description" : "Faction of the zone"
+						},
+						{
+							"type" : "array",
+							"items" : { "type" : "string" },
+							"description" : "Factions of the zone"
+						}
+					]
+				},
+				"alignment" : {
+					"anyOf": [
+						{
+							"type" : "string",
+							"enum" : ["good", "evil", "neutral"],
+							"description" : "Alignment of faction of the zone"
+						},
+						{
+							"type" : "array",
+							"items" : { "type" : "string" },
+							"description" : "Alignment of faction of the zone"
+						}
+					]
+				}
+			}
+		},
+		"templates" : {
+			"type" : "array",
+			"items" : { "type" : "string" },
+			"description" : "Object templates of the obstacle set"
+		}
+	}
+}
+

+ 4 - 0
config/schemas/faction.json

@@ -58,6 +58,10 @@
 			"type" : "boolean",
 			"description" : "Random map generator places player/cpu-owned towns underground if true is specified and on the ground otherwise. Parameter is unused for maps without underground."
 		},
+		"special" : {
+			"type" : "boolean",
+			"description" : "If true, faction is disabled from starting pick and in random map generation"
+		},
 		"creatureBackground" : {
 			"type" : "object",
 			"additionalProperties" : false,

+ 5 - 0
config/schemas/mod.json

@@ -255,6 +255,11 @@
 			"description" : "List of configuration files for objects",
 			"items" : { "type" : "string", "format" : "textFile" }
 		},
+		"biomes" : {
+			"type" : "array",
+			"description" : "List of configuration files for biomes",
+			"items" : { "type" : "string", "format" : "textFile" }
+		},
 		"bonuses" : {
 			"type" : "array",
 			"description" : "List of configuration files for bonuses",

+ 7 - 1
config/schemas/settings.json

@@ -160,7 +160,8 @@
 				"displayIndex",
 				"showfps",
 				"targetfps",
-				"vsync"
+				"vsync",
+				"scalingMode"
 			],
 			"properties" : {
 				"resolution" : {
@@ -223,6 +224,11 @@
 				"vsync" : {
 					"type" : "boolean",
 					"default" : true
+				},
+				"scalingMode" : {
+					"type" : "string",
+					"enum" : [ "nearest", "linear", "best" ],
+					"default" : "best"
 				}
 			}
 		},

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini