Browse Source

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

Xilmi 11 months ago
parent
commit
65f2d0c44e
100 changed files with 1339 additions and 652 deletions
  1. 1 1
      AI/Nullkiller/AIGateway.cpp
  2. 1 1
      AI/VCAI/VCAI.cpp
  3. 0 7
      CMakeLists.txt
  4. 3 1
      CMakePresets.json
  5. BIN
      Mods/vcmi/Data/stackWindow/bonus-effects.png
  6. BIN
      Mods/vcmi/Sprites/lobby/delete-normal.png
  7. BIN
      Mods/vcmi/Sprites/lobby/delete-pressed.png
  8. 8 0
      Mods/vcmi/Sprites/lobby/deleteButton.json
  9. 3 0
      Mods/vcmi/config/chinese.json
  10. 14 0
      Mods/vcmi/config/english.json
  11. 56 3
      Mods/vcmi/config/german.json
  12. 35 3
      Mods/vcmi/config/polish.json
  13. 1 0
      client/ArtifactsUIController.cpp
  14. 1 1
      client/ClientCommandManager.cpp
  15. 1 1
      client/NetPacksLobbyClient.cpp
  16. 10 8
      client/battle/BattleAnimationClasses.cpp
  17. 4 3
      client/battle/BattleAnimationClasses.h
  18. 5 4
      client/battle/BattleEffectsController.cpp
  19. 3 2
      client/battle/BattleEffectsController.h
  20. 1 3
      client/battle/BattleFieldController.cpp
  21. 2 2
      client/battle/BattleInterface.cpp
  22. 1 1
      client/battle/BattleInterfaceClasses.cpp
  23. 4 4
      client/battle/BattleObstacleController.cpp
  24. 1 1
      client/battle/BattleStacksController.cpp
  25. 5 5
      client/battle/CreatureAnimation.cpp
  26. 4 16
      client/lobby/OptionsTab.cpp
  27. 2 3
      client/lobby/OptionsTab.h
  28. 117 14
      client/lobby/SelectionTab.cpp
  29. 8 1
      client/lobby/SelectionTab.h
  30. 2 1
      client/mainmenu/CCampaignScreen.cpp
  31. 7 1
      client/mainmenu/CMainMenu.cpp
  32. 15 22
      client/mapView/MapRenderer.cpp
  33. 1 1
      client/mapView/MapRenderer.h
  34. 2 3
      client/mapView/MapRendererContextState.cpp
  35. 120 0
      client/render/AssetGenerator.cpp
  36. 2 0
      client/render/AssetGenerator.h
  37. 29 12
      client/render/IImage.h
  38. 7 2
      client/render/ImageLocator.cpp
  39. 5 11
      client/render/ImageLocator.h
  40. 54 35
      client/renderSDL/ImageScaled.cpp
  41. 7 9
      client/renderSDL/ImageScaled.h
  42. 120 30
      client/renderSDL/RenderHandler.cpp
  43. 9 7
      client/renderSDL/RenderHandler.h
  44. 77 73
      client/renderSDL/SDLImage.cpp
  45. 16 24
      client/renderSDL/SDLImage.h
  46. 1 1
      client/renderSDL/SDL_Extensions.cpp
  47. 21 21
      client/widgets/CArtifactsOfHeroBase.cpp
  48. 16 16
      client/widgets/CArtifactsOfHeroMarket.cpp
  49. 3 3
      client/widgets/CGarrisonInt.cpp
  50. 5 5
      client/widgets/Images.cpp
  51. 4 7
      client/widgets/ObjectLists.cpp
  52. 9 2
      client/windows/CCastleInterface.cpp
  53. 54 4
      client/windows/CCreatureWindow.cpp
  54. 4 0
      client/windows/CCreatureWindow.h
  55. 2 1
      client/windows/CHeroWindow.cpp
  56. 2 2
      client/windows/CreaturePurchaseCard.cpp
  57. 3 3
      client/windows/GUIClasses.cpp
  58. 1 1
      client/windows/GUIClasses.h
  59. 5 0
      clientapp/CMakeLists.txt
  60. 16 0
      config/campaignSets.json
  61. 14 11
      config/creatures/castle.json
  62. 19 17
      config/creatures/conflux.json
  63. 18 14
      config/creatures/dungeon.json
  64. 60 58
      config/creatures/fortress.json
  65. 14 12
      config/creatures/inferno.json
  66. 16 14
      config/creatures/necropolis.json
  67. 33 26
      config/creatures/neutral.json
  68. 17 15
      config/creatures/rampart.json
  69. 40 2
      config/creatures/special.json
  70. 14 10
      config/creatures/stronghold.json
  71. 13 9
      config/creatures/tower.json
  72. 2 1
      config/objects/generic.json
  73. 2 1
      config/schemas/spell.json
  74. 1 1
      config/spells/ability.json
  75. 2 2
      config/spells/offensive.json
  76. 1 1
      config/spells/other.json
  77. 2 2
      config/spells/timed.json
  78. 9 7
      docs/Readme.md
  79. 1 1
      docs/developers/Building_Windows.md
  80. 1 1
      docs/modders/Entities_Format/Spell_Format.md
  81. 34 0
      docs/modders/HD_Graphics.md
  82. 9 0
      docs/players/Heroes_Chronicles.md
  83. 46 13
      docs/players/Installation_iOS.md
  84. 7 7
      docs/players/Installation_macOS.md
  85. 1 1
      launcher/translation/chinese.ts
  86. 1 1
      launcher/translation/czech.ts
  87. 1 1
      launcher/translation/english.ts
  88. 1 1
      launcher/translation/french.ts
  89. 30 30
      launcher/translation/german.ts
  90. 1 1
      launcher/translation/polish.ts
  91. 1 1
      launcher/translation/portuguese.ts
  92. 1 1
      launcher/translation/russian.ts
  93. 1 1
      launcher/translation/spanish.ts
  94. 1 1
      launcher/translation/swedish.ts
  95. 1 1
      launcher/translation/ukrainian.ts
  96. 1 1
      launcher/translation/vietnamese.ts
  97. 1 1
      lib/ArtifactUtils.cpp
  98. 0 1
      lib/CArtHandler.cpp
  99. 2 2
      lib/CGameInfoCallback.cpp
  100. 10 0
      lib/CMakeLists.txt

+ 1 - 1
AI/Nullkiller/AIGateway.cpp

@@ -763,7 +763,7 @@ void AIGateway::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstan
 	//you can't request action from action-response thread
 	requestActionASAP([=]()
 	{
-		if(removableUnits && up->tempOwner == down->tempOwner && nullkiller->settings->isGarrisonTroopsUsageAllowed() && !cb->getStartInfo()->isSteadwickFallCampaignMission())
+		if(removableUnits && up->tempOwner == down->tempOwner && nullkiller->settings->isGarrisonTroopsUsageAllowed() && !cb->getStartInfo()->isRestorationOfErathiaCampaign())
 		{
 			pickBestCreatures(down, up);
 		}

+ 1 - 1
AI/VCAI/VCAI.cpp

@@ -731,7 +731,7 @@ void VCAI::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance *
 	//you can't request action from action-response thread
 	requestActionASAP([=]()
 	{
-		if(removableUnits && !cb->getStartInfo()->isSteadwickFallCampaignMission())
+		if(removableUnits && !cb->getStartInfo()->isRestorationOfErathiaCampaign())
 			pickBestCreatures(down, up);
 
 		answerQuery(queryID, 0);

+ 0 - 7
CMakeLists.txt

@@ -355,13 +355,6 @@ if(MINGW OR MSVC)
 		if(ICONV_FOUND)
 			set(SYSTEM_LIBS ${SYSTEM_LIBS} iconv)
 		endif()
-
-		# Prevent compiler issues when building Debug
-		# Assembler might fail with "too many sections"
-		# With big-obj or 64-bit build will take hours
-		if(CMAKE_BUILD_TYPE STREQUAL "Debug")
-			set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Og")
-		endif()
 	endif(MINGW)
 endif(MINGW OR MSVC)
 

+ 3 - 1
CMakePresets.json

@@ -134,7 +134,9 @@
             "description": "VCMI Windows Ninja using MinGW",
             "inherits": "default-release",
             "cacheVariables": {
-                "CMAKE_BUILD_TYPE": "Release"
+                "CMAKE_BUILD_TYPE": "Release",
+                "CMAKE_C_COMPILER": "gcc",
+                "CMAKE_CXX_COMPILER": "g++"
             }
         },
         {

BIN
Mods/vcmi/Data/stackWindow/bonus-effects.png


BIN
Mods/vcmi/Sprites/lobby/delete-normal.png


BIN
Mods/vcmi/Sprites/lobby/delete-pressed.png


+ 8 - 0
Mods/vcmi/Sprites/lobby/deleteButton.json

@@ -0,0 +1,8 @@
+{
+	"basepath" : "lobby/",
+	"images" :
+	[
+		{ "frame" : 0, "file" : "delete-normal.png"},
+		{ "frame" : 1, "file" : "delete-pressed.png"}
+	]
+}

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

@@ -85,6 +85,7 @@
 	"vcmi.spellResearch.research" : "研究此法术",
 	"vcmi.spellResearch.skip" : "跳过此法术",
 	"vcmi.spellResearch.abort" : "中止",
+	"vcmi.spellResearch.noMoreSpells" : "没有更多的法术可供研究。",
 
 	"vcmi.mainMenu.serverConnecting" : "连接中...",
 	"vcmi.mainMenu.serverAddressEnter" : "使用地址:",
@@ -705,6 +706,8 @@
 	"core.bonus.DISINTEGRATE.description": "死亡后不会留下尸体",
 	"core.bonus.INVINCIBLE.name": "无敌",
 	"core.bonus.INVINCIBLE.description": "不受任何效果影响",
+	"core.bonus.MECHANICAL.name": "机械",
+	"core.bonus.MECHANICAL.description": "免疫大多数效果,可修复",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.name": "棱光吐息",
 	"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "攻击后向三方向扩散攻击"
 }

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

@@ -28,6 +28,13 @@
 	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(Movement points: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Sorry, replay opponent turn is not implemented yet!",
 
+	"vcmi.bonusSource.artifact" : "Artifact",
+	"vcmi.bonusSource.creature" : "Ability",
+	"vcmi.bonusSource.spell" : "Spell",
+	"vcmi.bonusSource.hero" : "Hero",
+	"vcmi.bonusSource.commander" : "Commander",
+	"vcmi.bonusSource.other" : "Other",
+
 	"vcmi.capitalColors.0" : "Red",
 	"vcmi.capitalColors.1" : "Blue",
 	"vcmi.capitalColors.2" : "Tan",
@@ -85,6 +92,7 @@
 	"vcmi.spellResearch.research" : "Research this Spell",
 	"vcmi.spellResearch.skip" : "Skip this Spell",
 	"vcmi.spellResearch.abort" : "Abort",
+	"vcmi.spellResearch.noMoreSpells" : "There are no more spells available for research.",
 
 	"vcmi.mainMenu.serverConnecting" : "Connecting...",
 	"vcmi.mainMenu.serverAddressEnter" : "Enter address:",
@@ -106,6 +114,12 @@
 	"vcmi.lobby.handicap.resource" : "Gives players appropriate resources to start with in addition to the normal starting resources. Negative values are allowed, but are limited to 0 in total (the player never starts with negative resources).",
 	"vcmi.lobby.handicap.income" : "Changes the player's various incomes by the percentage. Is rounded up.",
 	"vcmi.lobby.handicap.growth" : "Changes the growth rate of creatures in the towns owned by the player. Is rounded up.",
+	"vcmi.lobby.deleteUnsupportedSave" : "{Unsupported saves found}\n\nVCMI has found %d saved games that are no longer supported, possibly due to differences in VCMI versions.\n\nDo you want to delete them?",
+	"vcmi.lobby.deleteSaveGameTitle" : "Select a Saved Game to delete",
+	"vcmi.lobby.deleteMapTitle" : "Select a Scenario to delete",
+	"vcmi.lobby.deleteFile" : "Do you want to delete following file?",
+	"vcmi.lobby.deleteFolder" : "Do you want to delete following folder?",
+	"vcmi.lobby.deleteMode" : "Switch to delete mode and back",
 		
 	"vcmi.lobby.login.title" : "VCMI Online Lobby",
 	"vcmi.lobby.login.username" : "Username:",

+ 56 - 3
Mods/vcmi/config/german.json

@@ -25,8 +25,16 @@
 	"vcmi.adventureMap.playerAttacked"                   : "Spieler wurde attackiert: %s",
 	"vcmi.adventureMap.moveCostDetails"                  : "Bewegungspunkte - Kosten: %TURNS Runden + %POINTS Punkte, Verbleibende Punkte: %REMAINING",
 	"vcmi.adventureMap.moveCostDetailsNoTurns"           : "Bewegungspunkte - Kosten: %POINTS Punkte, Verbleibende Punkte: %REMAINING",
+	"vcmi.adventureMap.movementPointsHeroInfo" 			 : "(Bewegungspunkte: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Das Wiederholen des gegnerischen Zuges ist aktuell noch nicht implementiert!",
 
+	"vcmi.bonusSource.artifact" : "Artefakt",
+	"vcmi.bonusSource.creature" : "Fähigkeit",
+	"vcmi.bonusSource.spell" : "Zauber",
+	"vcmi.bonusSource.hero" : "Held",
+	"vcmi.bonusSource.commander" : "Commander",
+	"vcmi.bonusSource.other" : "Anderes",
+
 	"vcmi.capitalColors.0" : "Rot",
 	"vcmi.capitalColors.1" : "Blau",
 	"vcmi.capitalColors.2" : "Braun",
@@ -41,6 +49,12 @@
 	"vcmi.heroOverview.secondarySkills" : "Sekundäre Skills",
 	"vcmi.heroOverview.spells" : "Zaubersprüche",
 	
+	"vcmi.quickExchange.moveUnit" : "Einheit bewegen",
+	"vcmi.quickExchange.moveAllUnits" : "Alle Einheiten bewegen",
+	"vcmi.quickExchange.swapAllUnits" : "Einheiten tauschen",
+	"vcmi.quickExchange.moveAllArtifacts" : "Alle Artefakte bewegen",
+	"vcmi.quickExchange.swapAllArtifacts" : "Artefakte tauschen",
+	
 	"vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen",
 	"vcmi.radialWheel.fillSingleUnit" : "Füllen mit einzelnen Kreaturen",
 	"vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen",
@@ -59,6 +73,16 @@
 	"vcmi.radialWheel.moveUp" : "Nach oben bewegen",
 	"vcmi.radialWheel.moveDown" : "Nach unten bewegen",
 	"vcmi.radialWheel.moveBottom" : "Ganz nach unten bewegen",
+	
+	"vcmi.randomMap.description" : "Die Karte wurde mit dem Zufallsgenerator erstellt.\nTemplate war %s, Größe %dx%d, Level %d, Spieler %d, Computer %d, Wasser %s, Monster %s, VCMI-Karte",
+	"vcmi.randomMap.description.isHuman" : ", %s ist Mensch",
+	"vcmi.randomMap.description.townChoice" : ", %s Stadt-Wahl ist %s",
+	"vcmi.randomMap.description.water.none" : "Kein",
+	"vcmi.randomMap.description.water.normal" : "Normal",
+	"vcmi.randomMap.description.water.islands" : "Inseln",
+	"vcmi.randomMap.description.monster.weak" : "Schwach",
+	"vcmi.randomMap.description.monster.normal" : "Normal",
+	"vcmi.randomMap.description.monster.strong" : "Stark",
 
 	"vcmi.spellBook.search" : "suchen...",
 
@@ -68,6 +92,7 @@
 	"vcmi.spellResearch.research" : "Erforsche diesen Zauberspruch",
 	"vcmi.spellResearch.skip" : "Überspringe diesen Zauberspruch",
 	"vcmi.spellResearch.abort" : "Abbruch",
+	"vcmi.spellResearch.noMoreSpells" : "Es sind keine weiteren Zaubersprüche für die Forschung verfügbar.",
 
 	"vcmi.mainMenu.serverConnecting" : "Verbinde...",
 	"vcmi.mainMenu.serverAddressEnter" : "Addresse eingeben:",
@@ -89,7 +114,13 @@
 	"vcmi.lobby.handicap.resource" : "Gibt den Spielern entsprechende Ressourcen zum Start zusätzlich zu den normalen Startressourcen. Negative Werte sind erlaubt, werden aber insgesamt auf 0 begrenzt (der Spieler beginnt nie mit negativen Ressourcen).",
 	"vcmi.lobby.handicap.income" : "Verändert die verschiedenen Einkommen des Spielers um den Prozentsatz. Wird aufgerundet.",
 	"vcmi.lobby.handicap.growth" : "Verändert die Wachstumsrate der Kreaturen in den Städten, die der Spieler besitzt. Wird aufgerundet.",
-	
+	"vcmi.lobby.deleteUnsupportedSave" : "{Nicht unterstützte Spielstände gefunden}\n\nVCMI hat %d gespeicherte Spiele gefunden, die nicht mehr unterstützt werden, möglicherweise aufgrund von Unterschieden in VCMI-Versionen.\n\nMöchtet Ihr sie löschen?",
+	"vcmi.lobby.deleteSaveGameTitle" : "Wählt gespeichertes Spiel zum Löschen aus",
+	"vcmi.lobby.deleteMapTitle" : "Wählt ein zu löschendes Szenario",
+	"vcmi.lobby.deleteFile" : "Möchtet Ihr folgende Datei löschen?",
+	"vcmi.lobby.deleteFolder" : "Möchtet Ihr folgenden Ordner löschen?",
+	"vcmi.lobby.deleteMode" : "In den Löschmodus wechseln und zurück",
+		
 	"vcmi.lobby.login.title" : "VCMI Online Lobby",
 	"vcmi.lobby.login.username" : "Benutzername:",
 	"vcmi.lobby.login.connecting" : "Verbinde...",
@@ -153,10 +184,12 @@
 	"vcmi.client.errors.invalidMap" : "{Ungültige Karte oder Kampagne}\n\nDas Spiel konnte nicht gestartet werden! Die ausgewählte Karte oder Kampagne ist möglicherweise ungültig oder beschädigt. Grund:\n%s",
 	"vcmi.client.errors.missingCampaigns" : "{Fehlende Dateien}\n\nEs wurden keine Kampagnendateien gefunden! Möglicherweise verwendest du unvollständige oder beschädigte Heroes 3 Datendateien. Bitte installiere die Spieldaten neu.",
 	"vcmi.server.errors.disconnected" : "{Netzwerkfehler}\n\nDie Verbindung zum Spielserver wurde unterbrochen!",
+	"vcmi.server.errors.playerLeft" : "{Verlassen eines Spielers}\n\n%s Spieler hat die Verbindung zum Spiel unterbrochen!", //%s -> player color
 	"vcmi.server.errors.existingProcess" : "Es läuft ein weiterer vcmiserver-Prozess, bitte beendet diesen zuerst",
 	"vcmi.server.errors.modsToEnable"    : "{Erforderliche Mods um das Spiel zu laden}",
 	"vcmi.server.errors.modsToDisable"   : "{Folgende Mods müssen deaktiviert werden}",
 	"vcmi.server.errors.modNoDependency" : "Mod {'%s'} konnte nicht geladen werden!\n Sie hängt von Mod {'%s'} ab, die nicht aktiv ist!\n",
+	"vcmi.server.errors.modDependencyLoop" : "Mod {'%s'} konnte nicht geladen werden.!\n Möglicherweise befindet sie sich in einer (weichen) Abhängigkeitsschleife.",
 	"vcmi.server.errors.modConflict" : "Mod {'%s'} konnte nicht geladen werden!\n Konflikte mit aktiver Mod {'%s'}!\n",
 	"vcmi.server.errors.unknownEntity" : "Spielstand konnte nicht geladen werden! Unbekannte Entität '%s' im gespeicherten Spiel gefunden! Der Spielstand ist möglicherweise nicht mit der aktuell installierten Version der Mods kompatibel!",
 	
@@ -339,6 +372,9 @@
 	"vcmi.townHall.missingBase"             : "Basis Gebäude %s muss als erstes gebaut werden",
 	"vcmi.townHall.noCreaturesToRecruit"    : "Es gibt keine Kreaturen zu rekrutieren!",
 
+	"vcmi.townStructure.bank.borrow" : "Ihr betretet die Bank. Ein Bankangestellter sieht Euch und sagt: \"Wir haben ein spezielles Angebot für Euch gemacht. Ihr könnt bei uns einen Kredit von 2500 Gold für 5 Tage aufnehmen. Ihr werdet jeden Tag 500 Gold zurückzahlen müssen.\"",
+	"vcmi.townStructure.bank.payBack" : "Ihr betretet die Bank. Ein Bankangestellter sieht Euch und sagt: \"Ihr habt Euren Kredit bereits erhalten. Zahlt Ihn ihn zurück, bevor Ihr einen neuen aufnehmt.\"",
+
 	"vcmi.logicalExpressions.anyOf"  : "Eines der folgenden:",
 	"vcmi.logicalExpressions.allOf"  : "Alles der folgenden:",
 	"vcmi.logicalExpressions.noneOf" : "Keines der folgenden:",
@@ -347,6 +383,13 @@
 	"vcmi.heroWindow.openCommander.help"  : "Zeige Informationen über Kommandanten dieses Helden",
 	"vcmi.heroWindow.openBackpack.hover" : "Artefakt-Rucksack-Fenster öffnen",
 	"vcmi.heroWindow.openBackpack.help"  : "Öffnet ein Fenster, das die Verwaltung des Artefakt-Rucksacks erleichtert",
+	"vcmi.heroWindow.sortBackpackByCost.hover"  : "Nach Kosten sortieren",
+	"vcmi.heroWindow.sortBackpackByCost.help"  : "Artefakte im Rucksack nach Kosten sortieren.",
+	"vcmi.heroWindow.sortBackpackBySlot.hover"  : "Nach Slot sortieren",
+	"vcmi.heroWindow.sortBackpackBySlot.help"  : "Artefakte im Rucksack nach Ausrüstungsslot sortieren.",
+	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Nach Klasse sortieren",
+	"vcmi.heroWindow.sortBackpackByClass.help"  : "Artefakte im Rucksack nach Artefaktklasse sortieren. Schatz, Klein, Groß, Relikt",
+	"vcmi.heroWindow.fusingArtifact.fusing" : "Ihr verfügt über alle Komponenten, die für die Fusion der %s benötigt werden. Möchtet Ihr die Verschmelzung durchführen? {Alle Komponenten werden bei der Fusion verbraucht.}",
 
 	"vcmi.tavernWindow.inviteHero"  : "Helden einladen",
 
@@ -523,7 +566,9 @@
 	"core.seerhut.quest.reachDate.visit.3" : "Geschlossen bis %s.",
 	"core.seerhut.quest.reachDate.visit.4" : "Geschlossen bis %s.",
 	"core.seerhut.quest.reachDate.visit.5" : "Geschlossen bis %s.",
-
+	
+	"mapObject.core.hillFort.object.description" : "Aufwertungen von Kreaturen. Die Stufen 1 - 4 sind billiger als in der zugehörigen Stadt.",
+	
 	"core.bonus.ADDITIONAL_ATTACK.name": "Doppelschlag",
 	"core.bonus.ADDITIONAL_ATTACK.description": "Greift zweimal an",
 	"core.bonus.ADDITIONAL_RETALIATION.name": "Zusätzliche Vergeltungsmaßnahmen",
@@ -671,5 +716,13 @@
 	"core.bonus.WATER_IMMUNITY.name": "Wasser-Immunität",
 	"core.bonus.WATER_IMMUNITY.description": "Immun gegen alle Zauber der Wasserschule",
 	"core.bonus.WIDE_BREATH.name": "Breiter Atem",
-	"core.bonus.WIDE_BREATH.description": "Breiter Atem-Angriff (mehrere Felder)"
+	"core.bonus.WIDE_BREATH.description": "Breiter Atem-Angriff (mehrere Felder)",
+	"core.bonus.DISINTEGRATE.name": "Auflösen",
+	"core.bonus.DISINTEGRATE.description": "Kein Leichnam bleibt nach dem Tod übrig",
+	"core.bonus.INVINCIBLE.name": "Unbesiegbar",
+	"core.bonus.INVINCIBLE.description": "Kann durch nichts beeinflusst werden",
+	"core.bonus.MECHANICAL.name": "Mechanisch",
+	"core.bonus.MECHANICAL.description": "Immunität gegen viele Effekte, reparierbar",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Prisma-Atem",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Prisma-Atem-Angriff (drei Richtungen)"
 }

+ 35 - 3
Mods/vcmi/config/polish.json

@@ -12,9 +12,11 @@
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "Przytłaczający",
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Śmiertelny",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Nie do pokonania",
-	"vcmi.adventureMap.monsterLevel"            : "\n\n%Jednostka %ATTACK_TYPE %LEVEL poziomu z miasta %TOWN",
+	"vcmi.adventureMap.monsterLevel"            : "\n\nJednostka %ATTACK_TYPE %LEVEL poziomu z miasta %TOWN",
 	"vcmi.adventureMap.monsterMeleeType"        : "Walcząca wręcz",
 	"vcmi.adventureMap.monsterRangedType"       : "Dystansowa",
+	"vcmi.adventureMap.search.hover"            : "Wyszukiwarka obiektów",
+	"vcmi.adventureMap.search.help"             : "Wybierz obiekt który chcesz znaleźć na mapie.",
 
 	"vcmi.adventureMap.confirmRestartGame"     : "Czy na pewno chcesz zrestartować grę?",
 	"vcmi.adventureMap.noTownWithMarket"       : "Brak dostępnego targowiska!",
@@ -26,6 +28,13 @@
 	"vcmi.adventureMap.movementPointsHeroInfo" : "(Punkty ruchu: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Wybacz, powtórka ruchu wroga nie została jeszcze zaimplementowana!",
 
+	"vcmi.bonusSource.artifact" : "Artefakt",
+	"vcmi.bonusSource.creature" : "Umiej.",
+	"vcmi.bonusSource.spell" : "Zaklęcie",
+	"vcmi.bonusSource.hero" : "Bohater",
+	"vcmi.bonusSource.commander" : "Dowódca",
+	"vcmi.bonusSource.other" : "Inne",
+	
 	"vcmi.capitalColors.0" : "Czerwony",
 	"vcmi.capitalColors.1" : "Niebieski",
 	"vcmi.capitalColors.2" : "Brązowy",
@@ -40,6 +49,12 @@
 	"vcmi.heroOverview.secondarySkills" : "Umiejętności drugorzędne",
 	"vcmi.heroOverview.spells" : "Zaklęcia",
 
+	"vcmi.quickExchange.moveUnit" : "Przenieś jednostkę",
+	"vcmi.quickExchange.moveAllUnits" : "Przenieś wszystkie jednostki",
+	"vcmi.quickExchange.swapAllUnits" : "Zamień armię",
+	"vcmi.quickExchange.moveAllArtifacts" : "Przenieś wszystkie artefakty",
+	"vcmi.quickExchange.swapAllArtifacts" : "Zamień artefakty",
+
 	"vcmi.radialWheel.mergeSameUnit" : "Złącz takie same stworzenia",
 	"vcmi.radialWheel.fillSingleUnit" : "Wypełnij pojedynczymi stworzeniami",
 	"vcmi.radialWheel.splitSingleUnit" : "Wydziel pojedyncze stworzenie",
@@ -59,14 +74,25 @@
 	"vcmi.radialWheel.moveDown" : "Przenieś w dół",
 	"vcmi.radialWheel.moveBottom" : "Przenieś na spód",
 
+	"vcmi.randomMap.description" : "Mapa stworzona przez generator map losowych.\nSzablon: %s, rozmiar %dx%d, poziomów %d, graczy %d, komputerowych %d, woda %s, potwory %s, mapa VCMI",
+	"vcmi.randomMap.description.isHuman" : ", %s jest człowiekiem",
+	"vcmi.randomMap.description.townChoice" : ", %s wybiera %s",
+	"vcmi.randomMap.description.water.none" : "brak",
+	"vcmi.randomMap.description.water.normal" : "norm.",
+	"vcmi.randomMap.description.water.islands" : "wyspy",
+	"vcmi.randomMap.description.monster.weak" : "słabi",
+	"vcmi.randomMap.description.monster.normal" : "norm.",
+	"vcmi.randomMap.description.monster.strong" : "silni",
+
 	"vcmi.spellBook.search" : "szukaj...",
 
 	"vcmi.spellResearch.canNotAfford" : "Nie stać Cię na zastąpienie {%SPELL1} przez {%SPELL2}, ale za to możesz odrzucić ten czar i kontynuować badania.",
 	"vcmi.spellResearch.comeAgain" : "Badania zostały już przeprowadzone dzisiaj. Wróć jutro.",
-	"vcmi.spellResearch.pay" : "Czy chcesz zastąpić {%SPELL1} czarem {%SPELL2}? Czy odrzucić ten czar i kontynuować badania?",
+	"vcmi.spellResearch.pay" : "Czy chcesz zastąpić {%SPELL1} zaklęciem {%SPELL2}? Czy odrzucić ten czar i kontynuować badania?",
 	"vcmi.spellResearch.research" : "Zamień zaklęcia",
 	"vcmi.spellResearch.skip" : "Kontynuuj badania",
 	"vcmi.spellResearch.abort" : "Anuluj",
+	"vcmi.spellResearch.noMoreSpells" : "Nie ma już więcej zaklęć do zbadania.",
 
 	"vcmi.mainMenu.serverConnecting" : "Łączenie...",
 	"vcmi.mainMenu.serverAddressEnter" : "Wprowadź adres:",
@@ -157,6 +183,7 @@
 	"vcmi.server.errors.modsToEnable"    : "{Następujące mody są wymagane do wczytania gry}",
 	"vcmi.server.errors.modsToDisable"   : "{Następujące mody muszą zostać wyłączone}",
 	"vcmi.server.errors.modNoDependency" : "Nie udało się wczytać moda {'%s'}!\n Jest on zależny od moda {'%s'} który nie jest aktywny!\n",
+	"vcmi.server.errors.modDependencyLoop" : "Nie udało się wczytać moda {'%s'}!\n Być może znajduje się w pętli zależności",
 	"vcmi.server.errors.modConflict" : "Nie udało się wczytać moda {'%s'}!\n Konflikty z aktywnym modem {'%s'}!\n",
 	"vcmi.server.errors.unknownEntity" : "Nie udało się wczytać zapisu! Nieznany element '%s' znaleziony w pliku zapisu! Zapis może nie być zgodny z aktualnie zainstalowaną wersją modów!",
 
@@ -356,6 +383,7 @@
 	"vcmi.heroWindow.sortBackpackBySlot.help"  : "Sortuj artefakty w sakwie według umiejscowienia na ciele",
 	"vcmi.heroWindow.sortBackpackByClass.hover"  : "Sortuj wg. jakości",
 	"vcmi.heroWindow.sortBackpackByClass.help"  : "Sortuj artefakty w sakwie według jakości: Skarb, Pomniejszy, Potężny, Relikt",
+	"vcmi.heroWindow.fusingArtifact.fusing" : "Posiadasz wszystkie niezbędne komponenty do stworzenia %s. Czy chcesz wykonać fuzję? {Wszystkie komponenty zostaną użyte}",
 
 	"vcmi.tavernWindow.inviteHero"  : "Zaproś bohatera",
 
@@ -686,5 +714,9 @@
 	"core.bonus.DISINTEGRATE.name": "Rozpadanie",
 	"core.bonus.DISINTEGRATE.description": "Po śmierci nie pozostaje żaden trup",
 	"core.bonus.INVINCIBLE.name": "Niezwyciężony",
-	"core.bonus.INVINCIBLE.description": "Nic nie może mieć na niego wpływu"
+	"core.bonus.INVINCIBLE.description": "Nic nie może mieć na niego wpływu",
+	"core.bonus.MECHANICAL.name": "Mechaniczny",
+	"core.bonus.MECHANICAL.description": "Odporny na wiele efektów, naprawialny",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.name": "Pryzmatyczny oddech",
+	"core.bonus.PRISM_HEX_ATTACK_BREATH.description": "Atakuje pryzmatycznym zionięciem (trzy kierunki)"
 }

+ 1 - 0
client/ArtifactsUIController.cpp

@@ -26,6 +26,7 @@
 ArtifactsUIController::ArtifactsUIController()
 {
 	numOfMovedArts = 0;
+	numOfArtsAskAssembleSession = 0;
 }
 
 bool ArtifactsUIController::askToAssemble(const ArtifactLocation & al, const bool onlyEquipped, const bool checkIgnored)

+ 1 - 1
client/ClientCommandManager.cpp

@@ -394,7 +394,7 @@ void ClientCommandManager::handleDef2bmpCommand(std::istringstream& singleWordBu
 {
 	std::string URI;
 	singleWordBuffer >> URI;
-	auto anim = GH.renderHandler().loadAnimation(AnimationPath::builtin(URI), EImageBlitMode::ALPHA);
+	auto anim = GH.renderHandler().loadAnimation(AnimationPath::builtin(URI), EImageBlitMode::SIMPLE);
 	anim->exportBitmaps(VCMIDirs::get().userExtractedPath());
 }
 

+ 1 - 1
client/NetPacksLobbyClient.cpp

@@ -226,7 +226,7 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState &
 	else
 		lobby->updateAfterStateChange();
 
-	if(pack.hostChanged)
+	if(pack.hostChanged || pack.refreshList)
 		lobby->toggleMode(handler.isHost());
 }
 

+ 10 - 8
client/battle/BattleAnimationClasses.cpp

@@ -881,9 +881,10 @@ uint32_t CastAnimation::getAttackClimaxFrame() const
 	return maxFrames / 2;
 }
 
-EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects, bool reversed):
+EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects, float transparencyFactor, bool reversed):
 	BattleAnimation(owner),
-	animation(GH.renderHandler().loadAnimation(animationName, EImageBlitMode::ALPHA)),
+	animation(GH.renderHandler().loadAnimation(animationName, EImageBlitMode::SIMPLE)),
+	transparencyFactor(transparencyFactor),
 	effectFlags(effects),
 	effectFinished(false),
 	reversed(reversed)
@@ -892,32 +893,32 @@ EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath &
 }
 
 EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<BattleHex> hex, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
+	EffectAnimation(owner, animationName, effects, 1.0f, reversed)
 {
 	battlehexes = hex;
 }
 
-EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
+EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex, int effects, float transparencyFactor, bool reversed):
+	EffectAnimation(owner, animationName, effects, transparencyFactor, reversed)
 {
 	assert(hex.isValid());
 	battlehexes.push_back(hex);
 }
 
 EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<Point> pos, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
+	EffectAnimation(owner, animationName, effects, 1.0f, reversed)
 {
 	positions = pos;
 }
 
 EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
+	EffectAnimation(owner, animationName, effects, 1.0f, reversed)
 {
 	positions.push_back(pos);
 }
 
 EffectAnimation::EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex, int effects, bool reversed):
-	EffectAnimation(owner, animationName, effects, reversed)
+	EffectAnimation(owner, animationName, effects, 1.0f, reversed)
 {
 	assert(hex.isValid());
 	battlehexes.push_back(hex);
@@ -951,6 +952,7 @@ bool EffectAnimation::init()
 	be.effectID = ID;
 	be.animation = animation;
 	be.currentFrame = 0;
+	be.transparencyFactor = transparencyFactor;
 	be.type = reversed ? BattleEffect::AnimType::REVERSE : BattleEffect::AnimType::DEFAULT;
 
 	for (size_t i = 0; i < std::max(battlehexes.size(), positions.size()); ++i)

+ 4 - 3
client/battle/BattleAnimationClasses.h

@@ -309,9 +309,10 @@ public:
 class EffectAnimation : public BattleAnimation
 {
 	std::string soundName;
+	int effectFlags;
+	float transparencyFactor;
 	bool effectFinished;
 	bool reversed;
-	int effectFlags;
 
 	std::shared_ptr<CAnimation>	animation;
 	std::vector<Point> positions;
@@ -335,14 +336,14 @@ public:
 	};
 
 	/// Create animation with screen-wide effect
-	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects = 0, bool reversed = false);
+	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, int effects = 0, float transparencyFactor = 1.f, bool reversed = false);
 
 	/// Create animation positioned at point(s). Note that positions must be are absolute, including battleint position offset
 	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos                 , int effects = 0, bool reversed = false);
 	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<Point> pos    , int effects = 0, bool reversed = false);
 
 	/// Create animation positioned at certain hex(es)
-	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex             , int effects = 0, bool reversed = false);
+	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, BattleHex hex             , int effects = 0, float transparencyFactor = 1.0f, bool reversed = false);
 	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, std::vector<BattleHex> hex, int effects = 0, bool reversed = false);
 
 	EffectAnimation(BattleInterface & owner, const AnimationPath & animationName, Point pos, BattleHex hex,   int effects = 0, bool reversed = false);

+ 5 - 4
client/battle/BattleEffectsController.cpp

@@ -44,7 +44,7 @@ void BattleEffectsController::displayEffect(EBattleEffect effect, const BattleHe
 	displayEffect(effect, AudioPath(), destTile);
 }
 
-void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile)
+void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile, float transparencyFactor)
 {
 	size_t effectID = static_cast<size_t>(effect);
 
@@ -52,7 +52,7 @@ void BattleEffectsController::displayEffect(EBattleEffect effect, const AudioPat
 
 	CCS->soundh->playSound( soundFile );
 
-	owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile));
+	owner.stacksController->addNewAnim(new EffectAnimation(owner, customAnim, destTile, 0, transparencyFactor));
 }
 
 void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bte)
@@ -69,7 +69,7 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt
 	switch(static_cast<BonusType>(bte.effect))
 	{
 		case BonusType::HP_REGENERATION:
-			displayEffect(EBattleEffect::REGENERATION, AudioPath::builtin("REGENER"), stack->getPosition());
+			displayEffect(EBattleEffect::REGENERATION, AudioPath::builtin("REGENER"), stack->getPosition(), 0.5);
 			break;
 		case BonusType::MANA_DRAIN:
 			displayEffect(EBattleEffect::MANA_DRAIN, AudioPath::builtin("MANADRAI"), stack->getPosition());
@@ -78,7 +78,7 @@ void BattleEffectsController::battleTriggerEffect(const BattleTriggerEffect & bt
 			displayEffect(EBattleEffect::POISON, AudioPath::builtin("POISON"), stack->getPosition());
 			break;
 		case BonusType::FEAR:
-			displayEffect(EBattleEffect::FEAR, AudioPath::builtin("FEAR"), stack->getPosition());
+			displayEffect(EBattleEffect::FEAR, AudioPath::builtin("FEAR"), stack->getPosition(), 0.5);
 			break;
 		case BonusType::MORALE:
 		{
@@ -124,6 +124,7 @@ void BattleEffectsController::collectRenderableObjects(BattleRenderer & renderer
 			currentFrame %= elem.animation->size();
 
 			auto img = elem.animation->getImage(currentFrame, static_cast<size_t>(elem.type));
+			img->setAlpha(255 * elem.transparencyFactor);
 
 			canvas.draw(img, elem.pos);
 		});

+ 3 - 2
client/battle/BattleEffectsController.h

@@ -39,7 +39,8 @@ struct BattleEffect
 
 	AnimType type;
 	Point pos; //position on the screen
-	float currentFrame;
+	float currentFrame = 0.0;
+	float transparencyFactor = 1.0;
 	std::shared_ptr<CAnimation> animation;
 	int effectID; //uniqueID equal ot ID of appropriate CSpellEffectAnim
 	BattleHex tile; //Indicates if effect which hex the effect is drawn on
@@ -65,7 +66,7 @@ public:
 
 	//displays custom effect on the battlefield
 	void displayEffect(EBattleEffect effect, const BattleHex & destTile);
-	void displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile);
+	void displayEffect(EBattleEffect effect, const AudioPath & soundFile, const BattleHex & destTile, float transparencyFactor = 1.f);
 
 	void battleTriggerEffect(const BattleTriggerEffect & bte);
 

+ 1 - 3
client/battle/BattleFieldController.cpp

@@ -114,7 +114,7 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 
 	//preparing cells and hexes
 	cellBorder = GH.renderHandler().loadImage(ImagePath::builtin("CCELLGRD.BMP"), EImageBlitMode::COLORKEY);
-	cellShade = GH.renderHandler().loadImage(ImagePath::builtin("CCELLSHD.BMP"), EImageBlitMode::ALPHA);
+	cellShade = GH.renderHandler().loadImage(ImagePath::builtin("CCELLSHD.BMP"), EImageBlitMode::SIMPLE);
 	cellUnitMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMovementHighlight.PNG"), EImageBlitMode::COLORKEY);
 	cellUnitMaxMovementHighlight = GH.renderHandler().loadImage(ImagePath::builtin("UnitMaxMovementHighlight.PNG"), EImageBlitMode::COLORKEY);
 
@@ -124,8 +124,6 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 	rangedFullDamageLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json"), EImageBlitMode::COLORKEY);
 	shootingRangeLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsRed.json"), EImageBlitMode::COLORKEY);
 
-	cellShade->setShadowEnabled(true);
-
 	if(!owner.siegeController)
 	{
 		auto bfieldType = owner.getBattle()->battleGetBattlefieldType();

+ 2 - 2
client/battle/BattleInterface.cpp

@@ -535,9 +535,9 @@ void BattleInterface::displaySpellAnimationQueue(const CSpell * spell, const CSp
 				flags |= EffectAnimation::SCREEN_FILL;
 
 			if (!destinationTile.isValid())
-				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags));
+				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, flags, animation.transparency));
 			else
-				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags));
+				stacksController->addNewAnim(new EffectAnimation(*this, animation.resourceName, destinationTile, flags, animation.transparency));
 		}
 	}
 }

+ 1 - 1
client/battle/BattleInterfaceClasses.cpp

@@ -398,7 +398,7 @@ BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * her
 	else
 		animationPath = hero->getHeroClass()->imageBattleMale;
 
-	animation = GH.renderHandler().loadAnimation(animationPath, EImageBlitMode::ALPHA);
+	animation = GH.renderHandler().loadAnimation(animationPath, EImageBlitMode::WITH_SHADOW);
 
 	pos.w = 64;
 	pos.h = 136;

+ 4 - 4
client/battle/BattleObstacleController.cpp

@@ -50,11 +50,11 @@ void BattleObstacleController::loadObstacleImage(const CObstacleInstance & oi)
 	if (oi.obstacleType == CObstacleInstance::ABSOLUTE_OBSTACLE)
 	{
 		// obstacle uses single bitmap image for animations
-		obstacleImages[oi.uniqueID] = GH.renderHandler().loadImage(animationName.toType<EResType::IMAGE>(), EImageBlitMode::COLORKEY);
+		obstacleImages[oi.uniqueID] = GH.renderHandler().loadImage(animationName.toType<EResType::IMAGE>(), EImageBlitMode::SIMPLE);
 	}
 	else
 	{
-		obstacleAnimations[oi.uniqueID] = GH.renderHandler().loadAnimation(animationName, EImageBlitMode::COLORKEY);
+		obstacleAnimations[oi.uniqueID] = GH.renderHandler().loadAnimation(animationName, EImageBlitMode::SIMPLE);
 		obstacleImages[oi.uniqueID] = obstacleAnimations[oi.uniqueID]->getImage(0);
 	}
 }
@@ -78,7 +78,7 @@ void BattleObstacleController::obstacleRemoved(const std::vector<ObstacleChanges
 		if(animationPath.empty())
 			continue;
 
-		auto animation = GH.renderHandler().loadAnimation(animationPath, EImageBlitMode::COLORKEY);
+		auto animation = GH.renderHandler().loadAnimation(animationPath, EImageBlitMode::SIMPLE);
 		auto first = animation->getImage(0, 0);
 		if(!first)
 			continue;
@@ -105,7 +105,7 @@ void BattleObstacleController::obstaclePlaced(const std::vector<std::shared_ptr<
 		if(!oi->visibleForSide(side, owner.getBattle()->battleHasNativeStack(side)))
 			continue;
 
-		auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation(), EImageBlitMode::ALPHA);
+		auto animation = GH.renderHandler().loadAnimation(oi->getAppearAnimation(), EImageBlitMode::SIMPLE);
 		auto first = animation->getImage(0, 0);
 		if(!first)
 			continue;

+ 1 - 1
client/battle/BattleStacksController.cpp

@@ -636,7 +636,7 @@ void BattleStacksController::stackAttacking( const StackAttackInfo & info )
 	{
 		owner.addToAnimationStage(EAnimationEvents::AFTER_HIT, [=]()
 		{
-			owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, AudioPath::builtin("DRAINLIF"), attacker->getPosition());
+			owner.effectsController->displayEffect(EBattleEffect::DRAIN_LIFE, AudioPath::builtin("DRAINLIF"), attacker->getPosition(), 0.5);
 		});
 	}
 

+ 5 - 5
client/battle/CreatureAnimation.cpp

@@ -17,6 +17,7 @@
 #include "../render/CAnimation.h"
 #include "../render/Canvas.h"
 #include "../render/ColorFilter.h"
+#include "../render/Colors.h"
 #include "../render/IRenderHandler.h"
 
 static const ColorRGBA creatureBlueBorder = { 0, 255, 255, 255 };
@@ -199,8 +200,8 @@ CreatureAnimation::CreatureAnimation(const AnimationPath & name_, TSpeedControll
 	  speedController(controller),
 	  once(false)
 {
-	forward = GH.renderHandler().loadAnimation(name_, EImageBlitMode::ALPHA);
-	reverse = GH.renderHandler().loadAnimation(name_, EImageBlitMode::ALPHA);
+	forward = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_OVERLAY);
+	reverse = GH.renderHandler().loadAnimation(name_, EImageBlitMode::WITH_SHADOW_AND_OVERLAY);
 
 	// if necessary, add one frame into vcmi-only group DEAD
 	if(forward->size(size_t(ECreatureAnimType::DEAD)) == 0)
@@ -339,15 +340,14 @@ void CreatureAnimation::nextFrame(Canvas & canvas, const ColorFilter & shifter,
 
 	if(image)
 	{
-		image->setShadowEnabled(true);
-		image->setOverlayEnabled(isIdle());
 		if (isIdle())
 			image->setOverlayColor(genBorderColor(getBorderStrength(elapsedTime), border));
+		else
+			image->setOverlayColor(Colors::TRANSPARENCY);
 
 		image->adjustPalette(shifter, 0);
 
 		canvas.draw(image, pos.topLeft(), Rect(0, 0, pos.w, pos.h));
-
 	}
 }
 

+ 4 - 16
client/lobby/OptionsTab.cpp

@@ -765,9 +765,9 @@ void OptionsTab::SelectionWindow::sliderMove(int slidPos)
 	redraw();
 }
 
-bool OptionsTab::SelectionWindow::receiveEvent(const Point & position, int eventType) const
+void OptionsTab::SelectionWindow::notFocusedClick()
 {
-	return true;  // capture click also outside of window
+	close();
 }
 
 void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition)
@@ -775,12 +775,6 @@ void OptionsTab::SelectionWindow::clickReleased(const Point & cursorPosition)
 	if(slider && slider->pos.isInside(cursorPosition))
 		return;
 
-	if(!pos.isInside(cursorPosition))
-	{
-		close();
-		return;
-	}
-
 	int elem = getElement(cursorPosition);
 
 	setElement(elem, true);
@@ -898,15 +892,9 @@ OptionsTab::HandicapWindow::HandicapWindow()
 	center();
 }
 
-bool OptionsTab::HandicapWindow::receiveEvent(const Point & position, int eventType) const
+void OptionsTab::HandicapWindow::notFocusedClick()
 {
-	return true;  // capture click also outside of window
-}
-
-void OptionsTab::HandicapWindow::clickReleased(const Point & cursorPosition)
-{
-	if(!pos.isInside(cursorPosition)) // make it possible to close window by touching/clicking outside of window
-		close();
+	close();
 }
 
 OptionsTab::SelectedBox::SelectedBox(Point position, PlayerSettings & playerSettings, SelType type)

+ 2 - 3
client/lobby/OptionsTab.h

@@ -63,8 +63,7 @@ public:
 		std::map<PlayerColor, std::map<EGameResID, std::shared_ptr<CTextInput>>> textinputs;
 		std::vector<std::shared_ptr<CButton>> buttons;
 
-		bool receiveEvent(const Point & position, int eventType) const override;
-		void clickReleased(const Point & cursorPosition) override;
+		void notFocusedClick() override;
 	public:
 		HandicapWindow();
 	};
@@ -167,7 +166,7 @@ private:
 
 		void sliderMove(int slidPos);
 
-		bool receiveEvent(const Point & position, int eventType) const override;
+		void notFocusedClick() override;
 		void clickReleased(const Point & cursorPosition) override;
 		void showPopupWindow(const Point & cursorPosition) override;
 

+ 117 - 14
client/lobby/SelectionTab.cpp

@@ -42,9 +42,11 @@
 #include "../../lib/mapping/CMapInfo.h"
 #include "../../lib/mapping/CMapHeader.h"
 #include "../../lib/mapping/MapFormat.h"
+#include "../../lib/networkPacks/PacksForLobby.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
 #include "../../lib/texts/TextOperations.h"
 #include "../../lib/TerrainHandler.h"
+#include "../../lib/UnlockGuard.h"
 
 bool mapSorter::operator()(const std::shared_ptr<ElementInfo> aaa, const std::shared_ptr<ElementInfo> bbb)
 {
@@ -152,7 +154,7 @@ static ESortBy getSortBySelectionScreen(ESelectionScreen Type)
 }
 
 SelectionTab::SelectionTab(ESelectionScreen Type)
-	: CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder(""), currentMapSizeFilter(0), showRandom(false)
+	: CIntObject(LCLICK | SHOW_POPUP | KEYBOARD | DOUBLECLICK), callOnSelect(nullptr), tabType(Type), selectionPos(0), sortModeAscending(true), inputNameRect{32, 539, 350, 20}, curFolder(""), currentMapSizeFilter(0), showRandom(false), deleteMode(false)
 {
 	OBJECT_CONSTRUCTION;
 		
@@ -192,20 +194,23 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
 
 	int positionsToShow = 18;
 	std::string tabTitle;
+	std::string tabTitleDelete;
 	switch(tabType)
 	{
 	case ESelectionScreen::newGame:
-		tabTitle = CGI->generaltexth->arraytxt[229];
+		tabTitle = "{" + CGI->generaltexth->arraytxt[229] + "}";
+		tabTitleDelete = "{red|" + CGI->generaltexth->translate("vcmi.lobby.deleteMapTitle") + "}";
 		break;
 	case ESelectionScreen::loadGame:
-		tabTitle = CGI->generaltexth->arraytxt[230];
+		tabTitle = "{" + CGI->generaltexth->arraytxt[230] + "}";
+		tabTitleDelete = "{red|" + CGI->generaltexth->translate("vcmi.lobby.deleteSaveGameTitle") + "}";
 		break;
 	case ESelectionScreen::saveGame:
 		positionsToShow = 16;
-		tabTitle = CGI->generaltexth->arraytxt[231];
+		tabTitle = "{" + CGI->generaltexth->arraytxt[231] + "}";
 		break;
 	case ESelectionScreen::campaignList:
-		tabTitle = CGI->generaltexth->allTexts[726];
+		tabTitle = "{" + CGI->generaltexth->allTexts[726] + "}";
 		setRedrawParent(true); // we use parent background so we need to make sure it's will be redrawn too
 		pos.w = parent->pos.w;
 		pos.h = parent->pos.h;
@@ -225,12 +230,26 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
 		auto sortByDate = std::make_shared<CButton>(Point(371, 85), AnimationPath::builtin("selectionTabSortDate"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.sortDate")), std::bind(&SelectionTab::sortBy, this, ESortBy::_changeDate), EShortcut::MAPS_SORT_CHANGEDATE);
 		sortByDate->setOverlay(std::make_shared<CPicture>(ImagePath::builtin("lobby/selectionTabSortDate")));
 		buttonsSortBy.push_back(sortByDate);
+
+		if(tabType == ESelectionScreen::loadGame || tabType == ESelectionScreen::newGame)
+		{
+			buttonDeleteMode = std::make_shared<CButton>(Point(367, 18), AnimationPath::builtin("lobby/deleteButton"), CButton::tooltip("", CGI->generaltexth->translate("vcmi.lobby.deleteMode")), [this, tabTitle, tabTitleDelete](){
+				deleteMode = !deleteMode;
+				if(deleteMode)
+					labelTabTitle->setText(tabTitleDelete);
+				else
+					labelTabTitle->setText(tabTitle);
+			});
+
+			if(tabType == ESelectionScreen::newGame)
+				buttonDeleteMode->setEnabled(false);
+		}
 	}
 
 	for(int i = 0; i < positionsToShow; i++)
 		listItems.push_back(std::make_shared<ListItem>(Point(30, 129 + i * 25)));
 
-	labelTabTitle = std::make_shared<CLabel>(205, 28, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, tabTitle);
+	labelTabTitle = std::make_shared<CLabel>(205, 28, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, tabTitle);
 	slider = std::make_shared<CSlider>(Point(372, 86 + (enableUiEnhancements ? 30 : 0)), (tabType != ESelectionScreen::saveGame ? 480 : 430) - (enableUiEnhancements ? 30 : 0), std::bind(&SelectionTab::sliderMove, this, _1), positionsToShow, (int)curItems.size(), 0, Orientation::VERTICAL, CSlider::BLUE);
 	slider->setPanningStep(24);
 
@@ -242,10 +261,10 @@ SelectionTab::SelectionTab(ESelectionScreen Type)
 
 void SelectionTab::toggleMode()
 {
+	allItems.clear();
+	curItems.clear();
 	if(CSH->isGuest())
 	{
-		allItems.clear();
-		curItems.clear();
 		if(slider)
 			slider->block(true);
 	}
@@ -263,9 +282,12 @@ void SelectionTab::toggleMode()
 			}
 
 		case ESelectionScreen::loadGame:
-			inputName->disable();
-			parseSaves(getFiles("Saves/", EResType::SAVEGAME));
-			break;
+			{
+				inputName->disable();
+				auto unsupported = parseSaves(getFiles("Saves/", EResType::SAVEGAME));
+				handleUnsupportedSavegames(unsupported);
+				break;
+			}
 
 		case ESelectionScreen::saveGame:
 			parseSaves(getFiles("Saves/", EResType::SAVEGAME));
@@ -309,7 +331,35 @@ void SelectionTab::clickReleased(const Point & cursorPosition)
 
 	if(line != -1 && curItems.size() > line)
 	{
-		select(line);
+		if(!deleteMode)
+			select(line);
+		else
+		{
+			int py = line + slider->getValue();
+			vstd::amax(py, 0);
+			vstd::amin(py, curItems.size() - 1);
+
+			if(curItems[py]->isFolder && boost::algorithm::starts_with(curItems[py]->folderName, ".."))
+			{
+				select(line);
+				return;
+			}
+
+			if(!curItems[py]->isFolder)
+				CInfoWindow::showYesNoDialog(CGI->generaltexth->translate("vcmi.lobby.deleteFile") + "\n\n" + curItems[py]->fullFileURI, std::vector<std::shared_ptr<CComponent>>(), [this, py](){
+					LobbyDelete ld;
+					ld.type = tabType == ESelectionScreen::newGame ? LobbyDelete::EType::RANDOMMAP : LobbyDelete::EType::SAVEGAME;
+					ld.name = curItems[py]->fileURI;
+					CSH->sendLobbyPack(ld);
+				}, nullptr);
+			else
+				CInfoWindow::showYesNoDialog(CGI->generaltexth->translate("vcmi.lobby.deleteFolder") + "\n\n" + curFolder + curItems[py]->folderName, std::vector<std::shared_ptr<CComponent>>(), [this, py](){
+					LobbyDelete ld;
+					ld.type = LobbyDelete::EType::SAVEGAME_FOLDER;
+					ld.name = curFolder + curItems[py]->folderName;
+					CSH->sendLobbyPack(ld);
+				}, nullptr);
+		}
 	}
 #ifdef VCMI_MOBILE
 	// focus input field if clicked inside it
@@ -475,6 +525,9 @@ void SelectionTab::filter(int size, bool selectFirst)
 
 	curItems.clear();
 
+	if(buttonDeleteMode)
+		buttonDeleteMode->setEnabled(tabType != ESelectionScreen::newGame || showRandom);
+
 	for(auto elem : allItems)
 	{
 		if((elem->mapHeader && (!size || elem->mapHeader->width == size)) || tabType == ESelectionScreen::campaignList)
@@ -826,8 +879,10 @@ void SelectionTab::parseMaps(const std::unordered_set<ResourcePath> & files)
 	}
 }
 
-void SelectionTab::parseSaves(const std::unordered_set<ResourcePath> & files)
+std::vector<ResourcePath> SelectionTab::parseSaves(const std::unordered_set<ResourcePath> & files)
 {
+	std::vector<ResourcePath> unsupported;
+
 	for(auto & file : files)
 	{
 		try
@@ -866,15 +921,43 @@ void SelectionTab::parseSaves(const std::unordered_set<ResourcePath> & files)
 
 			allItems.push_back(mapInfo);
 		}
+		catch(const IdentifierResolutionException & e)
+		{
+			logGlobal->error("Error: Failed to process %s: %s", file.getName(), e.what());
+		}
 		catch(const std::exception & e)
 		{
+			unsupported.push_back(file); // IdentifierResolutionException is not relevant -> not ask to delete, when mods are disabled
 			logGlobal->error("Error: Failed to process %s: %s", file.getName(), e.what());
 		}
 	}
+
+	return unsupported;
+}
+
+void SelectionTab::handleUnsupportedSavegames(const std::vector<ResourcePath> & files)
+{
+	if(CSH->isHost() && files.size())
+	{
+		MetaString text = MetaString::createFromTextID("vcmi.lobby.deleteUnsupportedSave");
+		text.replaceNumber(files.size());
+		CInfoWindow::showYesNoDialog(text.toString(), std::vector<std::shared_ptr<CComponent>>(), [files](){
+			for(auto & file : files)
+			{
+				LobbyDelete ld;
+				ld.type = LobbyDelete::EType::SAVEGAME;
+				ld.name = file.getName();
+				CSH->sendLobbyPack(ld);
+			}
+		}, nullptr);
+	}
 }
 
 void SelectionTab::parseCampaigns(const std::unordered_set<ResourcePath> & files)
 {
+	auto campaignSets = JsonNode(JsonPath::builtin("config/campaignSets.json"));
+	auto mainmenu = JsonNode(JsonPath::builtin("config/mainmenu.json"));
+
 	allItems.reserve(files.size());
 	for(auto & file : files)
 	{
@@ -882,8 +965,28 @@ void SelectionTab::parseCampaigns(const std::unordered_set<ResourcePath> & files
 		info->fileURI = file.getOriginalName();
 		info->campaignInit();
 		info->name = info->getNameForList();
+				
 		if(info->campaign)
-			allItems.push_back(info);
+		{
+			// skip campaigns organized in sets
+			std::string foundInSet = "";
+			for (auto const & set : campaignSets.Struct())
+				for (auto const & item : set.second["items"].Vector())
+					if(file.getName() == ResourcePath(item["file"].String()).getName())
+						foundInSet = set.first;
+			
+			// set has to be used in main menu
+			bool setInMainmenu = false;
+			if(!foundInSet.empty())
+				for (auto const & item : mainmenu["window"]["items"].Vector())
+					if(item["name"].String() == "campaign")
+						for (auto const & button : item["buttons"].Vector())
+							if(boost::algorithm::ends_with(boost::algorithm::to_lower_copy(button["command"].String()), boost::algorithm::to_lower_copy(foundInSet)))
+								setInMainmenu = true;
+
+			if(!setInMainmenu)
+				allItems.push_back(info);
+		}
 	}
 }
 

+ 8 - 1
client/lobby/SelectionTab.h

@@ -72,6 +72,8 @@ class SelectionTab : public CIntObject
 	// FIXME: CSelectionBase use them too!
 	std::shared_ptr<CAnimation> iconsVictoryCondition;
 	std::shared_ptr<CAnimation> iconsLossCondition;
+
+	std::vector<std::shared_ptr<ListItem>> unSupportedSaves;
 public:
 	std::vector<std::shared_ptr<ElementInfo>> allItems;
 	std::vector<std::shared_ptr<ElementInfo>> curItems;
@@ -120,11 +122,16 @@ private:
 	ESelectionScreen tabType;
 	Rect inputNameRect;
 
+	std::shared_ptr<CButton> buttonDeleteMode;
+	bool deleteMode;
+
 	auto checkSubfolder(std::string path);
 
 	bool isMapSupported(const CMapInfo & info);
 	void parseMaps(const std::unordered_set<ResourcePath> & files);
-	void parseSaves(const std::unordered_set<ResourcePath> & files);
+	std::vector<ResourcePath> parseSaves(const std::unordered_set<ResourcePath> & files);
 	void parseCampaigns(const std::unordered_set<ResourcePath> & files);
 	std::unordered_set<ResourcePath> getFiles(std::string dirURI, EResType resType);
+
+	void handleUnsupportedSavegames(const std::vector<ResourcePath> & files);
 };

+ 2 - 1
client/mainmenu/CCampaignScreen.cpp

@@ -67,7 +67,8 @@ CCampaignScreen::CCampaignScreen(const JsonNode & config, std::string name)
 	}
 
 	for(const JsonNode & node : config[name]["items"].Vector())
-		campButtons.push_back(std::make_shared<CCampaignButton>(node, config, campaignSet));
+		if(CResourceHandler::get()->existsResource(ResourcePath(node["file"].String(), EResType::CAMPAIGN)))
+			campButtons.push_back(std::make_shared<CCampaignButton>(node, config, campaignSet));
 }
 
 void CCampaignScreen::activate()

+ 7 - 1
client/mainmenu/CMainMenu.cpp

@@ -38,6 +38,7 @@
 #include "../widgets/VideoWidget.h"
 #include "../windows/InfoWindows.h"
 #include "../CServerHandler.h"
+#include "../render/AssetGenerator.h"
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
@@ -403,6 +404,9 @@ void CMainMenu::openCampaignScreen(std::string name)
 {
 	auto const & config = CMainMenuConfig::get().getCampaigns();
 
+	AssetGenerator::createCampaignBackground();
+	AssetGenerator::createChroniclesCampaignImages();
+
 	if(!vstd::contains(config.Struct(), name))
 	{
 		logGlobal->error("Unknown campaign set: %s", name);
@@ -413,7 +417,9 @@ void CMainMenu::openCampaignScreen(std::string name)
 	for (auto const & entry : config[name]["items"].Vector())
 	{
 		ResourcePath resourceID(entry["file"].String(), EResType::CAMPAIGN);
-		if (!CResourceHandler::get()->existsResource(resourceID))
+		if(entry["optional"].Bool())
+			continue;
+		if(!CResourceHandler::get()->existsResource(resourceID))
 			campaignsFound = false;
 	}
 

+ 15 - 22
client/mapView/MapRenderer.cpp

@@ -316,7 +316,7 @@ uint8_t MapRendererBorder::checksum(IMapRendererContext & context, const int3 &
 MapRendererFow::MapRendererFow()
 {
 	fogOfWarFullHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRC"), EImageBlitMode::OPAQUE);
-	fogOfWarPartialHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRE"), EImageBlitMode::ALPHA);
+	fogOfWarPartialHide = GH.renderHandler().loadAnimation(AnimationPath::builtin("TSHRE"), EImageBlitMode::SIMPLE);
 
 	static const std::vector<int> rotations = {22, 15, 2, 13, 12, 16, 28, 17, 20, 19, 7, 24, 26, 25, 30, 32, 27};
 
@@ -383,24 +383,25 @@ std::shared_ptr<CAnimation> MapRendererObjects::getBaseAnimation(const CGObjectI
 	}
 
 	bool generateMovementGroups = (info->id == Obj::BOAT) || (info->id == Obj::HERO);
+	bool enableOverlay = obj->ID != Obj::BOAT && obj->ID != Obj::HERO && obj->getOwner() != PlayerColor::UNFLAGGABLE;
 
 	// Boat appearance files only contain single, unanimated image
 	// proper boat animations are actually in different file
 	if (info->id == Obj::BOAT)
 		if(auto boat = dynamic_cast<const CGBoat*>(obj); boat && !boat->actualAnimation.empty())
-			return getAnimation(boat->actualAnimation, generateMovementGroups);
+			return getAnimation(boat->actualAnimation, generateMovementGroups, enableOverlay);
 
-	return getAnimation(info->animationFile, generateMovementGroups);
+	return getAnimation(info->animationFile, generateMovementGroups, enableOverlay);
 }
 
-std::shared_ptr<CAnimation> MapRendererObjects::getAnimation(const AnimationPath & filename, bool generateMovementGroups)
+std::shared_ptr<CAnimation> MapRendererObjects::getAnimation(const AnimationPath & filename, bool generateMovementGroups, bool enableOverlay)
 {
 	auto it = animations.find(filename);
 
 	if(it != animations.end())
 		return it->second;
 
-	auto ret = GH.renderHandler().loadAnimation(filename, EImageBlitMode::ALPHA);
+	auto ret = GH.renderHandler().loadAnimation(filename, enableOverlay ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::WITH_SHADOW);
 	animations[filename] = ret;
 
 	if(generateMovementGroups)
@@ -427,14 +428,14 @@ std::shared_ptr<CAnimation> MapRendererObjects::getFlagAnimation(const CGObjectI
 	{
 		assert(dynamic_cast<const CGHeroInstance *>(obj) != nullptr);
 		assert(obj->tempOwner.isValidPlayer());
-		return getAnimation(AnimationPath::builtin(heroFlags[obj->tempOwner.getNum()]), true);
+		return getAnimation(AnimationPath::builtin(heroFlags[obj->tempOwner.getNum()]), true, false);
 	}
 
 	if(obj->ID == Obj::BOAT)
 	{
 		const auto * boat = dynamic_cast<const CGBoat *>(obj);
 		if(boat && boat->hero && !boat->flagAnimations[boat->hero->tempOwner.getNum()].empty())
-			return getAnimation(boat->flagAnimations[boat->hero->tempOwner.getNum()], true);
+			return getAnimation(boat->flagAnimations[boat->hero->tempOwner.getNum()], true, false);
 	}
 
 	return nullptr;
@@ -447,7 +448,7 @@ std::shared_ptr<CAnimation> MapRendererObjects::getOverlayAnimation(const CGObje
 		// Boats have additional animation with waves around boat
 		const auto * boat = dynamic_cast<const CGBoat *>(obj);
 		if(boat && boat->hero && !boat->overlayAnimation.empty())
-			return getAnimation(boat->overlayAnimation, true);
+			return getAnimation(boat->overlayAnimation, true, false);
 	}
 	return nullptr;
 }
@@ -478,22 +479,14 @@ void MapRendererObjects::renderImage(IMapRendererContext & context, Canvas & tar
 		return;
 
 	image->setAlpha(transparency);
-	image->setShadowEnabled(true);
-	if (object->ID != Obj::HERO)
+	if (object->ID != Obj::HERO) // heroes use separate image with flag instead of player-colored palette
 	{
-		image->setOverlayEnabled(object->getOwner().isValidPlayer() || object->getOwner() == PlayerColor::NEUTRAL);
-
 		if (object->getOwner().isValidPlayer())
 			image->setOverlayColor(graphics->playerColors[object->getOwner().getNum()]);
 
 		if (object->getOwner() == PlayerColor::NEUTRAL)
 			image->setOverlayColor(graphics->neutralColor);
 	}
-	else
-	{
-		// heroes use separate image with flag instead of player-colored palette
-		image->setOverlayEnabled(false);
-	}
 
 	Point offsetPixels = context.objectImageOffset(object->id, coordinates);
 
@@ -567,10 +560,10 @@ uint8_t MapRendererObjects::checksum(IMapRendererContext & context, const int3 &
 }
 
 MapRendererOverlay::MapRendererOverlay()
-	: imageGrid(GH.renderHandler().loadImage(ImagePath::builtin("debug/grid"), EImageBlitMode::ALPHA))
-	, imageBlocked(GH.renderHandler().loadImage(ImagePath::builtin("debug/blocked"), EImageBlitMode::ALPHA))
-	, imageVisitable(GH.renderHandler().loadImage(ImagePath::builtin("debug/visitable"), EImageBlitMode::ALPHA))
-	, imageSpellRange(GH.renderHandler().loadImage(ImagePath::builtin("debug/spellRange"), EImageBlitMode::ALPHA))
+	: imageGrid(GH.renderHandler().loadImage(ImagePath::builtin("debug/grid"), EImageBlitMode::COLORKEY))
+	, imageBlocked(GH.renderHandler().loadImage(ImagePath::builtin("debug/blocked"), EImageBlitMode::COLORKEY))
+	, imageVisitable(GH.renderHandler().loadImage(ImagePath::builtin("debug/visitable"), EImageBlitMode::COLORKEY))
+	, imageSpellRange(GH.renderHandler().loadImage(ImagePath::builtin("debug/spellRange"), EImageBlitMode::COLORKEY))
 {
 
 }
@@ -626,7 +619,7 @@ uint8_t MapRendererOverlay::checksum(IMapRendererContext & context, const int3 &
 }
 
 MapRendererPath::MapRendererPath()
-	: pathNodes(GH.renderHandler().loadAnimation(AnimationPath::builtin("ADAG"), EImageBlitMode::ALPHA))
+	: pathNodes(GH.renderHandler().loadAnimation(AnimationPath::builtin("ADAG"), EImageBlitMode::SIMPLE))
 {
 }
 

+ 1 - 1
client/mapView/MapRenderer.h

@@ -77,7 +77,7 @@ class MapRendererObjects
 	std::shared_ptr<CAnimation> getFlagAnimation(const CGObjectInstance * obj);
 	std::shared_ptr<CAnimation> getOverlayAnimation(const CGObjectInstance * obj);
 
-	std::shared_ptr<CAnimation> getAnimation(const AnimationPath & filename, bool generateMovementGroups);
+	std::shared_ptr<CAnimation> getAnimation(const AnimationPath & filename, bool generateMovementGroups, bool enableOverlay);
 
 	std::shared_ptr<IImage> getImage(IMapRendererContext & context, const CGObjectInstance * obj, const std::shared_ptr<CAnimation> & animation) const;
 

+ 2 - 3
client/mapView/MapRendererContextState.cpp

@@ -54,9 +54,8 @@ void MapRendererContextState::addObject(const CGObjectInstance * obj)
 			if(LOCPLINT->cb->isInTheMap(currTile) && obj->coveringAt(currTile))
 			{
 				auto & container = objects[currTile.z][currTile.x][currTile.y];
-
-				container.push_back(obj->id);
-				boost::range::sort(container, compareObjectBlitOrder);
+				auto position = std::upper_bound(container.begin(), container.end(), obj->id, compareObjectBlitOrder);
+				container.insert(position, obj->id);
 			}
 		}
 	}

+ 120 - 0
client/render/AssetGenerator.cpp

@@ -30,6 +30,8 @@ void AssetGenerator::generateAll()
 	for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
 		createPlayerColoredBackground(PlayerColor(i));
 	createCombatUnitNumberWindow();
+	createCampaignBackground();
+	createChroniclesCampaignImages();
 }
 
 void AssetGenerator::createAdventureOptionsCleanBackground()
@@ -206,3 +208,121 @@ void AssetGenerator::createCombatUnitNumberWindow()
 	texture->adjustPalette(shifterNeutral, ignoredMask);
 	texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePathNeutral));
 }
+
+void AssetGenerator::createCampaignBackground()
+{
+	std::string filename = "data/CampaignBackground8.png";
+
+	if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation
+		return;
+
+	if(!CResourceHandler::get("local")->createResource(filename))
+		return;
+	ResourcePath savePath(filename, EResType::IMAGE);
+
+	auto locator = ImageLocator(ImagePath::builtin("CAMPBACK"));
+	locator.scalingFactor = 1;
+
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
+	Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::IGNORE);
+	
+	canvas.draw(img, Point(0, 0), Rect(0, 0, 800, 600));
+
+	// left image
+	canvas.draw(img, Point(220, 73), Rect(290, 73, 141, 115));
+	canvas.draw(img, Point(37, 70), Rect(87, 70, 207, 120));
+
+	// right image
+	canvas.draw(img, Point(513, 67), Rect(463, 67, 71, 126));
+	canvas.draw(img, Point(586, 71), Rect(536, 71, 207, 117));
+
+	// middle image
+	canvas.draw(img, Point(306, 68), Rect(86, 68, 209, 122));
+
+	// disabled fields
+	canvas.draw(img, Point(40, 72), Rect(313, 74, 197, 114));
+	canvas.draw(img, Point(310, 72), Rect(313, 74, 197, 114));
+	canvas.draw(img, Point(590, 72), Rect(313, 74, 197, 114));
+	canvas.draw(img, Point(43, 245), Rect(313, 74, 197, 114));
+	canvas.draw(img, Point(313, 244), Rect(313, 74, 197, 114));
+	canvas.draw(img, Point(586, 246), Rect(313, 74, 197, 114));
+	canvas.draw(img, Point(34, 417), Rect(313, 74, 197, 114));
+	canvas.draw(img, Point(404, 414), Rect(313, 74, 197, 114));
+
+	// skull
+	auto locatorSkull = ImageLocator(ImagePath::builtin("CAMPNOSC"));
+	locatorSkull.scalingFactor = 1;
+	std::shared_ptr<IImage> imgSkull = GH.renderHandler().loadImage(locatorSkull, EImageBlitMode::OPAQUE);
+	canvas.draw(imgSkull, Point(562, 509), Rect(178, 108, 43, 19));
+
+	std::shared_ptr<IImage> image = GH.renderHandler().createImage(canvas.getInternalSurface());
+
+	image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
+}
+
+void AssetGenerator::createChroniclesCampaignImages()
+{
+	for(int i = 1; i < 9; i++)
+	{
+		std::string filename = "data/CampaignHc" + std::to_string(i) + "Image.png";
+
+		if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation
+			continue;
+			
+		auto imgPathBg = ImagePath::builtin("data/chronicles_" + std::to_string(i) + "/GamSelBk");
+		if(!CResourceHandler::get()->existsResource(imgPathBg)) // Chronicle episode not installed
+			continue;
+
+		if(!CResourceHandler::get("local")->createResource(filename))
+			continue;
+		ResourcePath savePath(filename, EResType::IMAGE);
+
+		auto locator = ImageLocator(imgPathBg);
+		locator.scalingFactor = 1;
+
+		std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
+		Canvas canvas = Canvas(Point(200, 116), CanvasScalingPolicy::IGNORE);
+		
+		switch (i)
+		{
+		case 1:
+			canvas.draw(img, Point(0, 0), Rect(149, 144, 200, 116));
+			break;
+		case 2:
+			canvas.draw(img, Point(0, 0), Rect(156, 150, 200, 116));
+			break;
+		case 3:
+			canvas.draw(img, Point(0, 0), Rect(171, 153, 200, 116));
+			break;
+		case 4:
+			canvas.draw(img, Point(0, 0), Rect(35, 358, 200, 116));
+			break;
+		case 5:
+			canvas.draw(img, Point(0, 0), Rect(216, 248, 200, 116));
+			break;
+		case 6:
+			canvas.draw(img, Point(0, 0), Rect(58, 234, 200, 116));
+			break;
+		case 7:
+			canvas.draw(img, Point(0, 0), Rect(184, 219, 200, 116));
+			break;
+		case 8:
+			canvas.draw(img, Point(0, 0), Rect(268, 210, 200, 116));
+
+			//skull
+			auto locatorSkull = ImageLocator(ImagePath::builtin("CampSP1"));
+			locatorSkull.scalingFactor = 1;
+			std::shared_ptr<IImage> imgSkull = GH.renderHandler().loadImage(locatorSkull, EImageBlitMode::OPAQUE);
+			canvas.draw(imgSkull, Point(162, 94), Rect(162, 94, 41, 22));
+			canvas.draw(img, Point(162, 94), Rect(424, 304, 14, 4));
+			canvas.draw(img, Point(162, 98), Rect(424, 308, 10, 4));
+			canvas.draw(img, Point(158, 102), Rect(424, 312, 10, 4));
+			canvas.draw(img, Point(154, 106), Rect(424, 316, 10, 4));
+			break;
+		}
+
+		std::shared_ptr<IImage> image = GH.renderHandler().createImage(canvas.getInternalSurface());
+
+		image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
+	}
+}

+ 2 - 0
client/render/AssetGenerator.h

@@ -21,4 +21,6 @@ public:
 	static void createBigSpellBook();
 	static void createPlayerColoredBackground(const PlayerColor & player);
 	static void createCombatUnitNumberWindow();
+	static void createCampaignBackground();
+	static void createChroniclesCampaignImages();
 };

+ 29 - 12
client/render/IImage.h

@@ -37,9 +37,29 @@ enum class EImageBlitMode : uint8_t
 	/// RGBA: full alpha transparency range, e.g. shadows
 	COLORKEY,
 
-	/// Should be avoided if possible, use only for images that use def's with semi-transparency
-	/// Indexed or RGBA: Image might have full alpha transparency range, e.g. shadows
-	ALPHA
+	/// Full transparency including shadow, but treated as a single image
+	/// Indexed: Image can have alpha transparency, e.g. shadow
+	/// RGBA: full alpha transparency range, e.g. shadows
+	/// Upscaled form: single image, no option to display shadow separately
+	SIMPLE,
+
+	/// RGBA, may consist from 2 separate parts: base and shadow, overlay not preset or treated as part of body
+	WITH_SHADOW,
+
+	/// RGBA, may consist from 3 separate parts: base, shadow, and overlay
+	WITH_SHADOW_AND_OVERLAY,
+
+	/// RGBA, contains only body, with shadow and overlay disabled
+	ONLY_BODY,
+
+	/// RGBA, contains only body, with shadow disabled and overlay treated as part of body
+	ONLY_BODY_IGNORE_OVERLAY,
+
+	/// RGBA, contains only shadow
+	ONLY_SHADOW,
+
+	/// RGBA, contains only overlay
+	ONLY_OVERLAY,
 };
 
 /// Base class for images for use in client code.
@@ -75,10 +95,7 @@ public:
 	//only indexed bitmaps with 7 special colors
 	virtual void setOverlayColor(const ColorRGBA & color) = 0;
 
-	virtual void setShadowEnabled(bool on) = 0;
-	virtual void setBodyEnabled(bool on) = 0;
-	virtual void setOverlayEnabled(bool on) = 0;
-	virtual std::shared_ptr<ISharedImage> getSharedImage() const = 0;
+	virtual std::shared_ptr<const ISharedImage> getSharedImage() const = 0;
 
 	virtual ~IImage() = default;
 };
@@ -94,12 +111,12 @@ public:
 	virtual bool isTransparent(const Point & coords) const = 0;
 	virtual void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const = 0;
 
-	virtual std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) = 0;
+	virtual std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const = 0;
 
-	virtual std::shared_ptr<ISharedImage> horizontalFlip() const = 0;
-	virtual std::shared_ptr<ISharedImage> verticalFlip() const = 0;
-	virtual std::shared_ptr<ISharedImage> scaleInteger(int factor, SDL_Palette * palette) const = 0;
-	virtual std::shared_ptr<ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const = 0;
+	virtual std::shared_ptr<const ISharedImage> horizontalFlip() const = 0;
+	virtual std::shared_ptr<const ISharedImage> verticalFlip() const = 0;
+	virtual std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette) const = 0;
+	virtual std::shared_ptr<const ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const = 0;
 
 
 	virtual ~ISharedImage() = default;

+ 7 - 2
client/render/ImageLocator.cpp

@@ -71,6 +71,7 @@ ImageLocator ImageLocator::copyFile() const
 {
 	ImageLocator result;
 	result.scalingFactor = 1;
+	result.preScaledFactor = preScaledFactor;
 	result.image = image;
 	result.defFile = defFile;
 	result.defFrame = defFrame;
@@ -123,8 +124,12 @@ std::string ImageLocator::toString() const
 	if (playerColored.isValidPlayer())
 		result += "-player" + playerColored.toString();
 
-	if (layer != EImageLayer::ALL)
-		result += "-layer" + std::to_string(static_cast<int>(layer));
+	if (layer == EImageBlitMode::ONLY_OVERLAY)
+		result += "-overlay";
+
+	if (layer == EImageBlitMode::ONLY_SHADOW)
+		result += "-shadow";
+
 
 	return result;
 }

+ 5 - 11
client/render/ImageLocator.h

@@ -9,18 +9,11 @@
  */
 #pragma once
 
+#include "IImage.h"
+
 #include "../../lib/filesystem/ResourcePath.h"
 #include "../../lib/constants/EntityIdentifiers.h"
 
-enum class EImageLayer
-{
-	ALL,
-
-	BODY,
-	SHADOW,
-	OVERLAY,
-};
-
 struct ImageLocator
 {
 	std::optional<ImagePath> image;
@@ -28,12 +21,13 @@ struct ImageLocator
 	int defFrame = -1;
 	int defGroup = -1;
 
-	PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE;
+	PlayerColor playerColored = PlayerColor::CANNOT_DETERMINE; // FIXME: treat as identical to blue to avoid double-loading?
 
 	bool verticalFlip = false;
 	bool horizontalFlip = false;
 	int8_t scalingFactor = 0; // 0 = auto / use default scaling
-	EImageLayer layer = EImageLayer::ALL;
+	int8_t preScaledFactor = 1;
+	EImageBlitMode layer = EImageBlitMode::OPAQUE;
 
 	ImageLocator() = default;
 	ImageLocator(const AnimationPath & path, int frame, int group);

+ 54 - 35
client/renderSDL/ImageScaled.cpp

@@ -21,19 +21,17 @@
 
 #include <SDL_surface.h>
 
-ImageScaled::ImageScaled(const ImageLocator & inputLocator, const std::shared_ptr<ISharedImage> & source, EImageBlitMode mode)
+ImageScaled::ImageScaled(const ImageLocator & inputLocator, const std::shared_ptr<const ISharedImage> & source, EImageBlitMode mode)
 	: source(source)
 	, locator(inputLocator)
 	, colorMultiplier(Colors::WHITE_TRUE)
 	, alphaValue(SDL_ALPHA_OPAQUE)
 	, blitMode(mode)
 {
-	setBodyEnabled(true);
-	if (mode == EImageBlitMode::ALPHA)
-		setShadowEnabled(true);
+	prepareImages();
 }
 
-std::shared_ptr<ISharedImage> ImageScaled::getSharedImage() const
+std::shared_ptr<const ISharedImage> ImageScaled::getSharedImage() const
 {
 	return body;
 }
@@ -92,8 +90,7 @@ void ImageScaled::setOverlayColor(const ColorRGBA & color)
 void ImageScaled::playerColored(PlayerColor player)
 {
 	playerColor = player;
-	if (body)
-		setBodyEnabled(true); // regenerate
+	prepareImages();
 }
 
 void ImageScaled::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove)
@@ -106,41 +103,63 @@ void ImageScaled::adjustPalette(const ColorFilter &shifter, uint32_t colorsToSki
 	// TODO: implement
 }
 
-void ImageScaled::setShadowEnabled(bool on)
+void ImageScaled::prepareImages()
 {
-	assert(blitMode == EImageBlitMode::ALPHA);
-	if (on)
+	switch(blitMode)
 	{
-		locator.layer = EImageLayer::SHADOW;
-		locator.playerColored = PlayerColor::CANNOT_DETERMINE;
-		shadow = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+		case EImageBlitMode::OPAQUE:
+		case EImageBlitMode::COLORKEY:
+		case EImageBlitMode::SIMPLE:
+			locator.layer = blitMode;
+			locator.playerColored = playerColor;
+			body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+			break;
+
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+		case EImageBlitMode::ONLY_BODY:
+			locator.layer = EImageBlitMode::ONLY_BODY;
+			locator.playerColored = playerColor;
+			body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+			break;
+
+		case EImageBlitMode::WITH_SHADOW:
+		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
+			locator.layer = EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY;
+			locator.playerColored = playerColor;
+			body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+			break;
+
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::ONLY_OVERLAY:
+			body = nullptr;
+			break;
 	}
-	else
-		shadow = nullptr;
-}
 
-void ImageScaled::setBodyEnabled(bool on)
-{
-	if (on)
+	switch(blitMode)
 	{
-		locator.layer = blitMode == EImageBlitMode::ALPHA ? EImageLayer::BODY : EImageLayer::ALL;
-		locator.playerColored = playerColor;
-		body = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+		case EImageBlitMode::SIMPLE:
+		case EImageBlitMode::WITH_SHADOW:
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			locator.layer = EImageBlitMode::ONLY_SHADOW;
+			locator.playerColored = PlayerColor::CANNOT_DETERMINE;
+			shadow = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+			break;
+		default:
+			shadow = nullptr;
+			break;
 	}
-	else
-		body = nullptr;
-}
-
 
-void ImageScaled::setOverlayEnabled(bool on)
-{
-	assert(blitMode == EImageBlitMode::ALPHA);
-	if (on)
+	switch(blitMode)
 	{
-		locator.layer = EImageLayer::OVERLAY;
-		locator.playerColored = PlayerColor::CANNOT_DETERMINE;
-		overlay = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+		case EImageBlitMode::ONLY_OVERLAY:
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			locator.layer = EImageBlitMode::ONLY_OVERLAY;
+			locator.playerColored = PlayerColor::CANNOT_DETERMINE;
+			overlay = GH.renderHandler().loadImage(locator, blitMode)->getSharedImage();
+			break;
+		default:
+			overlay = nullptr;
+			break;
 	}
-	else
-		overlay = nullptr;
 }

+ 7 - 9
client/renderSDL/ImageScaled.h

@@ -25,16 +25,16 @@ class ImageScaled final : public IImage
 private:
 
 	/// Original unscaled image
-	std::shared_ptr<ISharedImage> source;
+	std::shared_ptr<const ISharedImage> source;
 
 	/// Upscaled shadow of our image, may be null
-	std::shared_ptr<ISharedImage> shadow;
+	std::shared_ptr<const ISharedImage> shadow;
 
 	/// Upscaled main part of our image, may be null
-	std::shared_ptr<ISharedImage> body;
+	std::shared_ptr<const ISharedImage> body;
 
 	/// Upscaled overlay (player color, selection highlight) of our image, may be null
-	std::shared_ptr<ISharedImage> overlay;
+	std::shared_ptr<const ISharedImage> overlay;
 
 	ImageLocator locator;
 
@@ -44,8 +44,9 @@ private:
 	uint8_t alphaValue;
 	EImageBlitMode blitMode;
 
+	void prepareImages();
 public:
-	ImageScaled(const ImageLocator & locator, const std::shared_ptr<ISharedImage> & source, EImageBlitMode mode);
+	ImageScaled(const ImageLocator & locator, const std::shared_ptr<const ISharedImage> & source, EImageBlitMode mode);
 
 	void scaleInteger(int factor) override;
 	void scaleTo(const Point & size) override;
@@ -60,8 +61,5 @@ public:
 	void shiftPalette(uint32_t firstColorID, uint32_t colorsToMove, uint32_t distanceToMove) override;
 	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;
 
-	void setShadowEnabled(bool on) override;
-	void setBodyEnabled(bool on) override;
-	void setOverlayEnabled(bool on) override;
-	std::shared_ptr<ISharedImage> getSharedImage() const override;
+	std::shared_ptr<const ISharedImage> getSharedImage() const override;
 };

+ 120 - 30
client/renderSDL/RenderHandler.cpp

@@ -55,6 +55,59 @@ std::shared_ptr<CDefFile> RenderHandler::getAnimationFile(const AnimationPath &
 	return result;
 }
 
+std::optional<ResourcePath> RenderHandler::getPathForScaleFactor(ResourcePath path, std::string factor)
+{
+	if(path.getType() == EResType::IMAGE)
+	{
+		auto p = ImagePath::builtin(path.getName());
+		if(CResourceHandler::get()->existsResource(p.addPrefix("SPRITES" + factor + "X/")))
+			return std::optional<ResourcePath>(p.addPrefix("SPRITES" + factor + "X/"));
+		if(CResourceHandler::get()->existsResource(p.addPrefix("DATA" + factor + "X/")))
+			return std::optional<ResourcePath>(p.addPrefix("DATA" + factor + "X/"));
+	}
+	else
+	{
+		auto p = AnimationPath::builtin(path.getName());
+		auto pJson = p.toType<EResType::JSON>();
+		if(CResourceHandler::get()->existsResource(p.addPrefix("SPRITES" + factor + "X/")))
+			return std::optional<ResourcePath>(p.addPrefix("SPRITES" + factor + "X/"));
+		if(CResourceHandler::get()->existsResource(pJson))
+			return std::optional<ResourcePath>(p);
+		if(CResourceHandler::get()->existsResource(pJson.addPrefix("SPRITES" + factor + "X/")))
+			return std::optional<ResourcePath>(p.addPrefix("SPRITES" + factor + "X/"));
+	}
+
+	return std::nullopt;
+}
+
+std::pair<ResourcePath, int> RenderHandler::getScalePath(ResourcePath p)
+{
+	auto path = p;
+	int scaleFactor = 1;
+	if(getScalingFactor() > 1)
+	{
+		std::vector<int> factorsToCheck = {getScalingFactor(), 4, 3, 2};
+		for(auto factorToCheck : factorsToCheck)
+		{
+			std::string name = boost::algorithm::to_upper_copy(p.getName());
+			boost::replace_all(name, "SPRITES/", std::string("SPRITES") + std::to_string(factorToCheck) + std::string("X/"));
+			boost::replace_all(name, "DATA/", std::string("DATA") + std::to_string(factorToCheck) + std::string("X/"));
+			ResourcePath scaledPath = ImagePath::builtin(name);
+			if(p.getType() != EResType::IMAGE)
+				scaledPath = AnimationPath::builtin(name);
+			auto tmpPath = getPathForScaleFactor(scaledPath, std::to_string(factorToCheck));
+			if(tmpPath)
+			{
+				path = *tmpPath;
+				scaleFactor = factorToCheck;
+				break;
+			}
+		}
+	}
+
+	return std::pair<ResourcePath, int>(path, scaleFactor);
+};
+
 void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & config)
 {
 	std::string basepath;
@@ -96,7 +149,9 @@ void RenderHandler::initFromJson(AnimationLayoutMap & source, const JsonNode & c
 
 RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const AnimationPath & path)
 {
-	AnimationPath actualPath = boost::starts_with(path.getName(), "SPRITES") ? path : path.addPrefix("SPRITES/");
+	auto tmp = getScalePath(path);
+	auto animPath = AnimationPath::builtin(tmp.first.getName());
+	AnimationPath actualPath = boost::starts_with(animPath.getName(), "SPRITES") ? animPath : animPath.addPrefix("SPRITES/");
 
 	auto it = animationLayouts.find(actualPath);
 
@@ -123,11 +178,15 @@ RenderHandler::AnimationLayoutMap & RenderHandler::getAnimationLayout(const Anim
 		std::unique_ptr<ui8[]> textData(new ui8[stream->getSize()]);
 		stream->read(textData.get(), stream->getSize());
 
-		const JsonNode config(reinterpret_cast<const std::byte*>(textData.get()), stream->getSize(), path.getOriginalName());
+		const JsonNode config(reinterpret_cast<const std::byte*>(textData.get()), stream->getSize(), animPath.getOriginalName());
 
 		initFromJson(result, config);
 	}
 
+	for(auto & g : result)
+		for(auto & i : g.second)
+			i.preScaledFactor = tmp.second;
+
 	animationLayouts[actualPath] = result;
 	return animationLayouts[actualPath];
 }
@@ -153,7 +212,7 @@ ImageLocator RenderHandler::getLocatorForAnimationFrame(const AnimationPath & pa
 	return ImageLocator(path, frame, group);
 }
 
-std::shared_ptr<ISharedImage> RenderHandler::loadImageImpl(const ImageLocator & locator)
+std::shared_ptr<const ISharedImage> RenderHandler::loadImageImpl(const ImageLocator & locator)
 {
 	auto it = imageFiles.find(locator);
 	if (it != imageFiles.end())
@@ -172,24 +231,34 @@ std::shared_ptr<ISharedImage> RenderHandler::loadImageImpl(const ImageLocator &
 	return scaledImage;
 }
 
-std::shared_ptr<ISharedImage> RenderHandler::loadImageFromFileUncached(const ImageLocator & locator)
+std::shared_ptr<const ISharedImage> RenderHandler::loadImageFromFileUncached(const ImageLocator & locator)
 {
 	if (locator.image)
 	{
 		// TODO: create EmptySharedImage class that will be instantiated if image does not exists or fails to load
-		return std::make_shared<SDLImageShared>(*locator.image);
+		return std::make_shared<SDLImageShared>(*locator.image, locator.preScaledFactor);
 	}
 
 	if (locator.defFile)
 	{
 		auto defFile = getAnimationFile(*locator.defFile);
-		return std::make_shared<SDLImageShared>(defFile.get(), locator.defFrame, locator.defGroup);
+		int preScaledFactor = locator.preScaledFactor;
+		if(!defFile) // no prescale for this frame
+		{
+			auto tmpPath = (*locator.defFile).getName();
+			boost::algorithm::replace_all(tmpPath, "SPRITES2X/", "SPRITES/");
+			boost::algorithm::replace_all(tmpPath, "SPRITES3X/", "SPRITES/");
+			boost::algorithm::replace_all(tmpPath, "SPRITES4X/", "SPRITES/");
+			preScaledFactor = 1;
+			defFile = getAnimationFile(AnimationPath::builtin(tmpPath));
+		}
+		return std::make_shared<SDLImageShared>(defFile.get(), locator.defFrame, locator.defGroup, preScaledFactor);
 	}
 
 	throw std::runtime_error("Invalid image locator received!");
 }
 
-void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_ptr<ISharedImage> image)
+void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image)
 {
 	imageFiles[locator] = image;
 
@@ -202,7 +271,7 @@ void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_p
 #endif
 }
 
-std::shared_ptr<ISharedImage> RenderHandler::loadImageFromFile(const ImageLocator & locator)
+std::shared_ptr<const ISharedImage> RenderHandler::loadImageFromFile(const ImageLocator & locator)
 {
 	if (imageFiles.count(locator))
 		return imageFiles.at(locator);
@@ -212,7 +281,7 @@ std::shared_ptr<ISharedImage> RenderHandler::loadImageFromFile(const ImageLocato
 	return result;
 }
 
-std::shared_ptr<ISharedImage> RenderHandler::transformImage(const ImageLocator & locator, std::shared_ptr<ISharedImage> image)
+std::shared_ptr<const ISharedImage> RenderHandler::transformImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image)
 {
 	if (imageFiles.count(locator))
 		return imageFiles.at(locator);
@@ -229,27 +298,19 @@ std::shared_ptr<ISharedImage> RenderHandler::transformImage(const ImageLocator &
 	return result;
 }
 
-std::shared_ptr<ISharedImage> RenderHandler::scaleImage(const ImageLocator & locator, std::shared_ptr<ISharedImage> image)
+std::shared_ptr<const ISharedImage> RenderHandler::scaleImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image)
 {
 	if (imageFiles.count(locator))
 		return imageFiles.at(locator);
 
-	auto handle = image->createImageReference(locator.layer == EImageLayer::ALL ? EImageBlitMode::OPAQUE : EImageBlitMode::ALPHA);
+	auto handle = image->createImageReference(locator.layer);
 
 	assert(locator.scalingFactor != 1); // should be filtered-out before
-
-	handle->setBodyEnabled(locator.layer == EImageLayer::ALL || locator.layer == EImageLayer::BODY);
-	if (locator.layer != EImageLayer::ALL)
-	{
-		handle->setOverlayEnabled(locator.layer == EImageLayer::OVERLAY);
-		handle->setShadowEnabled( locator.layer == EImageLayer::SHADOW);
-	}
-	if (locator.layer == EImageLayer::ALL && locator.playerColored != PlayerColor::CANNOT_DETERMINE)
+	if (locator.playerColored != PlayerColor::CANNOT_DETERMINE)
 		handle->playerColored(locator.playerColored);
 
 	handle->scaleInteger(locator.scalingFactor);
 
-	// TODO: try to optimize image size (possibly even before scaling?) - trim image borders if they are completely transparent
 	auto result = handle->getSharedImage();
 	storeCachedImage(locator, result);
 	return result;
@@ -257,10 +318,39 @@ std::shared_ptr<ISharedImage> RenderHandler::scaleImage(const ImageLocator & loc
 
 std::shared_ptr<IImage> RenderHandler::loadImage(const ImageLocator & locator, EImageBlitMode mode)
 {
-	if (locator.scalingFactor == 0 && getScalingFactor() != 1 )
+	ImageLocator adjustedLocator = locator;
+
+	if(adjustedLocator.image)
 	{
-		auto unscaledLocator = locator;
-		auto scaledLocator = locator;
+		std::string imgPath = (*adjustedLocator.image).getName();
+		if(adjustedLocator.layer == EImageBlitMode::ONLY_OVERLAY)
+			imgPath += "-OVERLAY";
+		if(adjustedLocator.layer == EImageBlitMode::ONLY_SHADOW)
+			imgPath += "-SHADOW";
+
+		if(CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath)) ||
+		   CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath).addPrefix("DATA/")) ||
+		   CResourceHandler::get()->existsResource(ImagePath::builtin(imgPath).addPrefix("SPRITES/")))
+			adjustedLocator.image = ImagePath::builtin(imgPath);
+	}
+
+	if(adjustedLocator.defFile && adjustedLocator.scalingFactor == 0)
+	{
+		auto tmp = getScalePath(*adjustedLocator.defFile);
+		adjustedLocator.defFile = AnimationPath::builtin(tmp.first.getName());
+		adjustedLocator.preScaledFactor = tmp.second;
+	}
+	if(adjustedLocator.image && adjustedLocator.scalingFactor == 0)
+	{
+		auto tmp = getScalePath(*adjustedLocator.image);
+		adjustedLocator.image = ImagePath::builtin(tmp.first.getName());
+		adjustedLocator.preScaledFactor = tmp.second;
+	}
+
+	if (adjustedLocator.scalingFactor == 0 && getScalingFactor() != 1 )
+	{
+		auto unscaledLocator = adjustedLocator;
+		auto scaledLocator = adjustedLocator;
 
 		unscaledLocator.scalingFactor = 1;
 		scaledLocator.scalingFactor = getScalingFactor();
@@ -269,22 +359,22 @@ std::shared_ptr<IImage> RenderHandler::loadImage(const ImageLocator & locator, E
 		return std::make_shared<ImageScaled>(scaledLocator, unscaledImage, mode);
 	}
 
-	if (locator.scalingFactor == 0)
+	if (adjustedLocator.scalingFactor == 0)
 	{
-		auto scaledLocator = locator;
+		auto scaledLocator = adjustedLocator;
 		scaledLocator.scalingFactor = getScalingFactor();
 
 		return loadImageImpl(scaledLocator)->createImageReference(mode);
 	}
 	else
-	{
-		return loadImageImpl(locator)->createImageReference(mode);
-	}
+		return loadImageImpl(adjustedLocator)->createImageReference(mode);
 }
 
 std::shared_ptr<IImage> RenderHandler::loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode)
 {
-	ImageLocator locator = getLocatorForAnimationFrame(path, frame, group);
+	auto tmp = getScalePath(path);
+	ImageLocator locator = getLocatorForAnimationFrame(AnimationPath::builtin(tmp.first.getName()), frame, group);
+	locator.preScaledFactor = tmp.second;
 	return loadImage(locator, mode);
 }
 
@@ -296,7 +386,7 @@ std::shared_ptr<IImage> RenderHandler::loadImage(const ImagePath & path, EImageB
 
 std::shared_ptr<IImage> RenderHandler::createImage(SDL_Surface * source)
 {
-	return std::make_shared<SDLImageShared>(source)->createImageReference(EImageBlitMode::ALPHA);
+	return std::make_shared<SDLImageShared>(source)->createImageReference(EImageBlitMode::SIMPLE);
 }
 
 std::shared_ptr<CAnimation> RenderHandler::loadAnimation(const AnimationPath & path, EImageBlitMode mode)

+ 9 - 7
client/renderSDL/RenderHandler.h

@@ -25,24 +25,26 @@ class RenderHandler : public IRenderHandler
 
 	std::map<AnimationPath, std::shared_ptr<CDefFile>> animationFiles;
 	std::map<AnimationPath, AnimationLayoutMap> animationLayouts;
-	std::map<ImageLocator, std::shared_ptr<ISharedImage>> imageFiles;
+	std::map<ImageLocator, std::shared_ptr<const ISharedImage>> imageFiles;
 	std::map<EFonts, std::shared_ptr<const IFont>> fonts;
 
 	std::shared_ptr<CDefFile> getAnimationFile(const AnimationPath & path);
+	std::optional<ResourcePath> getPathForScaleFactor(ResourcePath path, std::string factor);
+	std::pair<ResourcePath, int> getScalePath(ResourcePath p);
 	AnimationLayoutMap & getAnimationLayout(const AnimationPath & path);
 	void initFromJson(AnimationLayoutMap & layout, const JsonNode & config);
 
 	void addImageListEntry(size_t index, size_t group, const std::string & listName, const std::string & imageName);
 	void addImageListEntries(const EntityService * service);
-	void storeCachedImage(const ImageLocator & locator, std::shared_ptr<ISharedImage> image);
+	void storeCachedImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image);
 
-	std::shared_ptr<ISharedImage> loadImageImpl(const ImageLocator & config);
+	std::shared_ptr<const ISharedImage> loadImageImpl(const ImageLocator & config);
 
-	std::shared_ptr<ISharedImage> loadImageFromFileUncached(const ImageLocator & locator);
-	std::shared_ptr<ISharedImage> loadImageFromFile(const ImageLocator & locator);
+	std::shared_ptr<const ISharedImage> loadImageFromFileUncached(const ImageLocator & locator);
+	std::shared_ptr<const ISharedImage> loadImageFromFile(const ImageLocator & locator);
 
-	std::shared_ptr<ISharedImage> transformImage(const ImageLocator & locator, std::shared_ptr<ISharedImage> image);
-	std::shared_ptr<ISharedImage> scaleImage(const ImageLocator & locator, std::shared_ptr<ISharedImage> image);
+	std::shared_ptr<const ISharedImage> transformImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image);
+	std::shared_ptr<const ISharedImage> scaleImage(const ImageLocator & locator, std::shared_ptr<const ISharedImage> image);
 
 	ImageLocator getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group);
 

+ 77 - 73
client/renderSDL/SDLImage.cpp

@@ -89,11 +89,12 @@ int IImage::height() const
 	return dimensions().y;
 }
 
-SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group)
+SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group, int preScaleFactor)
 	: surf(nullptr),
 	margins(0, 0),
 	fullSize(0, 0),
-	originalPalette(nullptr)
+	originalPalette(nullptr),
+	preScaleFactor(preScaleFactor)
 {
 	SDLImageLoader loader(this);
 	data->loadFrame(frame, group, loader);
@@ -101,11 +102,12 @@ SDLImageShared::SDLImageShared(const CDefFile * data, size_t frame, size_t group
 	savePalette();
 }
 
-SDLImageShared::SDLImageShared(SDL_Surface * from)
+SDLImageShared::SDLImageShared(SDL_Surface * from, int preScaleFactor)
 	: surf(nullptr),
 	margins(0, 0),
 	fullSize(0, 0),
-	originalPalette(nullptr)
+	originalPalette(nullptr),
+	preScaleFactor(preScaleFactor)
 {
 	surf = from;
 	if (surf == nullptr)
@@ -118,11 +120,12 @@ SDLImageShared::SDLImageShared(SDL_Surface * from)
 	fullSize.y = surf->h;
 }
 
-SDLImageShared::SDLImageShared(const ImagePath & filename)
+SDLImageShared::SDLImageShared(const ImagePath & filename, int preScaleFactor)
 	: surf(nullptr),
 	margins(0, 0),
 	fullSize(0, 0),
-	originalPalette(nullptr)
+	originalPalette(nullptr),
+	preScaleFactor(preScaleFactor)
 {
 	surf = BitmapHandler::loadBitmap(filename);
 
@@ -136,6 +139,8 @@ SDLImageShared::SDLImageShared(const ImagePath & filename)
 		savePalette();
 		fullSize.x = surf->w;
 		fullSize.y = surf->h;
+
+		optimizeSurface();
 	}
 }
 
@@ -177,7 +182,7 @@ void SDLImageShared::draw(SDL_Surface * where, SDL_Palette * palette, const Poin
 	if (palette && surf->format->palette)
 		SDL_SetSurfacePalette(surf, palette);
 
-	if(surf->format->palette && mode == EImageBlitMode::ALPHA)
+	if(surf->format->palette && mode != EImageBlitMode::OPAQUE && mode != EImageBlitMode::COLORKEY)
 	{
 		CSDL_Ext::blit8bppAlphaTo24bpp(surf, sourceRect, where, destShift, alpha);
 	}
@@ -258,6 +263,13 @@ void SDLImageShared::optimizeSurface()
 		SDL_SetSurfaceBlendMode(surf, SDL_BLENDMODE_NONE);
 		SDL_BlitSurface(surf, &rectSDL, newSurface, nullptr);
 
+		if (SDL_HasColorKey(surf))
+		{
+			uint32_t colorKey;
+			SDL_GetColorKey(surf, &colorKey);
+			SDL_SetColorKey(newSurface, SDL_TRUE, colorKey);
+		}
+
 		SDL_FreeSurface(surf);
 		surf = newSurface;
 
@@ -266,7 +278,7 @@ void SDLImageShared::optimizeSurface()
 	}
 }
 
-std::shared_ptr<ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palette * palette) const
+std::shared_ptr<const ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palette * palette) const
 {
 	if (factor <= 0)
 		throw std::runtime_error("Unable to scale by integer value of " + std::to_string(factor));
@@ -274,9 +286,15 @@ std::shared_ptr<ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palet
 	if (palette && surf && surf->format->palette)
 		SDL_SetSurfacePalette(surf, palette);
 
-	SDL_Surface * scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ);
+	SDL_Surface * scaled = nullptr;
+	if(preScaleFactor == factor)
+		return shared_from_this();
+	else if(preScaleFactor == 1)
+		scaled = CSDL_Ext::scaleSurfaceIntegerFactor(surf, factor, EScalingAlgorithm::XBRZ);
+	else
+		scaled = CSDL_Ext::scaleSurface(surf, (surf->w / preScaleFactor) * factor, (surf->h / preScaleFactor) * factor);
 
-	auto ret = std::make_shared<SDLImageShared>(scaled);
+	auto ret = std::make_shared<SDLImageShared>(scaled, preScaleFactor);
 
 	ret->fullSize.x = fullSize.x * factor;
 	ret->fullSize.y = fullSize.y * factor;
@@ -294,10 +312,10 @@ std::shared_ptr<ISharedImage> SDLImageShared::scaleInteger(int factor, SDL_Palet
 	return ret;
 }
 
-std::shared_ptr<ISharedImage> SDLImageShared::scaleTo(const Point & size, SDL_Palette * palette) const
+std::shared_ptr<const ISharedImage> SDLImageShared::scaleTo(const Point & size, SDL_Palette * palette) const
 {
-	float scaleX = float(size.x) / dimensions().x;
-	float scaleY = float(size.y) / dimensions().y;
+	float scaleX = float(size.x) / fullSize.x;
+	float scaleY = float(size.y) / fullSize.y;
 
 	if (palette && surf->format->palette)
 		SDL_SetSurfacePalette(surf, palette);
@@ -311,7 +329,7 @@ std::shared_ptr<ISharedImage> SDLImageShared::scaleTo(const Point & size, SDL_Pa
 	else
 		CSDL_Ext::setDefaultColorKey(scaled);//just in case
 
-	auto ret = std::make_shared<SDLImageShared>(scaled);
+	auto ret = std::make_shared<SDLImageShared>(scaled, preScaleFactor);
 
 	ret->fullSize.x = (int) round((float)fullSize.x * scaleX);
 	ret->fullSize.y = (int) round((float)fullSize.y * scaleY);
@@ -348,17 +366,17 @@ void SDLImageIndexed::playerColored(PlayerColor player)
 bool SDLImageShared::isTransparent(const Point & coords) const
 {
 	if (surf)
-		return CSDL_Ext::isTransparent(surf, coords.x, coords.y);
+		return CSDL_Ext::isTransparent(surf, coords.x - margins.x, coords.y	- margins.y);
 	else
 		return true;
 }
 
 Point SDLImageShared::dimensions() const
 {
-	return fullSize;
+	return fullSize / preScaleFactor;
 }
 
-std::shared_ptr<IImage> SDLImageShared::createImageReference(EImageBlitMode mode)
+std::shared_ptr<IImage> SDLImageShared::createImageReference(EImageBlitMode mode) const
 {
 	if (surf && surf->format->palette)
 		return std::make_shared<SDLImageIndexed>(shared_from_this(), originalPalette, mode);
@@ -366,10 +384,10 @@ std::shared_ptr<IImage> SDLImageShared::createImageReference(EImageBlitMode mode
 		return std::make_shared<SDLImageRGB>(shared_from_this(), mode);
 }
 
-std::shared_ptr<ISharedImage> SDLImageShared::horizontalFlip() const
+std::shared_ptr<const ISharedImage> SDLImageShared::horizontalFlip() const
 {
 	SDL_Surface * flipped = CSDL_Ext::horizontalFlip(surf);
-	auto ret = std::make_shared<SDLImageShared>(flipped);
+	auto ret = std::make_shared<SDLImageShared>(flipped, preScaleFactor);
 	ret->fullSize = fullSize;
 	ret->margins.x = margins.x;
 	ret->margins.y = fullSize.y - surf->h - margins.y;
@@ -378,10 +396,10 @@ std::shared_ptr<ISharedImage> SDLImageShared::horizontalFlip() const
 	return ret;
 }
 
-std::shared_ptr<ISharedImage> SDLImageShared::verticalFlip() const
+std::shared_ptr<const ISharedImage> SDLImageShared::verticalFlip() const
 {
 	SDL_Surface * flipped = CSDL_Ext::verticalFlip(surf);
-	auto ret = std::make_shared<SDLImageShared>(flipped);
+	auto ret = std::make_shared<SDLImageShared>(flipped, preScaleFactor);
 	ret->fullSize = fullSize;
 	ret->margins.x = fullSize.x - surf->w - margins.x;
 	ret->margins.y = margins.y;
@@ -416,7 +434,7 @@ void SDLImageIndexed::shiftPalette(uint32_t firstColorID, uint32_t colorsToMove,
 void SDLImageIndexed::adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask)
 {
 	// If shadow is enabled, following colors must be skipped unconditionally
-	if (shadowEnabled)
+	if (blitMode == EImageBlitMode::WITH_SHADOW || blitMode == EImageBlitMode::WITH_SHADOW_AND_OVERLAY)
 		colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4);
 
 	// Note: here we skip first colors in the palette that are predefined in H3 images
@@ -432,19 +450,14 @@ void SDLImageIndexed::adjustPalette(const ColorFilter & shifter, uint32_t colors
 	}
 }
 
-SDLImageIndexed::SDLImageIndexed(const std::shared_ptr<ISharedImage> & image, SDL_Palette * originalPalette, EImageBlitMode mode)
+SDLImageIndexed::SDLImageIndexed(const std::shared_ptr<const ISharedImage> & image, SDL_Palette * originalPalette, EImageBlitMode mode)
 	:SDLImageBase::SDLImageBase(image, mode)
 	,originalPalette(originalPalette)
 {
-
 	currentPalette = SDL_AllocPalette(originalPalette->ncolors);
 	SDL_SetPaletteColors(currentPalette, originalPalette->colors, 0, originalPalette->ncolors);
 
-	if (mode == EImageBlitMode::ALPHA)
-	{
-		setOverlayColor(Colors::TRANSPARENCY);
-		setShadowTransparency(1.0);
-	}
+	preparePalette();
 }
 
 SDLImageIndexed::~SDLImageIndexed()
@@ -491,36 +504,42 @@ void SDLImageIndexed::setOverlayColor(const ColorRGBA & color)
 	}
 }
 
-void SDLImageIndexed::setShadowEnabled(bool on)
+void SDLImageIndexed::preparePalette()
 {
-	if (on)
-		setShadowTransparency(1.0);
-
-	if (!on && blitMode == EImageBlitMode::ALPHA)
-		setShadowTransparency(0.0);
-
-	shadowEnabled = on;
-}
-
-void SDLImageIndexed::setBodyEnabled(bool on)
-{
-	if (on)
-		adjustPalette(ColorFilter::genEmptyShifter(), 0);
-	else
-		adjustPalette(ColorFilter::genAlphaShifter(0), 0);
-
-	bodyEnabled = on;
-}
-
-void SDLImageIndexed::setOverlayEnabled(bool on)
-{
-	if (on)
-		setOverlayColor(Colors::WHITE_TRUE);
+	switch(blitMode)
+	{
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::ONLY_OVERLAY:
+			adjustPalette(ColorFilter::genAlphaShifter(0), 0);
+			break;
+	}
 
-	if (!on && blitMode == EImageBlitMode::ALPHA)
-		setOverlayColor(Colors::TRANSPARENCY);
+	switch(blitMode)
+	{
+		case EImageBlitMode::SIMPLE:
+		case EImageBlitMode::WITH_SHADOW:
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			setShadowTransparency(1.0);
+			break;
+		case EImageBlitMode::ONLY_BODY:
+		case EImageBlitMode::ONLY_BODY_IGNORE_OVERLAY:
+		case EImageBlitMode::ONLY_OVERLAY:
+			setShadowTransparency(0.0);
+			break;
+	}
 
-	overlayEnabled = on;
+	switch(blitMode)
+	{
+		case EImageBlitMode::ONLY_OVERLAY:
+		case EImageBlitMode::WITH_SHADOW_AND_OVERLAY:
+			setOverlayColor(Colors::WHITE_TRUE);
+			break;
+		case EImageBlitMode::ONLY_SHADOW:
+		case EImageBlitMode::ONLY_BODY:
+			setOverlayColor(Colors::TRANSPARENCY);
+			break;
+	}
 }
 
 SDLImageShared::~SDLImageShared()
@@ -529,13 +548,13 @@ SDLImageShared::~SDLImageShared()
 	SDL_FreePalette(originalPalette);
 }
 
-SDLImageBase::SDLImageBase(const std::shared_ptr<ISharedImage> & image, EImageBlitMode mode)
+SDLImageBase::SDLImageBase(const std::shared_ptr<const ISharedImage> & image, EImageBlitMode mode)
 	:image(image)
 	, alphaValue(SDL_ALPHA_OPAQUE)
 	, blitMode(mode)
 {}
 
-std::shared_ptr<ISharedImage> SDLImageBase::getSharedImage() const
+std::shared_ptr<const ISharedImage> SDLImageBase::getSharedImage() const
 {
 	return image;
 }
@@ -600,21 +619,6 @@ void SDLImageBase::setBlitMode(EImageBlitMode mode)
 	blitMode = mode;
 }
 
-void SDLImageRGB::setShadowEnabled(bool on)
-{
-	// Not supported. Theoretically we can try to extract all pixels of specific colors, but better to use 8-bit images or composite images
-}
-
-void SDLImageRGB::setBodyEnabled(bool on)
-{
-	// Not supported. Theoretically we can try to extract all pixels of specific colors, but better to use 8-bit images or composite images
-}
-
-void SDLImageRGB::setOverlayEnabled(bool on)
-{
-	// Not supported. Theoretically we can try to extract all pixels of specific colors, but better to use 8-bit images or composite images
-}
-
 void SDLImageRGB::setOverlayColor(const ColorRGBA & color)
 {}
 

+ 16 - 24
client/renderSDL/SDLImage.h

@@ -35,6 +35,9 @@ class SDLImageShared final : public ISharedImage, public std::enable_shared_from
 	//total size including borders
 	Point fullSize;
 
+	//pre scaled image
+	int preScaleFactor;
+
 	// Keep the original palette, in order to do color switching operation
 	void savePalette();
 
@@ -42,11 +45,11 @@ class SDLImageShared final : public ISharedImage, public std::enable_shared_from
 
 public:
 	//Load image from def file
-	SDLImageShared(const CDefFile *data, size_t frame, size_t group=0);
+	SDLImageShared(const CDefFile *data, size_t frame, size_t group=0, int preScaleFactor=1);
 	//Load from bitmap file
-	SDLImageShared(const ImagePath & filename);
+	SDLImageShared(const ImagePath & filename, int preScaleFactor=1);
 	//Create using existing surface, extraRef will increase refcount on SDL_Surface
-	SDLImageShared(SDL_Surface * from);
+	SDLImageShared(SDL_Surface * from, int preScaleFactor=1);
 	~SDLImageShared();
 
 	void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const override;
@@ -54,11 +57,11 @@ public:
 	void exportBitmap(const boost::filesystem::path & path, SDL_Palette * palette) const override;
 	Point dimensions() const override;
 	bool isTransparent(const Point & coords) const override;
-	std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) override;
-	std::shared_ptr<ISharedImage> horizontalFlip() const override;
-	std::shared_ptr<ISharedImage> verticalFlip() const override;
-	std::shared_ptr<ISharedImage> scaleInteger(int factor, SDL_Palette * palette) const override;
-	std::shared_ptr<ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const override;
+	std::shared_ptr<IImage> createImageReference(EImageBlitMode mode) const override;
+	std::shared_ptr<const ISharedImage> horizontalFlip() const override;
+	std::shared_ptr<const ISharedImage> verticalFlip() const override;
+	std::shared_ptr<const ISharedImage> scaleInteger(int factor, SDL_Palette * palette) const override;
+	std::shared_ptr<const ISharedImage> scaleTo(const Point & size, SDL_Palette * palette) const override;
 
 	friend class SDLImageLoader;
 };
@@ -66,19 +69,19 @@ public:
 class SDLImageBase : public IImage, boost::noncopyable
 {
 protected:
-	std::shared_ptr<ISharedImage> image;
+	std::shared_ptr<const ISharedImage> image;
 
 	uint8_t alphaValue;
 	EImageBlitMode blitMode;
 
 public:
-	SDLImageBase(const std::shared_ptr<ISharedImage> & image, EImageBlitMode mode);
+	SDLImageBase(const std::shared_ptr<const ISharedImage> & image, EImageBlitMode mode);
 
 	bool isTransparent(const Point & coords) const override;
 	Point dimensions() const override;
 	void setAlpha(uint8_t value) override;
 	void setBlitMode(EImageBlitMode mode) override;
-	std::shared_ptr<ISharedImage> getSharedImage() const override;
+	std::shared_ptr<const ISharedImage> getSharedImage() const override;
 };
 
 class SDLImageIndexed final : public SDLImageBase
@@ -86,13 +89,10 @@ class SDLImageIndexed final : public SDLImageBase
 	SDL_Palette * currentPalette = nullptr;
 	SDL_Palette * originalPalette = nullptr;
 
-	bool bodyEnabled = true;
-	bool shadowEnabled = false;
-	bool overlayEnabled = false;
-
 	void setShadowTransparency(float factor);
+	void preparePalette();
 public:
-	SDLImageIndexed(const std::shared_ptr<ISharedImage> & image, SDL_Palette * palette, EImageBlitMode mode);
+	SDLImageIndexed(const std::shared_ptr<const ISharedImage> & image, SDL_Palette * palette, EImageBlitMode mode);
 	~SDLImageIndexed();
 
 	void draw(SDL_Surface * where, const Point & pos, const Rect * src) const override;
@@ -103,10 +103,6 @@ public:
 	void scaleInteger(int factor) override;
 	void scaleTo(const Point & size) override;
 	void exportBitmap(const boost::filesystem::path & path) const override;
-
-	void setShadowEnabled(bool on) override;
-	void setBodyEnabled(bool on) override;
-	void setOverlayEnabled(bool on) override;
 };
 
 class SDLImageRGB final : public SDLImageBase
@@ -122,8 +118,4 @@ public:
 	void scaleInteger(int factor) override;
 	void scaleTo(const Point & size) override;
 	void exportBitmap(const boost::filesystem::path & path) const override;
-
-	void setShadowEnabled(bool on) override;
-	void setBodyEnabled(bool on) override;
-	void setOverlayEnabled(bool on) override;
 };

+ 1 - 1
client/renderSDL/SDL_Extensions.cpp

@@ -90,7 +90,7 @@ SDL_Surface * CSDL_Ext::newSurface(const Point & dimensions, SDL_Surface * mod)
 	if (mod->format->palette)
 	{
 		assert(ret->format->palette);
-		assert(ret->format->palette->ncolors == mod->format->palette->ncolors);
+		assert(ret->format->palette->ncolors >= mod->format->palette->ncolors);
 		memcpy(ret->format->palette->colors, mod->format->palette->colors, mod->format->palette->ncolors * sizeof(SDL_Color));
 	}
 	return ret;

+ 21 - 21
client/widgets/CArtifactsOfHeroBase.cpp

@@ -103,38 +103,38 @@ void CArtifactsOfHeroBase::setShowPopupArtPlacesCallback(const CArtPlace::ClickF
 
 void CArtifactsOfHeroBase::clickPressedArtPlace(CComponentHolder & artPlace, const Point & cursorPosition)
 {
-	auto ownedPlace = getArtPlace(cursorPosition);
-	assert(ownedPlace != nullptr);
-
-	if(ownedPlace->isLocked())
-		return;
+	if(auto ownedPlace = getArtPlace(cursorPosition))
+	{
+		if(ownedPlace->isLocked())
+			return;
 
-	if(clickPressedCallback)
-		clickPressedCallback(*ownedPlace, cursorPosition);
+		if(clickPressedCallback)
+			clickPressedCallback(*ownedPlace, cursorPosition);
+	}
 }
 
 void CArtifactsOfHeroBase::showPopupArtPlace(CComponentHolder & artPlace, const Point & cursorPosition)
 {
-	auto ownedPlace = getArtPlace(cursorPosition);
-	assert(ownedPlace != nullptr);
-
-	if(ownedPlace->isLocked())
-		return;
+	if(auto ownedPlace = getArtPlace(cursorPosition))
+	{
+		if(ownedPlace->isLocked())
+			return;
 
-	if(showPopupCallback)
-		showPopupCallback(*ownedPlace, cursorPosition);
+		if(showPopupCallback)
+			showPopupCallback(*ownedPlace, cursorPosition);
+	}
 }
 
 void CArtifactsOfHeroBase::gestureArtPlace(CComponentHolder & artPlace, const Point & cursorPosition)
 {
-	auto ownedPlace = getArtPlace(cursorPosition);
-	assert(ownedPlace != nullptr);
-
-	if(ownedPlace->isLocked())
-		return;
+	if(auto ownedPlace = getArtPlace(cursorPosition))
+	{
+		if(ownedPlace->isLocked())
+			return;
 
-	if(gestureCallback)
-		gestureCallback(*ownedPlace, cursorPosition);
+		if(gestureCallback)
+			gestureCallback(*ownedPlace, cursorPosition);
+	}
 }
 
 void CArtifactsOfHeroBase::setHero(const CGHeroInstance * hero)

+ 16 - 16
client/widgets/CArtifactsOfHeroMarket.cpp

@@ -24,24 +24,24 @@ CArtifactsOfHeroMarket::CArtifactsOfHeroMarket(const Point & position, const int
 
 void CArtifactsOfHeroMarket::clickPressedArtPlace(CComponentHolder & artPlace, const Point & cursorPosition)
 {
-	auto ownedPlace = getArtPlace(cursorPosition);
-	assert(ownedPlace != nullptr);
-
-	if(ownedPlace->isLocked())
-		return;
-
-	if(const auto art = getArt(ownedPlace->slot))
+	if(auto ownedPlace = getArtPlace(cursorPosition))
 	{
-		if(onSelectArtCallback && art->getType()->isTradable())
-		{
-			unmarkSlots();
-			artPlace.selectSlot(true);
-			onSelectArtCallback(ownedPlace.get());
-		}
-		else
+		if(ownedPlace->isLocked())
+			return;
+
+		if(const auto art = getArt(ownedPlace->slot))
 		{
-			if(onClickNotTradableCallback)
-				onClickNotTradableCallback();
+			if(onSelectArtCallback && art->getType()->isTradable())
+			{
+				unmarkSlots();
+				artPlace.selectSlot(true);
+				onSelectArtCallback(ownedPlace.get());
+			}
+			else
+			{
+				if(onClickNotTradableCallback)
+					onClickNotTradableCallback();
+			}
 		}
 	}
 }

+ 3 - 3
client/widgets/CGarrisonInt.cpp

@@ -274,12 +274,12 @@ bool CGarrisonSlot::mustForceReselection() const
 	if (!LOCPLINT->makingTurn)
 		return true;
 
-	if (!creature || !selection->creature)
-		return false;
-
 	// Attempt to take creatures from ally (select theirs first)
 	if (!selection->our())
 		return true;
+	
+	if (!creature || !selection->creature)
+		return false;
 
 	// Attempt to swap creatures with ally (select ours first)
 	if (selection->creature != creature && withAlly)

+ 5 - 5
client/widgets/Images.cpp

@@ -20,6 +20,7 @@
 #include "../render/CAnimation.h"
 #include "../render/Canvas.h"
 #include "../render/ColorFilter.h"
+#include "../render/Colors.h"
 
 #include "../battle/BattleInterface.h"
 #include "../battle/BattleInterfaceClasses.h"
@@ -194,12 +195,12 @@ CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, size_t Group, i
 {
 	pos.x += x;
 	pos.y += y;
-	anim = GH.renderHandler().loadAnimation(name, EImageBlitMode::COLORKEY);
+	anim = GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY);
 	init();
 }
 
 CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, Rect targetPos, size_t Group, ui8 Flags):
-	anim(GH.renderHandler().loadAnimation(name, EImageBlitMode::COLORKEY)),
+	anim(GH.renderHandler().loadAnimation(name, (Flags & CCreatureAnim::CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY)),
 	frame(Frame),
 	group(Group),
 	flags(Flags),
@@ -317,7 +318,7 @@ bool CAnimImage::isPlayerColored() const
 }
 
 CShowableAnim::CShowableAnim(int x, int y, const AnimationPath & name, ui8 Flags, ui32 frameTime, size_t Group, uint8_t alpha):
-	anim(GH.renderHandler().loadAnimation(name, (Flags & CREATURE_MODE) ? EImageBlitMode::ALPHA : EImageBlitMode::COLORKEY)),
+	anim(GH.renderHandler().loadAnimation(name, (Flags & CREATURE_MODE) ? EImageBlitMode::WITH_SHADOW_AND_OVERLAY : EImageBlitMode::COLORKEY)),
 	group(Group),
 	frame(0),
 	first(0),
@@ -430,9 +431,8 @@ void CShowableAnim::blitImage(size_t frame, size_t group, Canvas & to)
 	auto img = anim->getImage(frame, group);
 	if(img)
 	{
-		if (flags & CREATURE_MODE)
-			img->setShadowEnabled(true);
 		img->setAlpha(alpha);
+		img->setOverlayColor(Colors::TRANSPARENCY);
 		to.draw(img, pos.topLeft(), src);
 	}
 }

+ 4 - 7
client/widgets/ObjectLists.cpp

@@ -116,12 +116,12 @@ void CListBox::updatePositions()
 		(elem)->moveTo(itemPos);
 		itemPos += itemOffset;
 	}
-	if (isActive())
+	if(slider)
 	{
-		redraw();
-		if (slider)
-			slider->scrollTo((int)first);
+		slider->scrollTo((int)first);
+		moveChildForeground(slider.get());
 	}
+	redraw();
 }
 
 void CListBox::reset()
@@ -185,9 +185,6 @@ void CListBox::scrollTo(size_t which)
 	//scroll down
 	else if (first + items.size() <= which && which < totalSize)
 		moveToPos(which - items.size() + 1);
-		
-	if(slider)
-		slider->scrollTo(which);
 }
 
 void CListBox::moveToPos(size_t which)

+ 9 - 2
client/windows/CCastleInterface.cpp

@@ -98,7 +98,7 @@ CBuildingRect::CBuildingRect(CCastleBuildings * Par, const CGTownInstance * Town
 		border = GH.renderHandler().loadImage(str->borderName, EImageBlitMode::COLORKEY);
 
 	if(!str->areaName.empty())
-		area = GH.renderHandler().loadImage(str->areaName, EImageBlitMode::ALPHA);
+		area = GH.renderHandler().loadImage(str->areaName, EImageBlitMode::SIMPLE);
 }
 
 const CBuilding * CBuildingRect::getBuilding()
@@ -2055,7 +2055,14 @@ void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition)
 		auto cost = costBase * std::pow(town->spellResearchAcceptedCounter + 1, costExponent);
 
 		std::vector<std::shared_ptr<CComponent>> resComps;
-		auto newSpell = town->spells[level].at(town->spellsAtLevel(level, false));
+
+		int index = town->spellsAtLevel(level, false);
+		if (index >= town->spells[level].size())
+		{
+			LOCPLINT->showInfoDialog(CGI->generaltexth->translate("vcmi.spellResearch.noMoreSpells"));
+			return;
+		}
+		auto newSpell = town->spells[level].at(index);
 		resComps.push_back(std::make_shared<CComponent>(ComponentType::SPELL, spell->id));
 		resComps.push_back(std::make_shared<CComponent>(ComponentType::SPELL, newSpell));
 		resComps.back()->newLine = true;

+ 54 - 4
client/windows/CCreatureWindow.cpp

@@ -22,6 +22,7 @@
 #include "../widgets/Images.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/ObjectLists.h"
+#include "../widgets/GraphicalPrimitiveCanvas.h"
 #include "../windows/InfoWindows.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/Shortcut.h"
@@ -250,6 +251,47 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li
 		Point(214, 4)
 	};
 
+	auto drawBonusSource = [this](int leftRight, Point p, BonusInfo & bi)
+	{
+		std::map<BonusSource, ColorRGBA> bonusColors = {
+			{BonusSource::ARTIFACT,          Colors::GREEN},
+			{BonusSource::ARTIFACT_INSTANCE, Colors::GREEN},
+			{BonusSource::CREATURE_ABILITY,  Colors::YELLOW},
+			{BonusSource::SPELL_EFFECT,      Colors::ORANGE},
+			{BonusSource::SECONDARY_SKILL,   Colors::PURPLE},
+			{BonusSource::HERO_SPECIAL,      Colors::PURPLE},
+			{BonusSource::STACK_EXPERIENCE,  Colors::CYAN},
+			{BonusSource::COMMANDER,         Colors::CYAN},
+		};
+		
+		std::map<BonusSource, std::string> bonusNames = {
+			{BonusSource::ARTIFACT,          CGI->generaltexth->translate("vcmi.bonusSource.artifact")},
+			{BonusSource::ARTIFACT_INSTANCE, CGI->generaltexth->translate("vcmi.bonusSource.artifact")},
+			{BonusSource::CREATURE_ABILITY,  CGI->generaltexth->translate("vcmi.bonusSource.creature")},
+			{BonusSource::SPELL_EFFECT,      CGI->generaltexth->translate("vcmi.bonusSource.spell")},
+			{BonusSource::SECONDARY_SKILL,   CGI->generaltexth->translate("vcmi.bonusSource.hero")},
+			{BonusSource::HERO_SPECIAL,      CGI->generaltexth->translate("vcmi.bonusSource.hero")},
+			{BonusSource::STACK_EXPERIENCE,  CGI->generaltexth->translate("vcmi.bonusSource.commander")},
+			{BonusSource::COMMANDER,         CGI->generaltexth->translate("vcmi.bonusSource.commander")},
+		};
+
+		auto c = bonusColors.count(bi.bonusSource) ? bonusColors[bi.bonusSource] : ColorRGBA(192, 192, 192);
+		std::string t = bonusNames.count(bi.bonusSource) ? bonusNames[bi.bonusSource] : CGI->generaltexth->translate("vcmi.bonusSource.other");
+		int maxLen = 50;
+		EFonts f = FONT_TINY;
+		Point pText = p + Point(3, 40);
+
+		// 1px Black border
+		bonusSource[leftRight].push_back(std::make_shared<CLabel>(pText.x - 1, pText.y, f, ETextAlignment::TOPLEFT, Colors::BLACK, t, maxLen));
+		bonusSource[leftRight].push_back(std::make_shared<CLabel>(pText.x + 1, pText.y, f, ETextAlignment::TOPLEFT, Colors::BLACK, t, maxLen));
+		bonusSource[leftRight].push_back(std::make_shared<CLabel>(pText.x, pText.y - 1, f, ETextAlignment::TOPLEFT, Colors::BLACK, t, maxLen));
+		bonusSource[leftRight].push_back(std::make_shared<CLabel>(pText.x, pText.y + 1, f, ETextAlignment::TOPLEFT, Colors::BLACK, t, maxLen));
+		bonusSource[leftRight].push_back(std::make_shared<CLabel>(pText.x, pText.y, f, ETextAlignment::TOPLEFT, c, t, maxLen));
+
+		frame[leftRight] = std::make_shared<GraphicalPrimitiveCanvas>(Rect(p.x, p.y, 52, 52));
+		frame[leftRight]->addRectangle(Point(0, 0), Point(52, 52), c);
+	};
+
 	for(size_t leftRight : {0, 1})
 	{
 		auto position = offset[leftRight];
@@ -259,8 +301,9 @@ CStackWindow::BonusLineSection::BonusLineSection(CStackWindow * owner, size_t li
 		{
 			BonusInfo & bi = parent->activeBonuses[bonusIndex];
 			icon[leftRight] = std::make_shared<CPicture>(bi.imagePath, position.x, position.y);
-			name[leftRight] = std::make_shared<CLabel>(position.x + 60, position.y + 2, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, bi.name);
-			description[leftRight] = std::make_shared<CMultiLineLabel>(Rect(position.x + 60, position.y + 17, 137, 30), FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, bi.description);
+			name[leftRight] = std::make_shared<CLabel>(position.x + 60, position.y + 2, FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, bi.name, 137);
+			description[leftRight] = std::make_shared<CMultiLineLabel>(Rect(position.x + 60, position.y + 20, 137, 26), FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, bi.description);
+			drawBonusSource(leftRight, Point(position.x - 1, position.y - 1), bi);
 		}
 	}
 }
@@ -284,7 +327,7 @@ CStackWindow::BonusesSection::BonusesSection(CStackWindow * owner, int yOffset,
 		return std::make_shared<BonusLineSection>(owner, index);
 	};
 
-	lines = std::make_shared<CListBox>(onCreate, Point(0, 0), Point(0, itemHeight), visibleSize, totalSize, 0, 1, Rect(pos.w - 15, 0, pos.h, pos.h));
+	lines = std::make_shared<CListBox>(onCreate, Point(0, 0), Point(0, itemHeight), visibleSize, totalSize, 0, totalSize > 3 ? 1 : 0, Rect(pos.w - 15, 0, pos.h, pos.h));
 }
 
 CStackWindow::ButtonsSection::ButtonsSection(CStackWindow * owner, int yOffset)
@@ -533,7 +576,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s
 		animation->setAmount(parent->info->creatureCount);
 	}
 
-	name = std::make_shared<CLabel>(215, 12, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, parent->info->getName());
+	name = std::make_shared<CLabel>(215, 13, FONT_SMALL, ETextAlignment::CENTER, Colors::YELLOW, parent->info->getName());
 
 	const BattleInterface* battleInterface = LOCPLINT->battleInt.get();
 	const CStack* battleStack = parent->info->stack;
@@ -786,6 +829,12 @@ void CStackWindow::initBonusesList()
 	BonusList output;
 	BonusList input;
 	input = *(info->stackNode->getBonuses(CSelector(Bonus::Permanent), Selector::all));
+	std::sort(input.begin(), input.end(), [this](std::shared_ptr<Bonus> v1, std::shared_ptr<Bonus> & v2){
+		if (v1->source != v2->source)
+			return v1->source == BonusSource::CREATURE_ABILITY || (v1->source < v2->source);
+		else
+			return  info->stackNode->bonusToString(v1, false) < info->stackNode->bonusToString(v2, false);
+	});
 
 	while(!input.empty())
 	{
@@ -801,6 +850,7 @@ void CStackWindow::initBonusesList()
 		bonusInfo.name = info->stackNode->bonusToString(b, false);
 		bonusInfo.description = info->stackNode->bonusToString(b, true);
 		bonusInfo.imagePath = info->stackNode->bonusToGraphics(b);
+		bonusInfo.bonusSource = b->source;
 
 		//if it's possible to give any description or image for this kind of bonus
 		//TODO: figure out why half of bonuses don't have proper description

+ 4 - 0
client/windows/CCreatureWindow.h

@@ -31,6 +31,7 @@ class CListBox;
 class CArtPlace;
 class CCommanderArtPlace;
 class LRClickableArea;
+class GraphicalPrimitiveCanvas;
 
 class CCommanderSkillIcon : public LRClickableAreaWText //TODO: maybe bring commander skill button initialization logic inside?
 {
@@ -58,6 +59,7 @@ class CStackWindow : public CWindowObject
 		std::string name;
 		std::string description;
 		ImagePath imagePath;
+		BonusSource bonusSource;
 	};
 
 	class CWindowSection : public CIntObject
@@ -84,6 +86,8 @@ class CStackWindow : public CWindowObject
 		std::array<std::shared_ptr<CPicture>, 2> icon;
 		std::array<std::shared_ptr<CLabel>, 2> name;
 		std::array<std::shared_ptr<CMultiLineLabel>, 2> description;
+		std::array<std::shared_ptr<GraphicalPrimitiveCanvas>, 2> frame;
+		std::array<std::vector<std::shared_ptr<CLabel>>, 2> bonusSource;
 	public:
 		BonusLineSection(CStackWindow * owner, size_t lineIndex);
 	};

+ 2 - 1
client/windows/CHeroWindow.cpp

@@ -199,10 +199,11 @@ void CHeroWindow::update()
 		OBJECT_CONSTRUCTION;
 		if(!garr)
 		{
+			bool removableTroops = curHero->getOwner() == LOCPLINT->playerID;
 			std::string helpBox = heroscrn[32];
 			boost::algorithm::replace_first(helpBox, "%s", CGI->generaltexth->allTexts[43]);
 
-			garr = std::make_shared<CGarrisonInt>(Point(15, 485), 8, Point(), curHero);
+			garr = std::make_shared<CGarrisonInt>(Point(15, 485), 8, Point(), curHero, nullptr, removableTroops);
 			auto split = std::make_shared<CButton>(Point(539, 519), AnimationPath::builtin("hsbtns9.def"), CButton::tooltip(CGI->generaltexth->allTexts[256], helpBox), [this](){ garr->splitClick(); }, EShortcut::HERO_ARMY_SPLIT);
 			garr->addSplitBtn(split);
 		}

+ 2 - 2
client/windows/CreaturePurchaseCard.cpp

@@ -54,8 +54,8 @@ void CreaturePurchaseCard::switchCreatureLevel()
 	auto index = vstd::find_pos(upgradesID, creatureOnTheCard->getId());
 	auto nextCreatureId = vstd::circularAt(upgradesID, ++index);
 	creatureOnTheCard = nextCreatureId.toCreature();
-	picture = std::make_shared<CCreaturePic>(parent->pos.x, parent->pos.y, creatureOnTheCard);
-	creatureClickArea = std::make_shared<CCreatureClickArea>(Point(parent->pos.x, parent->pos.y), picture, creatureOnTheCard);
+	picture = std::make_shared<CCreaturePic>(picture->pos.x - pos.x, picture->pos.y - pos.y, creatureOnTheCard);
+	creatureClickArea = std::make_shared<CCreatureClickArea>(Point(picture->pos.x - pos.x, picture->pos.y - pos.y), picture, creatureOnTheCard);
 	parent->updateAllSliders();
 	cost->set(creatureOnTheCard->getFullRecruitCost() * slider->getValue());
 }

+ 3 - 3
client/windows/GUIClasses.cpp

@@ -950,7 +950,7 @@ CUniversityWindow::CUniversityWindow(const CGHeroInstance * _hero, BuildingID bu
 	}
 	else if(auto uni = dynamic_cast<const CGUniversity *>(_market); uni->appearance)
 	{
-		titlePic = std::make_shared<CAnimImage>(uni->appearance->animationFile, 0);
+		titlePic = std::make_shared<CAnimImage>(uni->appearance->animationFile, 0, 0, 0, 0, CShowableAnim::CREATURE_MODE);
 		titleStr = uni->title;
 		speechStr = uni->speech;
 	}
@@ -1703,7 +1703,7 @@ void VideoWindow::keyPressed(EShortcut key)
 	exit(true);
 }
 
-bool VideoWindow::receiveEvent(const Point & position, int eventType) const
+void VideoWindow::notFocusedClick()
 {
-	return true;  // capture click also outside of window
+	exit(true);
 }

+ 1 - 1
client/windows/GUIClasses.h

@@ -523,5 +523,5 @@ public:
 
 	void clickPressed(const Point & cursorPosition) override;
 	void keyPressed(EShortcut key) override;
-	bool receiveEvent(const Point & position, int eventType) const override;
+	void notFocusedClick() override;
 };

+ 5 - 0
clientapp/CMakeLists.txt

@@ -56,6 +56,11 @@ if(WIN32)
 	endif()
 	target_compile_definitions(vcmiclient PRIVATE WINDOWS_IGNORE_PACKING_MISMATCH)
 
+	if(NOT ffmpeg_LIBRARIES)
+		target_compile_definitions(vcmiclient PRIVATE DISABLE_VIDEO)
+	endif()
+
+
 	# TODO: very hacky, find proper solution to copy AI dlls into bin dir
 	if(MSVC)
 		add_custom_command(TARGET vcmiclient POST_BUILD

+ 16 - 0
config/campaignSets.json

@@ -47,5 +47,21 @@
 			{ "id": 6, "x":34,  "y":417, "file":"DATA/FINAL",    "image":"CAMPUA1", "video":"UNHOLY",  "requires": [4]          },
 			{ "id": 7, "x":404, "y":414, "file":"DATA/SECRET",   "image":"CAMPSP1", "video":"SPECTRE", "requires": [6]          }
 		]
+	},
+	"chr":
+	{
+		"images" : [ {"x": 0, "y": 0, "name":"data/CampaignBackground8"} ],
+		"exitbutton" : {"x": 658, "y": 482, "name":"CMPSCAN" },
+		"items":
+		[
+			{ "id": 1, "x":40,  "y":72,  "file":"Maps/Chronicles/Hc1_Main", "image":"CampaignHc1Image", "video":"", "requires": [], "optional": true },
+			{ "id": 2, "x":310, "y":72,  "file":"Maps/Chronicles/Hc2_Main", "image":"CampaignHc2Image", "video":"", "requires": [], "optional": true },
+			{ "id": 3, "x":590, "y":72,  "file":"Maps/Chronicles/Hc3_Main", "image":"CampaignHc3Image", "video":"", "requires": [], "optional": true },
+			{ "id": 4, "x":43,  "y":245, "file":"Maps/Chronicles/Hc4_Main", "image":"CampaignHc4Image", "video":"", "requires": [], "optional": true },
+			{ "id": 5, "x":313, "y":244, "file":"Maps/Chronicles/Hc5_Main", "image":"CampaignHc5Image", "video":"", "requires": [], "optional": true },
+			{ "id": 6, "x":586, "y":244, "file":"Maps/Chronicles/Hc6_Main", "image":"CampaignHc6Image", "video":"", "requires": [], "optional": true },
+			{ "id": 7, "x":34,  "y":413, "file":"Maps/Chronicles/Hc7_Main", "image":"CampaignHc7Image", "video":"", "requires": [], "optional": true },
+			{ "id": 8, "x":404, "y":414, "file":"Maps/Chronicles/Hc8_Main", "image":"CampaignHc8Image", "video":"", "requires": [], "optional": true }
+		]
 	}
 }

+ 14 - 11
config/creatures/castle.json

@@ -57,6 +57,7 @@
 		"extraNames": [ "lightCrossbowman" ],
 		"faction": "castle",
 		"upgrades": ["marksman"],
+		"shots" : 12,
 		"abilities" :
 		{
 			"shooter" : {
@@ -86,6 +87,7 @@
 		"index": 3,
 		"level": 2,
 		"faction": "castle",
+		"shots" : 24,
 		"abilities":
 		{
 			"shooter" : {
@@ -228,6 +230,7 @@
 		"level": 5,
 		"faction": "castle",
 		"upgrades": ["zealot"],
+		"shots" : 12,
 		"abilities" :
 		{
 			"shooter" : {
@@ -257,6 +260,7 @@
 		"index": 9,
 		"level": 5,
 		"faction": "castle",
+		"shots" : 24,
 		"abilities" :
 		{
 			"shooter" : {
@@ -344,14 +348,8 @@
 		"index": 12,
 		"level": 7,
 		"faction": "castle",
-		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_2" : // Will be affected by Advanced Slayer or better
-			{
-				"type" : "KING",
-				"val" : 2
-			},
 			"canFly" :
 			{
 				"type" : "FLYING"
@@ -363,6 +361,11 @@
 				"propagator" : "HERO",
 				"stacking" : "Angels"
 			},
+			"KING_2" : // Will be affected by Advanced Slayer or better
+			{
+				"type" : "KING",
+				"val" : 2
+			},
 			"hateDevils" :
 			{
 				"type" : "HATE",
@@ -398,11 +401,6 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_2" : // Will be affected by Advanced Slayer or better
-			{
-				"type" : "KING",
-				"val" : 2
-			},
 			"canFly" :
 			{
 				"type" : "FLYING"
@@ -430,6 +428,11 @@
 				"propagator" : "HERO",
 				"stacking" : "Angels"
 			},
+			"KING_2" : // Will be affected by Advanced Slayer or better
+			{
+				"type" : "KING",
+				"val" : 2
+			},
 			"hateDevils" :
 			{
 				"type" : "HATE",

+ 19 - 17
config/creatures/conflux.json

@@ -127,6 +127,7 @@
 		"index": 127,
 		"level": 2,
 		"faction": "conflux",
+		"shots" : 24,
 		"abilities":
 		{
 			"nonLiving" : 
@@ -301,6 +302,7 @@
 		"level": 3,
 		"faction": "conflux",
 		"doubleWide" : true,
+		"shots" : 24,
 		"abilities":
 		{
 			"nonLiving" : 
@@ -472,11 +474,11 @@
 			{
 				"type" : "NON_LIVING"
 			},
-			"canFly" :
+			"energizes" :
 			{
 				"type" : "FLYING"
 			},
-			"spellcaster":
+			"spellcaster" :
 			{
 				"type" : "SPELLCASTER",
 				"subtype" : "spell.protectFire",
@@ -767,11 +769,6 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
-			{
-				"type" : "KING",
-				"val" : 0
-			},
 			"canFly" :
 			{
 				"type" : "FLYING"
@@ -784,6 +781,11 @@
 			{
 				"type" : "SPELL_SCHOOL_IMMUNITY",
 				"subtype" : "spellSchool.fire"
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"graphics" :
@@ -807,11 +809,6 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
-			{
-				"type" : "KING",
-				"val" : 0
-			},
 			"canFly" :
 			{
 				"type" : "FLYING"
@@ -820,11 +817,6 @@
 			{
 				"type" : "TWO_HEX_ATTACK_BREATH"
 			},
-			"immuneToFire" :
-			{
-				"type" : "SPELL_SCHOOL_IMMUNITY",
-				"subtype" : "spellSchool.fire"
-			},
 			"rebirthOnce" :
 			{
 				"type" : "CASTS",
@@ -834,6 +826,16 @@
 			{
 				"type" : "REBIRTH",
 				"val" : 20
+			},
+			"immuneToFire" :
+			{
+				"type" : "SPELL_SCHOOL_IMMUNITY",
+				"subtype" : "spellSchool.fire"
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"graphics" :

+ 18 - 14
config/creatures/dungeon.json

@@ -138,6 +138,7 @@
 		"level": 3,
 		"faction": "dungeon",
 		"upgrades": ["evilEye"],
+		"shots" : 12,
 		"abilities" :
 		{
 			"shooter" :
@@ -179,6 +180,7 @@
 		"index": 75,
 		"level": 3,
 		"faction": "dungeon",
+		"shots" : 24,
 		"abilities" :
 		{
 			"shooter" :
@@ -221,6 +223,7 @@
 		"level": 4,
 		"faction": "dungeon",
 		"doubleWide" : true,
+		"shots" : 4,
 		"abilities":
 		{
 			"shooter" :
@@ -264,6 +267,7 @@
 		"level": 4,
 		"faction": "dungeon",
 		"doubleWide" : true,
+		"shots" : 8,
 		"abilities":
 		{
 			"shooter" :
@@ -425,19 +429,14 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
+			"dragon" : 
 			{
-				"type" : "KING",
-				"val" : 0
+				"type" : "DRAGON_NATURE"
 			},
 			"canFly" :
 			{
 				"type" : "FLYING"
 			},
-			"dragon" : 
-			{
-				"type" : "DRAGON_NATURE"
-			},
 			"twoHexAttackBreath" :
 			{
 				"type" : "TWO_HEX_ATTACK_BREATH"
@@ -446,6 +445,11 @@
 			{
 				"type" : "LEVEL_SPELL_IMMUNITY",
 				"val" : 3
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"upgrades": ["blackDragon"],
@@ -470,19 +474,14 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
+			"dragon" : 
 			{
-				"type" : "KING",
-				"val" : 0
+				"type" : "DRAGON_NATURE"
 			},
 			"canFly" :
 			{
 				"type" : "FLYING"
 			},
-			"dragon" : 
-			{
-				"type" : "DRAGON_NATURE"
-			},
 			"twoHexAttackBreath" :
 			{
 				"type" : "TWO_HEX_ATTACK_BREATH"
@@ -492,6 +491,11 @@
 				"type" : "LEVEL_SPELL_IMMUNITY",
 				"val" : 5
 			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
+			},
 			"hateTitans" :
 			{
 				"type" : "HATE",

+ 60 - 58
config/creatures/fortress.json

@@ -44,6 +44,7 @@
 		"faction": "fortress",
 		"upgrades": ["lizardWarrior"],
 		"hasDoubleWeek": true,
+		"shots" : 12,
 		"abilities" :
 		{
 			"shooter" :
@@ -74,6 +75,7 @@
 		"index": 101,
 		"level": 2,
 		"faction": "fortress",
+		"shots" : 24,
 		"abilities" :
 		{
 			"shooter" :
@@ -99,54 +101,6 @@
 			"wince": "ALIZWNCE.wav"
 		}
 	},
-	"gorgon" :
-	{
-		"index": 102,
-		"level": 5,
-		"faction": "fortress",
-		"upgrades": ["mightyGorgon"],
-		"doubleWide" : true,
-		"graphics" :
-		{
-			"animation": "CCGORG.DEF"
-		},
-		"sound" :
-		{
-			"attack": "CGORATTK.wav",
-			"defend": "CGORDFND.wav",
-			"killed": "CGORKILL.wav",
-			"move": "CGORMOVE.wav",
-			"wince": "CGORWNCE.wav"
-		}
-	},
-	"mightyGorgon" :
-	{
-		"index": 103,
-		"level": 5,
-		"faction": "fortress",
-		"doubleWide" : true,
-		"abilities":
-		{
-			"deathStare" : 
-			{
-				"type" : "DEATH_STARE",
-				"subtype" : "deathStareGorgon",
-				"val" : 10
-			}
-		},
-		"graphics" :
-		{
-			"animation": "CBGOG.DEF"
-		},
-		"sound" :
-		{
-			"attack": "BGORATTK.wav",
-			"defend": "BGORDFND.wav",
-			"killed": "BGORKILL.wav",
-			"move": "BGORMOVE.wav",
-			"wince": "BGORWNCE.wav"
-		}
-	},
 	"serpentFly" :
 	{
 		"index": 104,
@@ -276,6 +230,54 @@
 			"wince": "GBASWNCE.wav"
 		}
 	},
+	"gorgon" :
+	{
+		"index": 102,
+		"level": 5,
+		"faction": "fortress",
+		"upgrades": ["mightyGorgon"],
+		"doubleWide" : true,
+		"graphics" :
+		{
+			"animation": "CCGORG.DEF"
+		},
+		"sound" :
+		{
+			"attack": "CGORATTK.wav",
+			"defend": "CGORDFND.wav",
+			"killed": "CGORKILL.wav",
+			"move": "CGORMOVE.wav",
+			"wince": "CGORWNCE.wav"
+		}
+	},
+	"mightyGorgon" :
+	{
+		"index": 103,
+		"level": 5,
+		"faction": "fortress",
+		"doubleWide" : true,
+		"abilities":
+		{
+			"deathStare" : 
+			{
+				"type" : "DEATH_STARE",
+				"subtype" : "deathStareGorgon",
+				"val" : 10
+			}
+		},
+		"graphics" :
+		{
+			"animation": "CBGOG.DEF"
+		},
+		"sound" :
+		{
+			"attack": "BGORATTK.wav",
+			"defend": "BGORDFND.wav",
+			"killed": "BGORKILL.wav",
+			"move": "BGORMOVE.wav",
+			"wince": "BGORWNCE.wav"
+		}
+	},
 	"wyvern" :
 	{
 		"index": 108,
@@ -343,11 +345,6 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
-			{
-				"type" : "KING",
-				"val" : 0
-			},
 			"attackAllAdjacent" :
 			{
 				"type" : "ATTACKS_ALL_ADJACENT"
@@ -355,6 +352,11 @@
 			"noRetaliation" :
 			{
 				"type" : "BLOCKS_RETALIATION"
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"upgrades": ["chaosHydra"],
@@ -379,11 +381,6 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
-			{
-				"type" : "KING",
-				"val" : 0
-			},
 			"attackAllAdjacent" :
 			{
 				"type" : "ATTACKS_ALL_ADJACENT"
@@ -391,6 +388,11 @@
 			"noRetaliation" :
 			{
 				"type" : "BLOCKS_RETALIATION"
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"graphics" :

+ 14 - 12
config/creatures/inferno.json

@@ -51,6 +51,7 @@
 		"faction": "inferno",
 		"upgrades": ["magog"],
 		"hasDoubleWeek": true,
+		"shots" : 12,
 		"abilities" :
 		{
 			"shooter" :
@@ -81,6 +82,7 @@
 		"index": 45,
 		"level": 2,
 		"faction": "inferno",
+		"shots" : 24,
 		"abilities":
 		{
 			"shooter" :
@@ -353,12 +355,7 @@
 		"faction": "inferno",
 		"abilities":
 		{
-			"KING_2" : // Will be affected by Advanced Slayer or better
-			{
-				"type" : "KING",
-				"val" : 2
-			},
-			"canFly" :
+			"teleports" :
 			{
 				"type" : "FLYING",
 				"subtype" : "movementTeleporting"
@@ -376,6 +373,11 @@
 				"propagationUpdater" : "BONUS_OWNER_UPDATER",
 				"limiters" : [ "OPPOSITE_SIDE" ]
 			},
+			"KING_2" : // Will be affected by Advanced Slayer or better
+			{
+				"type" : "KING",
+				"val" : 2
+			},
 			"hateAngels" : 
 			{
 				"type" : "HATE",
@@ -413,12 +415,7 @@
 		"faction": "inferno",
 		"abilities" :
 		{
-			"KING_2" : // Will be affected by Advanced Slayer or better
-			{
-				"type" : "KING",
-				"val" : 2
-			},
-			"canFly" :
+			"teleports" :
 			{
 				"type" : "FLYING",
 				"subtype" : "movementTeleporting"
@@ -436,6 +433,11 @@
 				"propagationUpdater" : "BONUS_OWNER_UPDATER",
 				"limiters" : [ "OPPOSITE_SIDE" ]
 			},
+			"KING_2" : // Will be affected by Advanced Slayer or better
+			{
+				"type" : "KING",
+				"val" : 2
+			},
 			"hateAngels" : 
 			{
 				"type" : "HATE",

+ 16 - 14
config/creatures/necropolis.json

@@ -265,6 +265,7 @@
 		"index": 64,
 		"level": 5,
 		"faction": "necropolis",
+		"shots" : 12,
 		"abilities":
 		{
 			"undead" :
@@ -305,6 +306,7 @@
 		"index": 65,
 		"level": 5,
 		"faction": "necropolis",
+		"shots" : 24,
 		"abilities":
 		{
 			"undead" :
@@ -421,19 +423,14 @@
 			{
 				"type" : "UNDEAD"
 			},
-			"KING_1" : // Will be affected by Slayer with no expertise
+			"dragon" :
 			{
-				"type" : "KING",
-				"val" : 0
+				"type" : "DRAGON_NATURE"
 			},
 			"canFly" :
 			{
 				"type" : "FLYING"
 			},
-			"dragon" :
-			{
-				"type" : "DRAGON_NATURE"
-			},
 			"decreaseMorale" :
 			{
 				"type" : "MORALE",
@@ -442,6 +439,11 @@
 				"propagator": "BATTLE_WIDE",
 				"propagationUpdater" : "BONUS_OWNER_UPDATER",
 				"limiters" : [ "OPPOSITE_SIDE" ]
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"upgrades": ["ghostDragon"],
@@ -470,19 +472,14 @@
 			{
 				"type" : "UNDEAD"
 			},
-			"KING_1" : // Will be affected by Slayer with no expertise
+			"dragon" :
 			{
-				"type" : "KING",
-				"val" : 0
+				"type" : "DRAGON_NATURE"
 			},
 			"canFly" :
 			{
 				"type" : "FLYING"
 			},
-			"dragon" :
-			{
-				"type" : "DRAGON_NATURE"
-			},
 			"age" :
 			{
 				"type" : "SPELL_AFTER_ATTACK",
@@ -497,6 +494,11 @@
 				"propagator": "BATTLE_WIDE",
 				"propagationUpdater" : "BONUS_OWNER_UPDATER",
 				"limiters" : [ "OPPOSITE_SIDE" ]
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"graphics" :

+ 33 - 26
config/creatures/neutral.json

@@ -71,19 +71,14 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
+			"dragon" : 
 			{
-				"type" : "KING",
-				"val" : 0
+				"type" : "DRAGON_NATURE"
 			},
 			"canFly" :
 			{
 				"type" : "FLYING"
 			},
-			"dragon" : 
-			{
-				"type" : "DRAGON_NATURE"
-			},
 			"twoHexAttackBreath" :
 			{
 				"type" : "TWO_HEX_ATTACK_BREATH"
@@ -100,6 +95,11 @@
 			{
 				"type" : "LEVEL_SPELL_IMMUNITY",
 				"val" : 3
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"graphics" :
@@ -124,23 +124,23 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
-			{
-				"type" : "KING",
-				"val" : 0
-			},
 			"dragon" :
 			{
 				"type" : "DRAGON_NATURE"
 			},
+			"crystals" :
+			{
+				"type" : "SPECIAL_CRYSTAL_GENERATION"
+			},
 			"magicResistance" :
 			{
 				"type" : "MAGIC_RESISTANCE",
 				"val" : 20
 			},
-			"crystals" :
+			"KING_1" : // Will be affected by Slayer with no expertise
 			{
-				"type" : "SPECIAL_CRYSTAL_GENERATION"
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"graphics" :
@@ -165,15 +165,14 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
-			{
-				"type" : "KING",
-				"val" : 0
-			},
 			"dragon" :
 			{
 				"type" : "DRAGON_NATURE"
 			},
+			"canFly" :
+			{
+				"type" : "FLYING"
+			},
 			"mirror" :
 			{
 				"type" : "MAGIC_MIRROR",
@@ -244,6 +243,11 @@
 				"subtype" : "spell.meteorShower",
 				"addInfo" : 5,
 				"val" : 2
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"graphics" :
@@ -269,19 +273,14 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
+			"dragon" : 
 			{
-				"type" : "KING",
-				"val" : 0
+				"type" : "DRAGON_NATURE"
 			},
 			"canFly" :
 			{
 				"type" : "FLYING"
 			},
-			"dragon" : 
-			{
-				"type" : "DRAGON_NATURE"
-			},
 			"twoHexAttackBreath" :
 			{
 				"type" : "TWO_HEX_ATTACK_BREATH"
@@ -297,6 +296,11 @@
 				"type" : "SPELL_AFTER_ATTACK",
 				"subtype" : "spell.acidBreath",
 				"val" : 100
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"graphics" :
@@ -319,6 +323,7 @@
 		"extraNames": [ "enchanters" ],
 		"faction": "neutral",
 		"excludeFromRandomization" : true,
+		"shots" : 32,
 		"abilities":
 		{
 			"shooter" :
@@ -406,6 +411,7 @@
 		"extraNames": [ "sharpshooters" ],
 		"faction": "neutral",
 		"excludeFromRandomization" : true,
+		"shots" : 32,
 		"abilities":
 		{
 			"shooter" :
@@ -444,6 +450,7 @@
 		"index": 138,
 		"level": 1,
 		"faction": "neutral",
+		"shots" : 24,
 		"abilities": 
 		{
 			"shooter" :

+ 17 - 15
config/creatures/rampart.json

@@ -101,6 +101,7 @@
 		"level": 3,
 		"faction": "rampart",
 		"upgrades": ["grandElf"],
+		"shots" : 24,
 		"abilities" :
 		{
 			"shooter" :
@@ -131,7 +132,8 @@
 		"index": 19,
 		"level": 3,
 		"faction": "rampart",
-		"abilities": 
+		"shots" : 24,
+		"abilities" :
 		{
 			"shooter" :
 			{
@@ -359,19 +361,14 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
+			"dragon" :
 			{
-				"type" : "KING",
-				"val" : 0
+				"type" : "DRAGON_NATURE"
 			},
 			"canFly" :
 			{
 				"type" : "FLYING"
 			},
-			"dragon" :
-			{
-				"type" : "DRAGON_NATURE"
-			},
 			"twoHexAttackBreath" :
 			{
 				"type" : "TWO_HEX_ATTACK_BREATH"
@@ -380,6 +377,11 @@
 			{
 				"type" : "LEVEL_SPELL_IMMUNITY",
 				"val" : 3
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"upgrades": ["goldDragon"],
@@ -404,19 +406,14 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
+			"dragon" :
 			{
-				"type" : "KING",
-				"val" : 0
+				"type" : "DRAGON_NATURE"
 			},
 			"canFly" :
 			{
 				"type" : "FLYING"
 			},
-			"dragon" :
-			{
-				"type" : "DRAGON_NATURE"
-			},
 			"twoHexAttackBreath" :
 			{
 				"type" : "TWO_HEX_ATTACK_BREATH"
@@ -425,6 +422,11 @@
 			{
 				"type" : "LEVEL_SPELL_IMMUNITY",
 				"val" : 4
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"graphics" :

+ 40 - 2
config/creatures/special.json

@@ -37,8 +37,17 @@
 		"level": 0,
 		"faction": "neutral",
 		"doubleWide" : true,
+		"shots" : 24,
 		"abilities" :
 		{
+			"siegeWeapon" :
+			{
+				"type" : "SIEGE_WEAPON"
+			},
+			"shooter" :
+			{
+				"type" : "SHOOTER"
+			},
 			"siegeMachine" :
 			{
 				"type" : "CATAPULT",
@@ -67,6 +76,18 @@
 		"level": 0,
 		"faction": "neutral",
 		"doubleWide" : true,
+		"shots" : 24,
+		"abilities" :
+		{
+			"siegeWeapon" :
+			{
+				"type" : "SIEGE_WEAPON"
+			},
+			"shooter" :
+			{
+				"type" : "SHOOTER"
+			}
+		},
 		"graphics" :
 		{
 			"animation": "SMBAL.DEF",
@@ -91,7 +112,12 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"heals" : {
+			"siegeWeapon" :
+			{
+				"type" : "SIEGE_WEAPON"
+			},
+			"heals" :
+			{
 				"type" : "HEALER" ,
 				"subtype" : "spell.firstAid"
 			}
@@ -112,7 +138,17 @@
 		"index": 148,
 		"level": 0,
 		"faction": "neutral",
-		"abilities": { "inactive" : { "type" : "NOT_ACTIVE" } },
+		"abilities": 
+		{
+			"siegeWeapon" :
+			{
+				"type" : "SIEGE_WEAPON"
+			},
+			"inactive" :
+			{
+				"type" : "NOT_ACTIVE"
+			}
+		},
 		"graphics" :
 		{
 			"animation": "SMCART.DEF"
@@ -129,8 +165,10 @@
 		"index": 149,
 		"level": 0,
 		"faction": "neutral",
+		"shots" : 99,
 		"abilities":
 		{
+			"siegeWeapon" : { "type" : "SIEGE_WEAPON" },
 			"shooter" : { "type" : "SHOOTER" },
 			"ignoreDefence" : { "type" : "ENEMY_DEFENCE_REDUCTION", "val" : 100 },
 			"noWallPenalty" : { "type" : "NO_WALL_PENALTY" },

+ 14 - 10
config/creatures/stronghold.json

@@ -92,6 +92,7 @@
 		"level": 3,
 		"faction": "stronghold",
 		"upgrades": ["orcChieftain"],
+		"shots" : 12,
 		"abilities" :
 		{
 			"shooter" :
@@ -122,6 +123,7 @@
 		"index": 89,
 		"level": 3,
 		"faction": "stronghold",
+		"shots" : 24,
 		"abilities" :
 		{
 			"shooter" :
@@ -274,6 +276,7 @@
 		"index": 94,
 		"level": 6,
 		"faction": "stronghold",
+		"shots" : 16,
 		"abilities" :
 		{
 			"shooter" :
@@ -310,6 +313,7 @@
 		"index": 95,
 		"level": 6,
 		"faction": "stronghold",
+		"shots" : 24,
 		"abilities":
 		{
 			"shooter" :
@@ -355,15 +359,15 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
-			{
-				"type" : "KING",
-				"val" : 0
-			},
 			"reduceDefence" :
 			{
 				"type" : "ENEMY_DEFENCE_REDUCTION",
 				"val" : 40
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"upgrades": ["ancientBehemoth"],
@@ -388,15 +392,15 @@
 		"doubleWide" : true,
 		"abilities":
 		{
-			"KING_1" : // Will be affected by Slayer with no expertise
-			{
-				"type" : "KING",
-				"val" : 0
-			},
 			"reduceDefence" :
 			{
 				"type" : "ENEMY_DEFENCE_REDUCTION",
 				"val" : 80
+			},
+			"KING_1" : // Will be affected by Slayer with no expertise
+			{
+				"type" : "KING",
+				"val" : 0
 			}
 		},
 		"graphics" :

+ 13 - 9
config/creatures/tower.json

@@ -26,6 +26,7 @@
 		"index": 29,
 		"level": 1,
 		"faction": "tower",
+		"shots" : 8,
 		"abilities" :
 		{
 			"shooter" :
@@ -178,6 +179,7 @@
 		"index": 34,
 		"level": 4,
 		"faction": "tower",
+		"shots" : 24,
 		"abilities": 
 		{
 			"shooter" :
@@ -218,6 +220,7 @@
 		"index": 35,
 		"level": 4,
 		"faction": "tower",
+		"shots" : 24,
 		"abilities": 
 		{
 			"shooter" :
@@ -415,14 +418,14 @@
 		"faction": "tower",
 		"abilities" :
 		{
+			"immuneToMind" : 
+			{
+				"type" : "MIND_IMMUNITY"
+			},
 			"KING_3" : // Will be affected by Expert Slayer only
 			{
 				"type" : "KING",
 				"val" : 3
-			},
-			"immuneToMind" : 
-			{
-				"type" : "MIND_IMMUNITY"
 			}
 		},
 		"upgrades": ["titan"],
@@ -444,13 +447,9 @@
 		"index": 41,
 		"level": 7,
 		"faction": "tower",
+		"shots" : 24,
 		"abilities" :
 		{
-			"KING_3" : // Will be affected by Expert Slayer only
-			{
-				"type" : "KING",
-				"val" : 3
-			},
 			"shooter" :
 			{
 				"type" : "SHOOTER"
@@ -463,6 +462,11 @@
 			{
 				"type" : "MIND_IMMUNITY"
 			},
+			"KING_3" : // Will be affected by Expert Slayer only
+			{
+				"type" : "KING",
+				"val" : 3
+			},
 			"hateBlackDragons" : 
 			{
 				"type" : "HATE",

+ 2 - 1
config/objects/generic.json

@@ -222,7 +222,8 @@
 			"sounds" : {
 				"ambient" : ["LOOPFACT"],
 				"visit" : ["MILITARY"]
-			}
+			},
+			"creatures": [ ["ballista"], ["firstAidTent"], ["ammoCart"] ]
 		},
 		"types" : {
 			"object" : {

+ 2 - 1
config/schemas/spell.json

@@ -22,7 +22,8 @@
 						"properties" : {
 							"verticalPosition" : {"type" : "string", "enum" :["top","bottom"]},
 							"defName" : {"type" : "string", "format" : "animationFile"},
-							"effectName" : { "type" : "string" }
+							"effectName" : { "type" : "string" },
+							"transparency" : {"type" : "number", "minimum" : 0, "maximum" : 1}
 						},
 						"additionalProperties" : false
 					}

+ 1 - 1
config/spells/ability.json

@@ -252,7 +252,7 @@
 		"targetType": "NO_TARGET",
 
 		"animation":{
-			"hit":["SP04_"]
+			"hit":[{ "defName" : "SP04_", "transparency" : 0.5}]
 		},
 		"sounds": {
 			"cast": "DEATHCLD"

+ 2 - 2
config/spells/offensive.json

@@ -44,7 +44,7 @@
 				{"minimumAngle": 1.20 ,"defName":"C08SPW1"},
 				{"minimumAngle": 1.50 ,"defName":"C08SPW0"}
 			],
-			"hit":["C08SPW5"]
+			"hit":[ {"defName" : "C08SPW5", "transparency" : 0.5 }]
 		},
 		"sounds": {
 			"cast": "ICERAY"
@@ -309,7 +309,7 @@
 		"targetType" : "CREATURE",
 
 		"animation":{
-			"affect":["C14SPA0"]
+			"affect":[{"defName" : "C14SPA0", "transparency" : 0.5}]
 		},
 		"sounds": {
 			"cast": "SACBRETH"

+ 1 - 1
config/spells/other.json

@@ -483,7 +483,7 @@
 		"targetType" : "CREATURE",
 
 		"animation":{
-			"affect":["C01SPE0"]
+			"affect":[{ "defName" : "C01SPE0", "transparency" : 0.5}]
 		},
 		"sounds": {
 			"cast": "RESURECT"

+ 2 - 2
config/spells/timed.json

@@ -652,7 +652,7 @@
 		"targetType" : "CREATURE",
 
 		"animation":{
-			"affect":["C07SPA1"],
+			"affect":[{"defName" : "C07SPA1", "transparency" : 0.5}],
 			"projectile":[{"defName":"C07SPA0"}]//???
 		},
 		"sounds": {
@@ -696,7 +696,7 @@
 		"targetType" : "CREATURE",
 
 		"animation":{
-			"affect":[{"defName":"C10SPW", "verticalPosition":"bottom"}]
+			"affect":[{"defName":"C10SPW", "verticalPosition":"bottom", "transparency" : 0.5}]
 		},
 		"sounds": {
 			"cast": "PRAYER"

+ 9 - 7
docs/Readme.md

@@ -9,10 +9,10 @@
 VCMI is an open-source recreation of Heroes of Might & Magic III engine, giving it new and extended possibilities.
 
 <p>
-<img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.3.0/Castle%20Siege.jpg?raw=true" alt="Vanilla town siege in extended window" style="height:120px;"/>
-<img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.3.0/Town%20Screen%20with%20Radial%20Menu.jpg?raw=true" alt="Vanilla town view with radial menu for touchscreen devices" style="height:120px;"/>
-<img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Big%20spellbook.jpg?raw=true" alt="Large Spellbook with German translation" style="height:120px;"/>
-<img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Quick%20Hero%20Select%20Bastion.jpg?raw=true" alt="New widget for Hero selection, featuring Pavillon Town" style="height:120px;"/>
+  <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.3.0/Castle%20Siege.jpg?raw=true" alt="Vanilla town siege in extended window" style="height:120px;"/>
+  <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.3.0/Town%20Screen%20with%20Radial%20Menu.jpg?raw=true" alt="Vanilla town view with radial menu for touchscreen devices" style="height:120px;"/>
+  <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Big%20spellbook.jpg?raw=true" alt="Large Spellbook with German translation" style="height:120px;"/>
+  <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Quick%20Hero%20Select%20Bastion.jpg?raw=true" alt="New widget for Hero selection, featuring Pavillon Town" style="height:120px;"/>
 </p>
 
 
@@ -37,10 +37,12 @@ Please see corresponding installation guide articles for details for your platfo
 - [Android](players/Installation_Android.md)
 - [iOS](players/Installation_iOS.md)
 
+See also installation guide for [Heroes Chronicles](players/Heroes_Chronicles.md).
+
 <p>
-<img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Antagarich%20Burning%20Battle.jpg?raw=true" alt="Forge Town in battle" style="height:120px;"/>
-<img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Town%20and%20Unit.jpg?raw=true" alt="Asylum town with new creature dialog" style="height:120px;"/>
-<img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Siege.jpg?raw=true" alt="Ruins town siege" style="height:120px;"/>
+  <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Antagarich%20Burning%20Battle.jpg?raw=true" alt="Forge Town in battle" style="height:120px;"/>
+  <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Town%20and%20Unit.jpg?raw=true" alt="Asylum town with new creature dialog" style="height:120px;"/>
+  <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Siege.jpg?raw=true" alt="Ruins town siege" style="height:120px;"/>
   <img src="https://github.com/vcmi/VCMI.eu/blob/master/static/img/screenshots/1.4.0/Editor.jpg?raw=true" alt="Map editor" style="height:120px;"/>
 </p>
 

+ 1 - 1
docs/developers/Building_Windows.md

@@ -115,7 +115,7 @@ Extract `ccache` to a folder of your choosing, add the folder to the `PATH` envi
 ### Compile VCMI with MinGW via MSYS2
 - Install MSYS2 from https://www.msys2.org/
 - Start the `MSYS MinGW x64`-shell
-- Install dependencies: `pacman -S mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_image mingw-w64-x86_64-SDL2_mixer mingw-w64-x86_64-SDL2_ttf mingw-w64-x86_64-boost mingw-w64-x86_64-gcc mingw-w64-x86_64-ninja mingw-w64-x86_64-qt5-static`
+- Install dependencies: `pacman -S mingw-w64-x86_64-SDL2 mingw-w64-x86_64-SDL2_image mingw-w64-x86_64-SDL2_mixer mingw-w64-x86_64-SDL2_ttf mingw-w64-x86_64-boost mingw-w64-x86_64-gcc mingw-w64-x86_64-ninja mingw-w64-x86_64-qt5-static mingw-w64-x86_64-qt5-tools mingw-w64-x86_64-tbb`
 - Generate and build solution from VCMI-root dir: `cmake --preset windows-mingw-release && cmake --build --preset windows-mingw-release`
 
 **NOTE:** This will link Qt5 statically to `VCMI_launcher.exe` and `VCMI_Mapeditor.exe`. See [PR #3421](https://github.com/vcmi/vcmi/pull/3421) for some background.

+ 1 - 1
docs/modders/Entities_Format/Spell_Format.md

@@ -167,7 +167,7 @@ TODO
 	],
 	"cast" : []
 	"hit":["C20SPX"],
-	"affect":[{"defName":"C03SPA0", "verticalPosition":"bottom"}, "C11SPA1"]
+	"affect":[{"defName":"C03SPA0", "verticalPosition":"bottom", "transparency" : 0.5}, "C11SPA1"]
 }
 ```
 

+ 34 - 0
docs/modders/HD_Graphics.md

@@ -0,0 +1,34 @@
+# HD Graphics
+
+It's possible to provide alternative high-definition graphics within mods. They will be used if any upscaling filter is activated.
+
+## Preconditions
+
+It's still necessary to add 1x standard definition graphics as before. HD graphics are seperate from usual graphics. This allows to partitially use HD for a few graphics in mod. And avoid handling huge graphics if upscaling isn't enabled.
+
+Currently following scaling factors are possible to use: 2x, 3x, 4x. You can also provide multiple of them (increases size of mod, but improves loading performance for player). It's recommend to provide 2x and 3x images.
+
+If user for example selects 3x resolution and only 2x exists in mod then the 2x images are upscaled to 3x (same for other combinations > 1x).
+
+## Mod
+
+For upscaled images you have to use following folders (next to `sprites` and `data` folders):
+- `sprites2x`, `sprites3x`, `sprites4x` for sprites
+- `data2x`, `data3x`, `data4x` for images
+
+The sprites should have the same name and folder structure as in `sprites` and `data` folder. All images that are missing in the upscaled folders are scaled with the selected upscaling filter instead of using prescaled images.
+
+### Shadows / Overlays
+
+It's also possible (but not necessary) to add high-definition shadows: Just place a image next to the normal upscaled image with the suffix `-shadow`. E.g. `TestImage.png` and `TestImage-shadow.png`.
+In future, such shadows will likely become required to correctly exclude shadow from effects such as Clone spell.
+
+Shadow images are used only for animations of following objects:
+- All adventure map objects
+- All creature animations in combat
+
+Same for overlays with `-overlay`. But overlays are **necessary** for some animation graphics. They will be colorized by VCMI.
+
+Currently needed for:
+- Flaggable adventure map objects. Overlay must contain a transparent image with white flags on it and will be used to colorize flags to owning player
+- Creature battle animations, idle and mouse hover group. Overlay must contain a transparent image with white outline of creature for highlighting on mouse hover)

+ 9 - 0
docs/players/Heroes_Chronicles.md

@@ -0,0 +1,9 @@
+# Heroes Chronicles
+
+It also possible to play the Heroes Chronicles with VCMI. You still need a completly installed VCMI (with heroes 3 sod / complete files).
+
+You also need Heroes Chronicles from [gog.com](https://www.gog.com/en/game/heroes_chronicles_all_chapters). You need to download the offline installer. CD installations are not supported yet.
+
+You can use the "Install file" button in the launcher to select the downloaded exe files. This process can take a while (especially on mobile platforms) and need some temporary free space.
+
+After that you can select Heroes Chronicles from Campaign selection menu (button or custom campaign).

+ 46 - 13
docs/players/Installation_iOS.md

@@ -4,41 +4,74 @@ You can run VCMI on iOS 12.0 and later, all devices are supported. If you wish t
 
 ## Step 1: Download and install VCMI
 
+The easiest and recommended way to install on a non-jailbroken device is to install the [AltStore Classic](https://altstore.io/) or [Sideloadly](https://sideloadly.io/). We will use AltStore as an example below. Using this method means the VCMI certificate is auto-signed automatically.
+
+i) Use [AltStore Windows](https://faq.altstore.io/altstore-classic/how-to-install-altstore-windows) or [AltStore macOS](https://faq.altstore.io/altstore-classic/how-to-install-altstore-macos) instructions to install the store depending on the operating system you are using. 
+
+If you're having trouble enabling "sync with this iOS device over Wi-Fi" press on the rectangular shape below "Account". Example shown below.
+
+![image](https://github.com/user-attachments/assets/74fe2ca2-b55c-4b05-b083-89df604248f3)
+
+ii) Download the VCMI-iOS.ipa file on your iOS device directly from the [latest releases](https://github.com/vcmi/vcmi/releases/latest).
+
+iii) To install the .ipa file on your device do one of the following:
+
+- In AltStore go to >My Apps > press + in the top left corner. Select VCMI-iOS.ipa to install,
+- or drag and drop the .ipa file into your iOS device in iTunes
+
+
+## Step 2: Installing Heroes III data files
+
+If you bought HoMM3 on [GOG](https://www.gog.com/de/game/heroes_of_might_and_magic_3_complete_edition), you can download the files directly from the browser in the device. 
+
+Launch VCMI app on the device and the launcher will prompt two files to complete the installation. Select the **.bin** file first, then the **.exe** file. This may take a few seconds. Please be patient. 
+
+
+## Step 3: Configuration settings
+Once you have installed VCMI and have the launcher opened, select Settings on the left bar. The following Video settings are recommended:
+
+- Lower reserved screen area to zero.
+- Increase interface Scaling to maximum. This number will depend on your device. For 11" iPad Air it was at 273% as an example
+
+Together, the two options should eliminate black bars and enable full screen VCMI experience. Enjoy!
+
+## Alternative Step 1: Download and install VCMI
+
 - The latest release (recommended): <https://github.com/vcmi/vcmi/releases/latest>
 - Daily builds: <https://builds.vcmi.download/branch/develop/iOS/>
 
-To run on a non-jailbroken device you need to sign the IPA file, you
-have the following options:
+To run on a non-jailbroken device you need to sign the IPA file, you have the following aternative options:
 
-- (Easiest way) [AltStore](https://altstore.io/) or [Sideloadly](https://sideloadly.io/) - can be installed on Windows or macOS, don't require dealing with signing on your own 
-- if you're on iOS 14.0-15.4.1, you can try <https://github.com/opa334/TrollStore>
+- if you're on iOS 14.0-15.4.1, you can try <https://github.com/opa334/TrollStore>. 
 - Get signer tool [here](https://dantheman827.github.io/ios-app-signer/) and a guide [here](https://forum.kodi.tv/showthread.php?tid=245978) (it's for Kodi, but the logic is the same). Signing with this app can only be done on macOS.
 - [Create signing assets on macOS from terminal](https://github.com/kambala-decapitator/xcode-auto-signing-assets). In the command replace `your.bundle.id` with something like `com.MY-NAME.vcmi`. After that use the above signer tool.
 - [Sign from any OS (Rust)](https://github.com/indygreg/PyOxidizer/tree/main/tugger-code-signing) / [alternative project (C++)](https://github.com/zhlynn/zsign). You'd still need to find a way to create signing assets (private key and provisioning profile) though.
 
-To install the signed ipa on your device, you can use Xcode or Apple Configurator (available on the Mac App Store for free). The latter also allows installing ipa from the command line, here's an example that assumes you have only 1 device connected to your Mac and the signed ipa is on your desktop:
+The easiest way to install the ipa on your device is to do one of the following:
 
-    /Applications/Apple\ Configurator.app/Contents/MacOS/cfgutil install-app ~/Desktop/vcmi.ipa
+- In AltStore go to >My Apps > press + in the top left corner. Select VCMI-iOS.ipa to install or
 
-## Step 2: Installing Heroes III data files
+- Drag and drop the .ipa file into your iOS device in iTunes
 
-Note: if you don't need in-game videos, you can omit downloading/copying file VIDEO.VID from data folder - it will save your time and space. The same applies to the Mp3 directory.
+Alternatively, to install the signed ipa on your device, you can use Xcode or Apple Configurator (available on the Mac App Store for free). The latter also allows installing ipa from the command line, here's an example that assumes you have only 1 device connected to your Mac and the signed ipa is on your desktop:
+
+    /Applications/Apple\ Configurator.app/Contents/MacOS/cfgutil install-app ~/Desktop/vcmi.ipa
 
-### Step 2.a: Installing data files with GOG offline installer
+## Alternative Step 2: Installing Heroes III data files
 
-If you bought HoMM3 on [GOG](https://www.gog.com/de/game/heroes_of_might_and_magic_3_complete_edition), you can download the files directly from the browser and install them in the launcher. Select the .bin file first, then the .exe file. This may take a few seconds. Please be patient.
+Note: if you don't need in-game videos, you can omit downloading/copying file VIDEO.VID from the Data folder - it will save your time and space. The same applies to the Mp3 directory.
 
-### Step 2.b: Installing data files with Finder or Windows explorer
+### Step 2.a: Installing data files with Finder or Windows explorer
 
 To play the game, you need to upload HoMM3 data files - **Data**, **Maps** and **Mp3** directories - to the device. Use Finder (or iTunes, if you're on Windows or your macOS is 10.14 or earlier) for that. You can also add various mods by uploading **Mods** directory. Follow [official Apple guide](https://support.apple.com/en-us/HT210598) and place files into VCMI app. Unfortunately, Finder doesn't display copy progress, give it about 10 minutes to finish.
 
-### Step 2.c: Installing data files using iOS device only
+### Step 2.b: Installing data files using iOS device only
 
 If you have data somewhere on device or in shared folder or you have downloaded it, you can copy it directly on your iPhone/iPad using Files application.
 
 Place **Data**, **Maps** and **Mp3** folders into vcmi application - it will be visible in Files along with other applications' folders.
 
-### Step 2.d: Installing data files with Xcode on macOS
+### Step 2.c: Installing data files with Xcode on macOS
 
 You can also upload files with Xcode. You need to prepare "container" for that.
 

+ 7 - 7
docs/players/Installation_macOS.md

@@ -1,15 +1,15 @@
 # Installation macOS
 
-For iOS installation look here: (Installation on iOS)[Installation_iOS.md]
-
 ## Step 1: Download and install VCMI
 
-- The latest release (recommended): <https://github.com/vcmi/vcmi/releases/latest>
+- The latest release (recommended):
+  - manually: <https://github.com/vcmi/vcmi/releases/latest>
+  - via Homebrew: `brew install --cask --no-quarantine vcmi/vcmi/vcmi`
 - Daily builds (might be unstable) 
- - Intel (x86_64) builds: <https://builds.vcmi.download/branch/develop/macOS/intel>
- - Apple Silicon (arm64) builds: <https://builds.vcmi.download/branch/develop/macOS/arm>
+  - Intel (x86_64) builds: <https://builds.vcmi.download/branch/develop/macOS/intel>
+  - Apple Silicon (arm64) builds: <https://builds.vcmi.download/branch/develop/macOS/arm>
 
-If the app doesn't open, right-click the app bundle - select *Open* menu item - press *Open* button.
+If the app doesn't open, right-click the app bundle - select *Open* menu item - press *Open* button. You may also need to allow running it in System Settings - Privacy & Security.
 
 Please report about gameplay problem on forums: [Help & Bugs](https://forum.vcmi.eu/c/international-board/help-bugs) Make sure to specify what hardware and macOS version you use.
 
@@ -21,5 +21,5 @@ If you bought HoMM3 on [GOG](https://www.gog.com/de/game/heroes_of_might_and_mag
 
 ### Step 2.b: Installing by the classic way
 
-1.  Find a way to unpack Windows Heroes III or GOG installer. For example, use `vcmibuilder` script inside app bundle or install the game with [CrossOver](https://www.codeweavers.com/crossover) or [Wineskin](https://github.com/Gcenx/WineskinServer).
+1.  Find a way to unpack Windows Heroes III or GOG installer. For example, use `vcmibuilder` script inside app bundle or install the game with [CrossOver](https://www.codeweavers.com/crossover) or [Kegworks](https://github.com/Kegworks-App/Kegworks).
 2.  Place or symlink **Data**, **Maps** and **Mp3** directories from Heroes III to:`~/Library/Application\ Support/vcmi/`

+ 1 - 1
launcher/translation/chinese.ts

@@ -1091,7 +1091,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="104"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="105"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="141"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="156"/>
         <source>Heroes Chronicles</source>
         <translation>英雄无敌历代记</translation>
     </message>

+ 1 - 1
launcher/translation/czech.ts

@@ -1085,7 +1085,7 @@ Exkluzivní celá obrazovka - hra zakryje vaši celou obrazovku a použije vybra
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="104"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="105"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="141"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="156"/>
         <source>Heroes Chronicles</source>
         <translation>Heroes Chronicles</translation>
     </message>

+ 1 - 1
launcher/translation/english.ts

@@ -1071,7 +1071,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="104"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="105"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="141"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="156"/>
         <source>Heroes Chronicles</source>
         <translation type="unfinished"></translation>
     </message>

+ 1 - 1
launcher/translation/french.ts

@@ -1090,7 +1090,7 @@ Mode exclusif plein écran - le jeu couvrira l&quot;intégralité de votre écra
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="104"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="105"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="141"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="156"/>
         <source>Heroes Chronicles</source>
         <translation type="unfinished"></translation>
     </message>

+ 30 - 30
launcher/translation/german.ts

@@ -442,17 +442,17 @@
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="648"/>
         <source>Gog files</source>
-        <translation type="unfinished"></translation>
+        <translation>Gog-Dateien</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="650"/>
         <source>All files (*.*)</source>
-        <translation type="unfinished"></translation>
+        <translation>Alle Dateien (*.*)</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="652"/>
         <source>Select files (configs, mods, maps, campaigns, gog files) to install...</source>
-        <translation type="unfinished"></translation>
+        <translation>Wähle zu installierenden Dateien aus (Konfigs, Mods, Karten, Kampagnen, Gog-Dateien)...</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="677"/>
@@ -499,7 +499,7 @@ Installation erfolgreich heruntergeladen?</translation>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="852"/>
         <source>Installing chronicles</source>
-        <translation type="unfinished"></translation>
+        <translation>Installation der Chronicles</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="925"/>
@@ -667,7 +667,7 @@ Installation erfolgreich heruntergeladen?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="484"/>
         <source>Downscaling Filter</source>
-        <translation type="unfinished"></translation>
+        <translation>Herunterskalierungsfilter</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="597"/>
@@ -692,7 +692,7 @@ Installation erfolgreich heruntergeladen?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="908"/>
         <source>Automatic (Linear)</source>
-        <translation type="unfinished"></translation>
+        <translation>Automatisch (linear)</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="274"/>
@@ -709,42 +709,42 @@ Installation erfolgreich heruntergeladen?</translation>
         <location filename="../settingsView/csettingsview_moc.ui" line="539"/>
         <location filename="../settingsView/csettingsview_moc.ui" line="1389"/>
         <source>Automatic</source>
-        <translation type="unfinished"></translation>
+        <translation>Automatisch</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="75"/>
         <source>Mods Validation</source>
-        <translation type="unfinished"></translation>
+        <translation>Mod-Validierung</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="88"/>
         <source>None</source>
-        <translation type="unfinished"></translation>
+        <translation>Keiner</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="93"/>
         <source>xBRZ x2</source>
-        <translation type="unfinished"></translation>
+        <translation>xBRZ x2</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="98"/>
         <source>xBRZ x3</source>
-        <translation type="unfinished"></translation>
+        <translation>xBRZ x3</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="103"/>
         <source>xBRZ x4</source>
-        <translation type="unfinished"></translation>
+        <translation>xBRZ x4</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="316"/>
         <source>Full</source>
-        <translation type="unfinished"></translation>
+        <translation>Voll</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="788"/>
         <source>Use scalable fonts</source>
-        <translation type="unfinished"></translation>
+        <translation>Skalierbare Schriftarten verwenden</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="859"/>
@@ -754,27 +754,27 @@ Installation erfolgreich heruntergeladen?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1079"/>
         <source>Cursor Scaling</source>
-        <translation type="unfinished"></translation>
+        <translation>Cursor-Skalierung</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1108"/>
         <source>Scalable</source>
-        <translation type="unfinished"></translation>
+        <translation>Skalierbar</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1144"/>
         <source>Miscellaneous</source>
-        <translation type="unfinished"></translation>
+        <translation>Sonstiges</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1303"/>
         <source>Font Scaling (experimental)</source>
-        <translation type="unfinished"></translation>
+        <translation>Schriftskalierung (experimentell)</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1367"/>
         <source>Original</source>
-        <translation type="unfinished"></translation>
+        <translation>Original</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1406"/>
@@ -784,7 +784,7 @@ Installation erfolgreich heruntergeladen?</translation>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="1439"/>
         <source>Basic</source>
-        <translation type="unfinished"></translation>
+        <translation>Grundlegend</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="656"/>
@@ -1059,35 +1059,35 @@ Exklusiver Vollbildmodus - das Spiel bedeckt den gesamten Bildschirm und verwend
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="48"/>
         <source>File cannot opened</source>
-        <translation type="unfinished"></translation>
+        <translation>Datei kann nicht geöffnet werden</translation>
     </message>
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="56"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="71"/>
         <source>Invalid file selected</source>
-        <translation type="unfinished">Ungültige Datei ausgewählt</translation>
+        <translation>Ungültige Datei ausgewählt</translation>
     </message>
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="56"/>
         <source>You have to select an gog installer file!</source>
-        <translation type="unfinished"></translation>
+        <translation>Sie müssen eine Gog-Installer-Datei auswählen!</translation>
     </message>
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="71"/>
         <source>You have to select an chronicle installer file!</source>
-        <translation type="unfinished"></translation>
+        <translation>Sie müssen eine Chronicle-Installationsdatei auswählen!</translation>
     </message>
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="87"/>
         <source>Extracting error!</source>
-        <translation type="unfinished">Fehler beim Extrahieren!</translation>
+        <translation>Fehler beim Extrahieren!</translation>
     </message>
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="104"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="105"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="141"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="156"/>
         <source>Heroes Chronicles</source>
-        <translation type="unfinished"></translation>
+        <translation>Heroes Chronicles</translation>
     </message>
 </context>
 <context>
@@ -1422,18 +1422,18 @@ Bitte wählen Sie ein Verzeichnis mit Heroes III: Complete Edition oder Heroes I
         <location filename="../innoextract.cpp" line="42"/>
         <source>Stream error while extracting files!
 error reason: </source>
-        <translation type="unfinished">Stream-Fehler beim Extrahieren von Dateien!
+        <translation>Stream-Fehler beim Extrahieren von Dateien!
 Fehlerursache: </translation>
     </message>
     <message>
         <location filename="../innoextract.cpp" line="55"/>
         <source>Not a supported Inno Setup installer!</source>
-        <translation type="unfinished">Kein unterstütztes Inno Setup Installationsprogramm!</translation>
+        <translation>Kein unterstütztes Inno Setup Installationsprogramm!</translation>
     </message>
     <message>
         <location filename="../innoextract.cpp" line="58"/>
         <source>VCMI was compiled without innoextract support, which is needed to extract exe files!</source>
-        <translation type="unfinished"></translation>
+        <translation>VCMI wurde ohne innoextract-Unterstützung kompiliert, die zum Extrahieren von exe-Dateien benötigt wird!</translation>
     </message>
 </context>
 <context>

+ 1 - 1
launcher/translation/polish.ts

@@ -1085,7 +1085,7 @@ Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybrane
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="104"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="105"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="141"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="156"/>
         <source>Heroes Chronicles</source>
         <translation type="unfinished"></translation>
     </message>

+ 1 - 1
launcher/translation/portuguese.ts

@@ -1085,7 +1085,7 @@ Modo de tela cheia exclusivo - o jogo cobrirá toda a sua tela e usará a resolu
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="104"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="105"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="141"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="156"/>
         <source>Heroes Chronicles</source>
         <translation>Heroes Chronicles</translation>
     </message>

+ 1 - 1
launcher/translation/russian.ts

@@ -1071,7 +1071,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="104"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="105"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="141"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="156"/>
         <source>Heroes Chronicles</source>
         <translation type="unfinished"></translation>
     </message>

+ 1 - 1
launcher/translation/spanish.ts

@@ -1084,7 +1084,7 @@ Pantalla completa - el juego cubrirá la totalidad de la pantalla y utilizará l
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="104"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="105"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="141"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="156"/>
         <source>Heroes Chronicles</source>
         <translation type="unfinished"></translation>
     </message>

+ 1 - 1
launcher/translation/swedish.ts

@@ -1085,7 +1085,7 @@ Exklusivt helskärmsläge - spelet kommer att täcka hela skärmen och använda
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="104"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="105"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="141"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="156"/>
         <source>Heroes Chronicles</source>
         <translation type="unfinished"></translation>
     </message>

+ 1 - 1
launcher/translation/ukrainian.ts

@@ -1085,7 +1085,7 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="104"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="105"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="141"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="156"/>
         <source>Heroes Chronicles</source>
         <translation type="unfinished"></translation>
     </message>

+ 1 - 1
launcher/translation/vietnamese.ts

@@ -1077,7 +1077,7 @@ Toàn màn hình riêng biệt - Trò chơi chạy toàn màn hình và dùng đ
     <message>
         <location filename="../modManager/chroniclesextractor.cpp" line="104"/>
         <location filename="../modManager/chroniclesextractor.cpp" line="105"/>
-        <location filename="../modManager/chroniclesextractor.cpp" line="141"/>
+        <location filename="../modManager/chroniclesextractor.cpp" line="156"/>
         <source>Heroes Chronicles</source>
         <translation type="unfinished"></translation>
     </message>

+ 1 - 1
lib/ArtifactUtils.cpp

@@ -236,7 +236,7 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createArtifact(const ArtifactID &
 		assert(art);
 
 		auto * artInst = new CArtifactInstance(art);
-		if(art->isCombined())
+		if(art->isCombined() && !art->isFused())
 		{
 			for(const auto & part : art->getConstituents())
 				artInst->addPart(createArtInst(part), ArtifactPosition::PRE_FIRST);

+ 0 - 1
lib/CArtHandler.cpp

@@ -192,7 +192,6 @@ bool CArtifact::isTradable() const
 	switch(id.toEnum())
 	{
 	case ArtifactID::SPELLBOOK:
-	case ArtifactID::GRAIL:
 		return false;
 	default:
 		return !isBig();

+ 2 - 2
lib/CGameInfoCallback.cpp

@@ -957,12 +957,12 @@ void CGameInfoCallback::calculatePaths( const CGHeroInstance *hero, CPathsInfo &
 
 const CArtifactInstance * CGameInfoCallback::getArtInstance( ArtifactInstanceID aid ) const
 {
-	return gs->map->artInstances[aid.num];
+	return gs->map->artInstances.at(aid.num);
 }
 
 const CGObjectInstance * CGameInfoCallback::getObjInstance( ObjectInstanceID oid ) const
 {
-	return gs->map->objects[oid.num];
+	return gs->map->objects.at(oid.num);
 }
 
 const CArtifactSet * CGameInfoCallback::getArtSet(const ArtifactLocation & loc) const

+ 10 - 0
lib/CMakeLists.txt

@@ -790,6 +790,16 @@ if(WIN32)
 	)
 endif()
 
+# Use '-Wa,-mbig-obj' for files that generate very large object files
+# when compiling with MinGW lest you get "too many sections" assembler errors
+if(MINGW AND CMAKE_BUILD_TYPE STREQUAL "Debug")
+	set_source_files_properties(
+		serializer/SerializerReflection.cpp
+		IGameCallback.cpp
+		PROPERTIES
+		COMPILE_OPTIONS "-Wa,-mbig-obj")
+endif()
+
 vcmi_set_output_dir(vcmi "")
 
 enable_pch(vcmi)

Some files were not shown because too many files changed in this diff