Переглянути джерело

Merge remote-tracking branch 'origin/develop' into custom_objects_per_zone

Tomasz Zieliński 1 рік тому
батько
коміт
79c4d7d51f
100 змінених файлів з 784 додано та 309 видалено
  1. 11 5
      AI/BattleAI/AttackPossibility.cpp
  2. 4 1
      AI/BattleAI/BattleEvaluator.cpp
  3. 0 3
      AI/Nullkiller/Goals/RecruitHero.cpp
  4. 8 0
      AI/StupidAI/StupidAI.cpp
  5. 3 0
      CI/msvc/before_install.sh
  6. 3 3
      CMakeLists.txt
  7. 54 10
      ChangeLog.md
  8. 8 3
      Mods/vcmi/config/vcmi/english.json
  9. 11 8
      Mods/vcmi/config/vcmi/german.json
  10. 10 8
      Mods/vcmi/config/vcmi/portuguese.json
  11. 7 3
      client/CServerHandler.cpp
  12. 14 0
      client/Client.cpp
  13. 3 1
      client/Client.h
  14. 28 21
      client/NetPacksClient.cpp
  15. 3 2
      client/NetPacksLobbyClient.cpp
  16. 1 2
      client/adventureMap/AdventureMapWidget.cpp
  17. 6 17
      client/battle/BattleStacksController.cpp
  18. 3 1
      client/eventsSDL/InputSourceMouse.cpp
  19. 1 1
      client/globalLobby/GlobalLobbyInviteWindow.cpp
  20. 1 1
      client/globalLobby/GlobalLobbyLoginWindow.cpp
  21. 1 1
      client/globalLobby/GlobalLobbyRoomWindow.cpp
  22. 1 1
      client/globalLobby/GlobalLobbyServerSetup.cpp
  23. 6 5
      client/gui/CGuiHandler.cpp
  24. 4 3
      client/gui/CursorHandler.cpp
  25. 8 0
      client/gui/EventDispatcher.cpp
  26. 2 0
      client/gui/EventDispatcher.h
  27. 3 1
      client/gui/EventsReceiver.h
  28. 6 3
      client/gui/InterfaceObjectConfigurable.cpp
  29. 0 31
      client/lobby/CBonusSelection.cpp
  30. 0 14
      client/lobby/CBonusSelection.h
  31. 1 1
      client/lobby/CSelectionBase.cpp
  32. 2 2
      client/lobby/OptionsTab.cpp
  33. 2 0
      client/mainmenu/CHighScoreScreen.cpp
  34. 30 1
      client/mainmenu/CMainMenu.cpp
  35. 4 0
      client/mainmenu/CMainMenu.h
  36. 2 2
      client/mainmenu/CStatisticScreen.cpp
  37. 13 5
      client/mapView/MapRenderer.cpp
  38. 6 1
      client/mapView/MapViewActions.cpp
  39. 1 0
      client/mapView/MapViewActions.h
  40. 1 7
      client/media/CEmptyVideoPlayer.h
  41. 8 24
      client/media/CVideoHandler.cpp
  42. 3 4
      client/media/CVideoHandler.h
  43. 1 4
      client/media/IVideoPlayer.h
  44. 93 6
      client/render/AssetGenerator.cpp
  45. 9 3
      client/render/AssetGenerator.h
  46. 2 2
      client/render/Canvas.cpp
  47. 1 1
      client/render/IImage.h
  48. 39 0
      client/render/ImageLocator.cpp
  49. 6 1
      client/render/ImageLocator.h
  50. 1 2
      client/renderSDL/ImageScaled.cpp
  51. 49 17
      client/renderSDL/RenderHandler.cpp
  52. 1 1
      client/renderSDL/RenderHandler.h
  53. 30 7
      client/renderSDL/SDLImage.cpp
  54. 3 2
      client/renderSDL/SDLImage.h
  55. 9 18
      client/widgets/Images.cpp
  56. 1 1
      client/widgets/Images.h
  57. 13 2
      client/widgets/VideoWidget.cpp
  58. 3 0
      client/widgets/VideoWidget.h
  59. 16 1
      client/windows/CCastleInterface.cpp
  60. 1 1
      client/windows/CHeroOverview.cpp
  61. 7 7
      client/windows/CKingdomInterface.cpp
  62. 7 5
      client/windows/CWindowObject.cpp
  63. 49 1
      client/windows/GUIClasses.cpp
  64. 17 1
      client/windows/GUIClasses.h
  65. 35 1
      client/windows/InfoWindows.cpp
  66. 8 0
      client/windows/InfoWindows.h
  67. 12 0
      client/windows/settings/AdventureOptionsTab.cpp
  68. 3 1
      client/windows/settings/GeneralOptionsTab.cpp
  69. 7 0
      client/windows/settings/SettingsMainWindow.cpp
  70. 1 0
      client/windows/settings/SettingsMainWindow.h
  71. 0 1
      clientapp/CMakeLists.txt
  72. 7 20
      clientapp/EntryPoint.cpp
  73. 1 1
      clientapp/VCMI_client.rc
  74. 0 0
      clientapp/icons/generate_icns.py
  75. 0 0
      clientapp/icons/vcmi.ico
  76. 0 0
      clientapp/icons/vcmiclient.1024x1024.png
  77. 0 0
      clientapp/icons/vcmiclient.128x128.png
  78. 0 0
      clientapp/icons/vcmiclient.16x16.png
  79. 0 0
      clientapp/icons/vcmiclient.2048x2048.png
  80. 0 0
      clientapp/icons/vcmiclient.22x22.png
  81. 0 0
      clientapp/icons/vcmiclient.256x256.png
  82. 0 0
      clientapp/icons/vcmiclient.32x32.png
  83. 0 0
      clientapp/icons/vcmiclient.48x48.png
  84. 0 0
      clientapp/icons/vcmiclient.512x512.png
  85. 0 0
      clientapp/icons/vcmiclient.64x64.png
  86. 0 0
      clientapp/icons/vcmiclient.desktop
  87. 0 0
      clientapp/icons/vcmiclient.svg
  88. 1 2
      conanfile.py
  89. 9 0
      config/bonuses.json
  90. 4 0
      config/creatures/castle.json
  91. 5 1
      config/schemas/settings.json
  92. 4 0
      config/schemas/spell.json
  93. 28 2
      config/widgets/settings/adventureOptionsTab.json
  94. 5 1
      docs/modders/Bonus/Bonus_Types.md
  95. 3 0
      docs/modders/Entities_Format/Spell_Format.md
  96. 1 0
      docs/modders/Entities_Format/Town_Building_Format.md
  97. 1 0
      docs/modders/Translations.md
  98. 1 0
      include/vcmi/spells/Spell.h
  99. 1 0
      launcher/CMakeLists.txt
  100. 6 0
      launcher/modManager/cmodlistview_moc.cpp

+ 11 - 5
AI/BattleAI/AttackPossibility.cpp

@@ -44,24 +44,30 @@ void DamageCache::buildObstacleDamageCache(std::shared_ptr<HypotheticBattle> hb,
 		if(!triggerIsNegative)
 			continue;
 
-		const auto * hero = hb->battleGetFightingHero(spellObstacle->casterSide);
-		auto caster = spells::ObstacleCasterProxy(hb->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle);
+		std::unique_ptr<spells::BattleCast> cast = nullptr;
+		if(spellObstacle->obstacleType == SpellCreatedObstacle::EObstacleType::SPELL_CREATED)
+		{
+			const auto * hero = hb->battleGetFightingHero(spellObstacle->casterSide);
+			auto caster = spells::ObstacleCasterProxy(hb->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle);
+			cast = std::make_unique<spells::BattleCast>(spells::BattleCast(hb.get(), &caster, spells::Mode::PASSIVE, obst->getTrigger().toSpell()));
+		}
 
 		auto affectedHexes = obst->getAffectedTiles();
 		auto stacks = hb->battleGetUnitsIf([](const battle::Unit * u) -> bool {
 			return u->alive() && !u->isTurret() && u->getPosition().isValid();
 		});
 
+		std::shared_ptr<HypotheticBattle> inner = std::make_shared<HypotheticBattle>(hb->env, hb);
+
 		for(auto stack : stacks)
 		{
-			std::shared_ptr<HypotheticBattle> inner = std::make_shared<HypotheticBattle>(hb->env, hb);
-			auto cast = spells::BattleCast(hb.get(), &caster, spells::Mode::PASSIVE, obst->getTrigger().toSpell());
 			auto updated = inner->getForUpdate(stack->unitId());
 
 			spells::Target target;
 			target.push_back(spells::Destination(updated.get()));
 
-			cast.castEval(inner->getServerCallback(), target);
+			if(cast)
+				cast->castEval(inner->getServerCallback(), target);
 
 			auto damageDealt = stack->getAvailableHealth() - updated->getAvailableHealth();
 

+ 4 - 1
AI/BattleAI/BattleEvaluator.cpp

@@ -337,7 +337,10 @@ BattleAction BattleEvaluator::moveOrAttack(const CStack * stack, BattleHex hex,
 	}
 	else
 	{
-		return BattleAction::makeMove(stack, hex);
+		if(stack->position == hex)
+			return BattleAction::makeDefend(stack);
+		else
+			return BattleAction::makeMove(stack, hex);
 	}
 }
 

+ 0 - 3
AI/Nullkiller/Goals/RecruitHero.cpp

@@ -74,9 +74,6 @@ void RecruitHero::accept(AIGateway * ai)
 
 		ai->nullkiller->heroManager->update();
 	}
-
-	if(t->visitingHero)
-		ai->moveHeroToTile(t->visitablePos(), t->visitingHero.get());
 }
 
 }

+ 8 - 0
AI/StupidAI/StupidAI.cpp

@@ -296,7 +296,11 @@ BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stac
 	for(auto hex : hexes)
 	{
 		if(vstd::contains(avHexes, hex))
+		{
+			if(stack->position == hex)
+				return BattleAction::makeDefend(stack);
 			return BattleAction::makeMove(stack, hex);
+		}
 
 		if(stack->coversPos(hex))
 		{
@@ -336,7 +340,11 @@ BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stac
 			}
 
 			if(vstd::contains(avHexes, currentDest))
+			{
+				if(stack->position == currentDest)
+					return BattleAction::makeDefend(stack);
 				return BattleAction::makeMove(stack, currentDest);
+			}
 
 			currentDest = reachability.predecessors[currentDest];
 		}

+ 3 - 0
CI/msvc/before_install.sh

@@ -5,3 +5,6 @@ curl -LfsS -o "vcpkg-export-${VCMI_BUILD_PLATFORM}-windows-v143.7z" \
 #rm -r -f vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug
 #mkdir -p vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
 #cp vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/bin/* vcpkg/installed/${VCMI_BUILD_PLATFORM}-windows/debug/bin
+
+DUMPBIN_DIR=$(vswhere -latest -find **/dumpbin.exe | head -n 1)
+dirname "$DUMPBIN_DIR" > $GITHUB_PATH

+ 3 - 3
CMakeLists.txt

@@ -847,10 +847,10 @@ if(WIN32)
 	endif()
 	# set the install/unistall icon used for the installer itself
 	# There is a bug in NSI that does not handle full unix paths properly.
-	set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/client\\\\vcmi.ico")
-	set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/client\\\\vcmi.ico")
+	set(CPACK_NSIS_MUI_ICON "${CMAKE_CURRENT_SOURCE_DIR}/clientapp/icons\\\\vcmi.ico")
+	set(CPACK_NSIS_MUI_UNIICON "${CMAKE_CURRENT_SOURCE_DIR}/clientapp/icons\\\\vcmi.ico")
 	# set the package header icon for MUI
-	set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/client\\\\vcmi.ico")
+	set(CPACK_PACKAGE_ICON "${CMAKE_SOURCE_DIR}/clientapp/icons\\\\vcmi.ico")
 
 	set(CPACK_NSIS_MENU_LINKS "http://vcmi.eu/" "VCMI Web Site")
 

+ 54 - 10
ChangeLog.md

@@ -1,27 +1,32 @@
-# 1.5.5 -> 1.6.0 (in development)
+# 1.5.7 -> 1.6.0 (in development)
+
+### Major changes
+* Implemented handicap system, with options to reduce income and growth in addition to starting resources restriction
+* Game will now show statistics after scenario completion, such as resources or army strength over time
+* Implemented spell quick selection panel in combat
+* Implemented adventure map overlay accessible via Alt key that highlights all interactive objects on screen
+* Implemented xBRZ upscaling filter
+* It is now possible to import data from Heroes Chronicles (gog.com installer only) as custom campaigns
 
 ### General
 * Saved game size reduced by approximately 3 times, especially for large maps or games with a large number of mods.
 * Added option to start vcmi server on randomly selected TCP port
 * Fixed potential desynchronization between server and clients on randomization of map objects if client and server run on different operating systems
-* It is now possible to generate game statistics using `!statistic` command in game chat
 
 ### Stability
 * Fixed possible crash on connecting bluetooth mouse during gameplay on Android
 * VCMI will now write more detailed information to log file on crash due to uncaught exception
 
-### Multiplayer
-* Implemented handicap system, with options to reduce income and growth in addition to starting resources restriction
-
 ### Mechanics
 * Arrow tower will now prefer to attack more units that are viewed most dangerous instead of simply attacking top-most unit
 * Score in campaigns will now be correctly tracked for games loaded from a save
 * Fixed incorrect direction of Dragon Breath attack in some cases if wide creature attacks another wide creature
 * Map events and town events are now triggered on start of turn of player affected by event, in line with H3 instead of triggering on start of new day for all players
+* Neutral towns should now have initial garrison and weekly growth of garrison identical to H3
+* It is now possible to buy a new war machine in town if hero has different war machine in the slot
 
 ### Interface
-* Implemented spell quick selection panel in combat
-* Implemented adventure map overlay accessible via Alt key that highlights all interactive objects on screen
+* Added option to drag map with right-click
 * Added hotkeys to reorder list of owned towns or heroes
 * The number of units resurrected using the Life Drain ability is now written to the combat log.
 * Fixed playback of audio stream with different formats from video files in some Heroes 3 versions
@@ -29,6 +34,7 @@
 * When resuming video playback, the video will now be continued instead of being restarted.
 * Reduced video decompression artefacts for video formats that store RGB rather than YUV data.
 * Fixed order of popup dialogs after battle.
+* Right-click on wandering monster on adventure map will now also show creature level and faction it belongs to
 * Added additional information to map right-click popup dialog: map author, map creation date, map version
 * Added scrollbars for selection of starting town, starting hero, and tavern invite if number of objects is too large to fit into the screen
 * Fixed incorrect battle turn queue displaying incorrect turn order when all units have waited
@@ -37,10 +43,26 @@
 * Added unassigned by default shortcuts for toggling visibility of visitable and blocked tiles
 * Spellbook button in battle is now blocked if hero has no spellbook 
 * Adventure map will no longer scroll if window is not in focus
+* Removed second info window when player loses his last town
+* Fixed hero path not updating correctly after hiring or dismissing creatures
+* Fixed missing description of a stack artifact when accessed through unit window
+* Fixed text overflow on campaign scenario window if campaign name is too long
+* Intro videos are now played inside a frame on resolutions higher than 800x600 instead of filling entire screen
 
 ### Random Maps Generator
 * Implemented connection option 'forcePortal'
 * It is now possible to connect zone to itself using pair of portals
+* It is now possible for a random map template to change game settings
+* Road settings will now be correctly loaded when opening random map setup tab
+
+### Campaigns
+* It is now possible to use .zip archive for VCMI campaigns instead of raw gzip stream
+* Fixed handling of hero placeholders in VCMI map format (.vmap)
+* Fixed not functioning hero carryover in VCMI campaigns
+* Added support for campaign outro videos, such as outro in "Song for the Father" campaign
+* Added support for rim image for intro video, such as opening videos in Heroes Chronicles
+* Added support for custom loading screen in campaigns
+* Added support for custom region definitions (such as background images) for VCMI campaigns 
 
 ### AI
 * Fixed bug where BattleAI attempts to move double-wide unit to an unreachable hex
@@ -48,6 +70,13 @@
 * Nullkiller is now capable of visiting configurable objects from mods
 * Nullkiller now uses whirlpools for map movement
 * AI can now correctly estimate effect of Dragon Breath and other similar abilities
+* Battle AI should now avoid ending turn on the moat
+* Fixed case where BattleAI will go around the map to attack ranged units if direct path is blocked by another unit
+* Fixed evaluation of effects of waiting if unit is under haste effect by Battle AI
+* Battle AI can now use location spells
+
+### Launcher
+* Added Swedish translation
 
 ### Map Editor
 * Implemented tracking of building requirements for Building Dialog
@@ -55,20 +84,35 @@
 * It is now possible to set spells allowed or required to be present in Mages Guild
 * It is now possible to add timed events to a town
 * Fixed editor not marking mod as dependency if spells from mod are used in town Mages Guild or in hero starting spells
+* It is now possible to choose road types for random map generation in editor
+* Validator will now warn in case if map has players with no heroes or towns
 
 ### Modding
 * Fixed multiple issues with configurable town buildings
 * Added documentation for configurable town buildings. See docs/Moddders/Entities_Format/Town_Buildings_Format.md
 * Replaced some of hardcoded town buildings with configurable buildings. These building types are now deprecated and will be removed in future.
+* Added support for explicitly visitable town buildings that will activate only on click and not on construction or on hero visit (Mana Vortex from HotA)
+* It is now possible to add guards to a configurable objects. All H3 creature banks are now implemented as configurable object.
+* It is now possible to define starting position of units in a guarded configurable object
+* Added `canCastWithoutSkip` parameter to a spell. If such spell is cast by a creature, its turn will not end after a spellcast
+* Mod can now provide pregenerated assets in place of autogenerated, such as large spellbook.
 * Added support for custom music and opening sound for a battlefield
 * Added support for multiple music tracks for towns
 * Added support for multiple music tracks for terrains on adventure map
 * Fixed several cases where vcmi will report errors in json without specifying filename of invalid file
-* It is now possible to use .zip archive for VCMI campaigns instead of raw gzip stream
-* Added support for custom region definitions (such as background images) for VCMI campaigns 
 * It is now possible to change minimal values of primary skills for heroes
-* Added support for HotA bank building from Factory
+* Added support for HotA Bank building from Factory
+* Added support for HotA Grotto buiding from Cove
 * Added support for HotA-style 8th creature in town
+* Town building can now define war machine produced in this building (Blacksmith or Ballista Yard)
+* Town building can now define provided fortifications - health of walls, towers, presence of moat, identifier of creature shooter on tower
+* Added DISINTEGRATE bonus
+* Added INVINCIBLE bonus
+* Added THIEVES_GUILD_ACCESS bonus that changes amount of information available in thieves guild
+* TimesStackLevelUpdater now supports commanders
+* Black market restock period setting now correctly restocks on specified date instead of restocking on all dates other than specified one
+* Game now supports vp8 and vp9 encoding for video files on all platforms
+* Json Validator will now attempt to detect typos when encountering unknown property in Json
 
 # 1.5.6 -> 1.5.7
 

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

@@ -12,6 +12,7 @@
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "Overpowering",
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Deadly",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Impossible",
+	"vcmi.adventureMap.monsterLevel"            : "\n\nLevel %LEVEL %TOWN unit",
 
 	"vcmi.adventureMap.confirmRestartGame"               : "Are you sure you want to restart the game?",
 	"vcmi.adventureMap.noTownWithMarket"                 : "There are no available marketplaces!",
@@ -234,8 +235,10 @@
 	"vcmi.adventureOptions.borderScroll.help" : "{Border Scrolling}\n\nScroll adventure map when cursor is adjacent to window edge. Can be disabled by holding down CTRL key.",
 	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info Panel Creature Management",
 	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info Panel Creature Management}\n\nAllows rearranging creatures in info panel instead of cycling between default components.",
-	"vcmi.adventureOptions.leftButtonDrag.hover" : "Left Click Drag Map",
-	"vcmi.adventureOptions.leftButtonDrag.help" : "{Left Click Drag Map}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view.",
+	"vcmi.adventureOptions.leftButtonDrag.hover" : "Left Click Drag",
+	"vcmi.adventureOptions.leftButtonDrag.help" : "{Left Click Drag}\n\nWhen enabled, moving mouse with left button pressed will drag adventure map view.",
+	"vcmi.adventureOptions.rightButtonDrag.hover" : "Right Click Drag",
+	"vcmi.adventureOptions.rightButtonDrag.help" : "{Right Click Drag}\n\nWhen enabled, moving mouse with right button pressed will drag adventure map view.",
 	"vcmi.adventureOptions.smoothDragging.hover" : "Smooth Map Dragging",
 	"vcmi.adventureOptions.smoothDragging.help" : "{Smooth Map Dragging}\n\nWhen enabled, map dragging has a modern run out effect.",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Skip fading effects",
@@ -662,5 +665,7 @@
 	"core.bonus.WIDE_BREATH.name": "Wide breath",
 	"core.bonus.WIDE_BREATH.description": "Wide breath attack (multiple hexes)",
 	"core.bonus.DISINTEGRATE.name": "Disintegrate",
-	"core.bonus.DISINTEGRATE.description": "No corpse remains after death"
+	"core.bonus.DISINTEGRATE.description": "No corpse remains after death",
+	"core.bonus.INVINCIBLE.name": "Invincible",
+	"core.bonus.INVINCIBLE.description": "Cannot be affected by anything"
 }

+ 11 - 8
Mods/vcmi/config/vcmi/german.json

@@ -12,6 +12,7 @@
 	"vcmi.adventureMap.monsterThreat.levels.9"  : "Überwältigend",
 	"vcmi.adventureMap.monsterThreat.levels.10" : "Tödlich",
 	"vcmi.adventureMap.monsterThreat.levels.11" : "Unmöglich",
+	"vcmi.adventureMap.monsterLevel"            : "\n\nStufe %LEVEL %TOWN-Einheit",
 
 	"vcmi.adventureMap.confirmRestartGame"               : "Seid Ihr sicher, dass Ihr das Spiel neu starten wollt?",
 	"vcmi.adventureMap.noTownWithMarket"                 : "Kein Marktplatz verfügbar!",
@@ -203,13 +204,13 @@
 	"vcmi.systemOptions.resolutionMenu.hover"   : "Wähle Auflösung",
 	"vcmi.systemOptions.resolutionMenu.help"    : "Ändere die Spielauflösung.",
 	"vcmi.systemOptions.scalingButton.hover"   : "Interface-Skalierung: %p%",
-	"vcmi.systemOptions.scalingButton.help"    : "{Interface-Skalierung}\n\nÄndern der Skalierung des Interfaces im Spiel",
+	"vcmi.systemOptions.scalingButton.help"    : "{Interface-Skalierung}\n\nÄndern der Interface-Skalierung im Spiel",
 	"vcmi.systemOptions.scalingMenu.hover"     : "Skalierung auswählen",
-	"vcmi.systemOptions.scalingMenu.help"      : "Ändern der Skalierung des Interfaces im Spiel.",
-	"vcmi.systemOptions.longTouchButton.hover"   : "Berührungsdauer für langer Touch: %d ms", // Translation note: "ms" = "milliseconds"
-	"vcmi.systemOptions.longTouchButton.help"    : "{Berührungsdauer für langer Touch}\n\nBei Verwendung des Touchscreens erscheinen Popup-Fenster nach Berührung des Bildschirms für die angegebene Dauer (in Millisekunden)",
-	"vcmi.systemOptions.longTouchMenu.hover"     : "Wähle Berührungsdauer für langer Touch",
-	"vcmi.systemOptions.longTouchMenu.help"      : "Ändere die Berührungsdauer für den langen Touch",
+	"vcmi.systemOptions.scalingMenu.help"      : "Ändern der Interface-Skalierung im Spiel.",
+	"vcmi.systemOptions.longTouchButton.hover"   : "Dauer für langer Touch: %d ms", // Translation note: "ms" = "milliseconds"
+	"vcmi.systemOptions.longTouchButton.help"    : "{Dauer für langer Touch}\n\nBei Verwendung des Touchscreens erscheinen Popup-Fenster nach Berührung des Bildschirms für die angegebene Dauer (in Millisekunden)",
+	"vcmi.systemOptions.longTouchMenu.hover"     : "Wähle Dauer für Touch",
+	"vcmi.systemOptions.longTouchMenu.help"      : "Ändere die Dauer für den langen Touch",
 	"vcmi.systemOptions.longTouchMenu.entry"     : "%d Millisekunden",
 	"vcmi.systemOptions.framerateButton.hover"  : "FPS anzeigen",
 	"vcmi.systemOptions.framerateButton.help"   : "{FPS anzeigen}\n\n Schaltet die Sichtbarkeit des Zählers für die Bilder pro Sekunde in der Ecke des Spielfensters um.",
@@ -234,8 +235,10 @@
 	"vcmi.adventureOptions.borderScroll.help" : "{Scrollen am Rand}\n\nScrollt die Abenteuerkarte, wenn sich der Cursor neben dem Fensterrand befindet. Kann mit gedrückter STRG-Taste deaktiviert werden.",
 	"vcmi.adventureOptions.infoBarCreatureManagement.hover" : "Info-Panel Kreaturenmanagement",
 	"vcmi.adventureOptions.infoBarCreatureManagement.help" : "{Info-Panel Kreaturenmanagement}\n\nErmöglicht die Neuanordnung von Kreaturen im Info-Panel, anstatt zwischen den Standardkomponenten zu wechseln",
-	"vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen der Karte mit Links",
-	"vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen der Karte mit Links}\n\nWenn aktiviert, wird die Maus bei gedrückter linker Taste in die Kartenansicht gezogen",
+	"vcmi.adventureOptions.leftButtonDrag.hover" : "Ziehen mit Links",
+	"vcmi.adventureOptions.leftButtonDrag.help" : "{Ziehen mit Links}\n\nWenn aktiviert, kann mit gedrückter linker Taste die Kartenansicht gezogen werden",
+	"vcmi.adventureOptions.rightButtonDrag.hover" : "Ziehen mit Rechts",
+	"vcmi.adventureOptions.rightButtonDrag.help" : "{Ziehen mit Rechts}\n\nWenn aktiviert, kann mit gedrückter rechter Taste die Kartenansicht gezogen werden",
 	"vcmi.adventureOptions.smoothDragging.hover" : "Nahtloses Ziehen der Karte",
 	"vcmi.adventureOptions.smoothDragging.help" : "{Nahtloses Ziehen der Karte}\n\nWenn aktiviert hat das Ziehen der Karte einen sanften Auslaufeffekt.",
 	"vcmi.adventureOptions.skipAdventureMapAnimations.hover" : "Fading-Effekte überspringen",

+ 10 - 8
Mods/vcmi/config/vcmi/portuguese.json

@@ -3,9 +3,9 @@
 	"vcmi.adventureMap.monsterThreat.levels.0"  : "Sem Esforço",
 	"vcmi.adventureMap.monsterThreat.levels.1"  : "Muito Fraca",
 	"vcmi.adventureMap.monsterThreat.levels.2"  : "Fraca",
-	"vcmi.adventureMap.monsterThreat.levels.3"  : "Um pouco mais fraca",
+	"vcmi.adventureMap.monsterThreat.levels.3"  : "Um Pouco Mais Fraca",
 	"vcmi.adventureMap.monsterThreat.levels.4"  : "Igual",
-	"vcmi.adventureMap.monsterThreat.levels.5"  : "Um pouco mais forte",
+	"vcmi.adventureMap.monsterThreat.levels.5"  : "Um Pouco Mais Forte",
 	"vcmi.adventureMap.monsterThreat.levels.6"  : "Forte",
 	"vcmi.adventureMap.monsterThreat.levels.7"  : "Muito Forte",
 	"vcmi.adventureMap.monsterThreat.levels.8"  : "Desafiante",
@@ -163,8 +163,8 @@
 	"vcmi.systemOptions.townsGroup" : "Tela da Cidade",
 
 	"vcmi.statisticWindow.statistics" : "Estatísticas",
-	"vcmi.statisticWindow.tsvCopy" : "Copiar dados",
-	"vcmi.statisticWindow.selectView" : "Selecionar visualização",
+	"vcmi.statisticWindow.tsvCopy" : "Para a área de transf.",
+	"vcmi.statisticWindow.selectView" : "Selec. visualização",
 	"vcmi.statisticWindow.value" : "Valor",
 	"vcmi.statisticWindow.title.overview" : "Visão geral",
 	"vcmi.statisticWindow.title.resources" : "Recursos",
@@ -178,7 +178,7 @@
 	"vcmi.statisticWindow.title.experience" : "Experiência",
 	"vcmi.statisticWindow.title.resourcesSpentArmy" : "Custo do exército",
 	"vcmi.statisticWindow.title.resourcesSpentBuildings" : "Custo de construção",
-	"vcmi.statisticWindow.title.mapExplored" : "Exploração do mapa",
+	"vcmi.statisticWindow.title.mapExplored" : "Mapa explorado",
 	"vcmi.statisticWindow.param.playerName" : "Nome do jogador",
 	"vcmi.statisticWindow.param.daysSurvived" : "Dias sobrevividos",
 	"vcmi.statisticWindow.param.maxHeroLevel" : "Nível máximo do herói",
@@ -325,8 +325,8 @@
 	"vcmi.townHall.missingBase"             : "A construção base %s deve ser construída primeiro",
 	"vcmi.townHall.noCreaturesToRecruit"    : "Não há criaturas para recrutar!",
 
-	"vcmi.townStructure.bank.borrow" : "Você entra no banco. Um banqueiro o vê e diz: \"Temos uma oferta especial para você. Você pode tomar um empréstimo de 2500 de ouro por 5 dias. Você terá que pagar 500 de ouro todos os dias.\"",
-	"vcmi.townStructure.bank.payBack" : "Você entra no banco. Um banqueiro o vê e diz: \"Você já pegou um empréstimo. Pague-o antes de tomar um novo.\"",
+	"vcmi.townStructure.bank.borrow" : "Você entra no banco. Um banqueiro o vê e diz: \"Temos uma oferta especial para você. Você pode pegar um empréstimo de 2500 de ouro por 5 dias. Você terá que pagar 500 de ouro todos os dias.\"",
+	"vcmi.townStructure.bank.payBack" : "Você entra no banco. Um banqueiro o vê e diz: \"Você já pegou um empréstimo. Pague-o antes de pegar um novo.\"",
 
 	"vcmi.logicalExpressions.anyOf"  : "Qualquer um dos seguintes:",
 	"vcmi.logicalExpressions.allOf"  : "Todos os seguintes:",
@@ -660,5 +660,7 @@
 	"core.bonus.WATER_IMMUNITY.name" : "Imunidade à Água",
 	"core.bonus.WATER_IMMUNITY.description" : "Imune a todos os feitiços da escola de magia da Água",
 	"core.bonus.WIDE_BREATH.name" : "Sopro Amplo",
-	"core.bonus.WIDE_BREATH.description" : "Ataque de sopro amplo (vários hexágonos)"
+	"core.bonus.WIDE_BREATH.description" : "Ataque de sopro amplo (vários hexágonos)",
+	"core.bonus.DISINTEGRATE.name": "Desintegrar",
+	"core.bonus.DISINTEGRATE.description": "Nenhum corpo permanece após a morte"
 }

+ 7 - 3
client/CServerHandler.cpp

@@ -23,6 +23,7 @@
 #include "lobby/CLobbyScreen.h"
 #include "lobby/CBonusSelection.h"
 #include "windows/InfoWindows.h"
+#include "windows/GUIClasses.h"
 #include "media/CMusicHandler.h"
 #include "media/IVideoPlayer.h"
 
@@ -661,10 +662,13 @@ void CServerHandler::endGameplay()
 	{
 		GH.curInt = CMM.get();
 		CMM->enable();
+		CMM->playMusic();
 	}
 	else
 	{
-		GH.curInt = CMainMenu::create().get();
+		auto mainMenu = CMainMenu::create();
+		GH.curInt = mainMenu.get();
+		mainMenu->playMusic();
 	}
 }
 
@@ -708,10 +712,10 @@ void CServerHandler::startCampaignScenario(HighScoreParameter param, std::shared
 		else
 		{
 			CMM->openCampaignScreen(ourCampaign->campaignSet);
-			if(!ourCampaign->getOutroVideo().empty() && CCS->videoh->open(ourCampaign->getOutroVideo(), false))
+			if(!ourCampaign->getOutroVideo().empty() && CCS->videoh->open(ourCampaign->getOutroVideo(), 1))
 			{
 				CCS->musich->stopMusic();
-				GH.windows().createAndPushWindow<CampaignRimVideo>(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), [campaignScoreCalculator, statistic](){
+				GH.windows().createAndPushWindow<VideoWindow>(ourCampaign->getOutroVideo(), ourCampaign->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : ourCampaign->getVideoRim(), false, 1, [campaignScoreCalculator, statistic](bool skipped){
 					GH.windows().createAndPushWindow<CHighScoreInputScreen>(true, *campaignScoreCalculator, statistic);
 				});
 			}

+ 14 - 0
client/Client.cpp

@@ -13,6 +13,7 @@
 
 #include "CGameInfo.h"
 #include "CPlayerInterface.h"
+#include "PlayerLocalState.h"
 #include "CServerHandler.h"
 #include "ClientNetPackVisitors.h"
 #include "adventureMap/AdventureMapInterface.h"
@@ -495,6 +496,19 @@ void CClient::startPlayerBattleAction(const BattleID & battleID, PlayerColor col
 	}
 }
 
+void CClient::updatePath(const ObjectInstanceID & id)
+{
+	invalidatePaths();
+	auto hero = getHero(id);
+	updatePath(hero);
+}
+
+void CClient::updatePath(const CGHeroInstance * hero)
+{
+	if(LOCPLINT && hero)
+		LOCPLINT->localState->verifyPath(hero);
+}
+
 void CClient::invalidatePaths()
 {
 	boost::unique_lock<boost::mutex> pathLock(pathCacheMutex);

+ 3 - 1
client/Client.h

@@ -150,7 +150,9 @@ public:
 	void battleFinished(const BattleID & battleID);
 	void startPlayerBattleAction(const BattleID & battleID, PlayerColor color);
 
-	void invalidatePaths();
+	void invalidatePaths(); // clears this->pathCache()
+	void updatePath(const ObjectInstanceID & heroID); // invalidatePaths and update displayed hero path 
+	void updatePath(const CGHeroInstance * hero);
 	std::shared_ptr<const CPathsInfo> getPathsInfo(const CGHeroInstance * h);
 
 	friend class CCallback; //handling players actions

+ 28 - 21
client/NetPacksClient.cpp

@@ -101,7 +101,7 @@ void callBattleInterfaceIfPresentForBothSides(CClient & cl, const BattleID & bat
 {
 	assert(cl.gameState()->getBattle(battleID));
 
-	if (!cl.gameState()->getBattle(battleID))
+	if(!cl.gameState()->getBattle(battleID))
 	{
 		logGlobal->error("Attempt to call battle interface without ongoing battle!");
 		return;
@@ -161,14 +161,14 @@ void ApplyClientNetPackVisitor::visitSetMana(SetMana & pack)
 	if(settings["session"]["headless"].Bool())
 		return;
 
-	for (auto window : GH.windows().findWindows<BattleWindow>())
+	for(auto window : GH.windows().findWindows<BattleWindow>())
 		window->heroManaPointsChanged(h);
 }
 
 void ApplyClientNetPackVisitor::visitSetMovePoints(SetMovePoints & pack)
 {
 	const CGHeroInstance *h = cl.getHero(pack.hid);
-	cl.invalidatePaths();
+	cl.updatePath(h);
 	callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroMovePointsChanged, h);
 }
 
@@ -229,7 +229,7 @@ void ApplyClientNetPackVisitor::visitSetStackType(SetStackType & pack)
 void ApplyClientNetPackVisitor::visitEraseStack(EraseStack & pack)
 {
 	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
-	cl.invalidatePaths(); //it is possible to remove last non-native unit for current terrain and lose movement penalty
+	cl.updatePath(pack.army); //it is possible to remove last non-native unit for current terrain and lose movement penalty
 }
 
 void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack)
@@ -237,15 +237,14 @@ void ApplyClientNetPackVisitor::visitSwapStacks(SwapStacks & pack)
 	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
 
 	if(pack.srcArmy != pack.dstArmy)
-		cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains
+		cl.updatePath(pack.dstArmy); // adding/removing units may change terrain type penalty based on creature native terrains
 }
 
 void ApplyClientNetPackVisitor::visitInsertNewStack(InsertNewStack & pack)
 {
 	dispatchGarrisonChange(cl, pack.army, ObjectInstanceID());
 
-	if(gs.getHero(pack.army))
-		cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains
+	cl.updatePath(pack.army); // adding/removing units may change terrain type penalty based on creature native terrains
 }
 
 void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack)
@@ -253,7 +252,10 @@ void ApplyClientNetPackVisitor::visitRebalanceStacks(RebalanceStacks & pack)
 	dispatchGarrisonChange(cl, pack.srcArmy, pack.dstArmy);
 
 	if(pack.srcArmy != pack.dstArmy)
-		cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains
+	{
+		cl.updatePath(pack.srcArmy); // adding/removing units may change terrain type penalty based on creature native terrains
+		cl.updatePath(pack.dstArmy);
+	}
 }
 
 void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & pack)
@@ -266,7 +268,10 @@ void ApplyClientNetPackVisitor::visitBulkRebalanceStacks(BulkRebalanceStacks & p
 		dispatchGarrisonChange(cl, pack.moves[0].srcArmy, destArmy);
 
 		if(pack.moves[0].srcArmy != destArmy)
-			cl.invalidatePaths(); // adding/removing units may change terrain type penalty based on creature native terrains
+		{
+			cl.updatePath(destArmy); // adding/removing units may change terrain type penalty based on creature native terrains
+			cl.updatePath(pack.moves[0].srcArmy);
+		}
 	}
 }
 
@@ -292,6 +297,7 @@ void ApplyClientNetPackVisitor::visitPutArtifact(PutArtifact & pack)
 
 void ApplyClientNetPackVisitor::visitEraseArtifact(BulkEraseArtifacts & pack)
 {
+	cl.updatePath(pack.artHolder);
 	for(const auto & slotErase : pack.posPack)
 		callInterfaceIfPresent(cl, cl.getOwner(pack.artHolder), &IGameEventsReceiver::artifactRemoved, ArtifactLocation(pack.artHolder, slotErase));
 }
@@ -312,7 +318,8 @@ void ApplyClientNetPackVisitor::visitBulkMoveArtifacts(BulkMoveArtifacts & pack)
 			if(pack.interfaceOwner != dstOwner)
 				callInterfaceIfPresent(cl, dstOwner, &IGameEventsReceiver::artifactMoved, srcLoc, dstLoc);
 
-			cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
+			cl.updatePath(pack.srcArtHolder); // hero might have equipped/unequipped Angel Wings
+			cl.updatePath(pack.dstArtHolder);
 		}
 	};
 
@@ -342,14 +349,14 @@ void ApplyClientNetPackVisitor::visitAssembledArtifact(AssembledArtifact & pack)
 {
 	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactAssembled, pack.al);
 
-	cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
+	cl.updatePath(pack.al.artHolder); // hero might have equipped/unequipped Angel Wings
 }
 
 void ApplyClientNetPackVisitor::visitDisassembledArtifact(DisassembledArtifact & pack)
 {
 	callInterfaceIfPresent(cl, cl.getOwner(pack.al.artHolder), &IGameEventsReceiver::artifactDisassembled, pack.al);
 
-	cl.invalidatePaths(); // hero might have equipped/unequipped Angel Wings
+	cl.updatePath(pack.al.artHolder); // hero might have equipped/unequipped Angel Wings
 }
 
 void ApplyClientNetPackVisitor::visitHeroVisit(HeroVisit & pack)
@@ -363,7 +370,7 @@ void ApplyClientNetPackVisitor::visitNewTurn(NewTurn & pack)
 {
 	cl.invalidatePaths();
 
-	if (pack.newWeekNotification)
+	if(pack.newWeekNotification)
 	{
 		const auto & newWeek = *pack.newWeekNotification;
 
@@ -380,7 +387,7 @@ void ApplyClientNetPackVisitor::visitGiveBonus(GiveBonus & pack)
 	case GiveBonus::ETarget::OBJECT:
 		{
 			const CGHeroInstance *h = gs.getHero(pack.id.as<ObjectInstanceID>());
-			if (h)
+			if(h)
 				callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, true);
 		}
 		break;
@@ -419,7 +426,7 @@ void ApplyClientNetPackVisitor::visitPlayerEndsGame(PlayerEndsGame & pack)
 
 	bool lastHumanEndsGame = CSH->howManyPlayerInterfaces() == 1 && vstd::contains(cl.playerint, pack.player) && cl.getPlayerState(pack.player)->human && !settings["session"]["spectate"].Bool();
 
-	if (lastHumanEndsGame)
+	if(lastHumanEndsGame)
 	{
 		assert(adventureInt);
 		if(adventureInt)
@@ -446,9 +453,9 @@ void ApplyClientNetPackVisitor::visitPlayerReinitInterface(PlayerReinitInterface
 	{
 		cl.initPlayerInterfaces();
 
-		for (PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player)
+		for(PlayerColor player(0); player < PlayerColor::PLAYER_LIMIT; ++player)
 		{
-			if (cl.gameState()->isPlayerMakingTurn(player))
+			if(cl.gameState()->isPlayerMakingTurn(player))
 			{
 				callAllInterfaces(cl, &IGameEventsReceiver::playerStartsTurn, player);
 				callOnlyThatInterface(cl, player, &CGameInterface::yourTurn, QueryID::NONE);
@@ -482,7 +489,7 @@ void ApplyClientNetPackVisitor::visitRemoveBonus(RemoveBonus & pack)
 	case GiveBonus::ETarget::OBJECT:
 		{
 			const CGHeroInstance *h = gs.getHero(pack.whoID.as<ObjectInstanceID>());
-			if (h)
+			if(h)
 				callInterfaceIfPresent(cl, h->tempOwner, &IGameEventsReceiver::heroBonusChanged, h, pack.bonus, false);
 		}
 		break;
@@ -701,7 +708,7 @@ void ApplyFirstClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty &
 	}
 
 	// invalidate section of map view with our object and force an update with new flag color
-	if (pack.what == ObjProperty::OWNER && CGI->mh)
+	if(pack.what == ObjProperty::OWNER && CGI->mh)
 	{
 		auto object = gs.getObjInstance(pack.id);
 		CGI->mh->onObjectInstantRemove(object, object->getOwner());
@@ -718,7 +725,7 @@ void ApplyClientNetPackVisitor::visitSetObjectProperty(SetObjectProperty & pack)
 	}
 
 	// invalidate section of map view with our object and force an update with new flag color
-	if (pack.what == ObjProperty::OWNER && CGI->mh)
+	if(pack.what == ObjProperty::OWNER && CGI->mh)
 	{
 		auto object = gs.getObjInstance(pack.id);
 		CGI->mh->onObjectInstantAdd(object, object->getOwner());
@@ -807,7 +814,7 @@ void ApplyClientNetPackVisitor::visitBattleSetActiveStack(BattleSetActiveStack &
 
 	const CStack *activated = gs.getBattle(pack.battleID)->battleGetStackByID(pack.stack);
 	PlayerColor playerToCall; //pack.player that will move activated stack
-	if (activated->hasBonusOfType(BonusType::HYPNOTIZED))
+	if(activated->hasBonusOfType(BonusType::HYPNOTIZED))
 	{
 		playerToCall = gs.getBattle(pack.battleID)->getSide(BattleSide::ATTACKER).color == activated->unitOwner()
 			? gs.getBattle(pack.battleID)->getSide(BattleSide::DEFENDER).color

+ 3 - 2
client/NetPacksLobbyClient.cpp

@@ -33,6 +33,7 @@
 #include "widgets/TextControls.h"
 #include "media/CMusicHandler.h"
 #include "media/IVideoPlayer.h"
+#include "windows/GUIClasses.h"
 
 #include "../lib/CConfigHandler.h"
 #include "../lib/texts/CGeneralTextHandler.h"
@@ -207,10 +208,10 @@ void ApplyOnLobbyScreenNetPackVisitor::visitLobbyUpdateState(LobbyUpdateState &
 	{
 		auto bonusSel = std::make_shared<CBonusSelection>();
 		lobby->bonusSel = bonusSel;
-		if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty() && CCS->videoh->open(handler.si->campState->getIntroVideo(), false))
+		if(!handler.si->campState->conqueredScenarios().size() && !handler.si->campState->getIntroVideo().empty() && CCS->videoh->open(handler.si->campState->getIntroVideo(), 1))
 		{
 			CCS->musich->stopMusic();
-			GH.windows().createAndPushWindow<CampaignRimVideo>(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), [bonusSel](){
+			GH.windows().createAndPushWindow<VideoWindow>(handler.si->campState->getIntroVideo(), handler.si->campState->getVideoRim().empty() ? ImagePath::builtin("INTRORIM") : handler.si->campState->getVideoRim(), false, 1, [bonusSel](bool skipped){
 				if(!CSH->si->campState->getMusic().empty())
 					CCS->musich->playMusic(CSH->si->campState->getMusic(), true, false);
 				GH.windows().pushWindow(bonusSel);

+ 1 - 2
client/adventureMap/AdventureMapWidget.cpp

@@ -309,9 +309,8 @@ std::shared_ptr<CIntObject> AdventureMapWidget::buildStatusBar(const JsonNode &
 std::shared_ptr<CIntObject> AdventureMapWidget::buildTexturePlayerColored(const JsonNode & input)
 {
 	logGlobal->debug("Building widget CFilledTexture");
-	auto image = ImagePath::fromJson(input["image"]);
 	Rect area = readTargetArea(input["area"]);
-	return std::make_shared<FilledTexturePlayerColored>(image, area);
+	return std::make_shared<FilledTexturePlayerColored>(area);
 }
 
 std::shared_ptr<CHeroList> AdventureMapWidget::getHeroList()

+ 6 - 17
client/battle/BattleStacksController.cpp

@@ -27,6 +27,7 @@
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../media/ISoundPlayer.h"
+#include "../render/AssetGenerator.h"
 #include "../render/Colors.h"
 #include "../render/Canvas.h"
 #include "../render/IRenderHandler.h"
@@ -79,24 +80,12 @@ BattleStacksController::BattleStacksController(BattleInterface & owner):
 	stackToActivate(nullptr),
 	animIDhelper(0)
 {
+	AssetGenerator::createCombatUnitNumberWindow();
 	//preparing graphics for displaying amounts of creatures
-	amountNormal     = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY);
-	amountPositive   = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY);
-	amountNegative   = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY);
-	amountEffNeutral = GH.renderHandler().loadImage(ImagePath::builtin("CMNUMWIN.BMP"), EImageBlitMode::COLORKEY);
-
-	static const auto shifterNormal   = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f );
-	static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f );
-	static const auto shifterNegative = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 0.2f, 0.2f );
-	static const auto shifterNeutral  = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 1.0f, 0.2f );
-
-	// do not change border color
-	static const int32_t ignoredMask = 1 << 26;
-
-	amountNormal->adjustPalette(shifterNormal, ignoredMask);
-	amountPositive->adjustPalette(shifterPositive, ignoredMask);
-	amountNegative->adjustPalette(shifterNegative, ignoredMask);
-	amountEffNeutral->adjustPalette(shifterNeutral, ignoredMask);
+	amountNormal     = GH.renderHandler().loadImage(ImagePath::builtin("combatUnitNumberWindowDefault"), EImageBlitMode::COLORKEY);
+	amountPositive   = GH.renderHandler().loadImage(ImagePath::builtin("combatUnitNumberWindowPositive"), EImageBlitMode::COLORKEY);
+	amountNegative   = GH.renderHandler().loadImage(ImagePath::builtin("combatUnitNumberWindowNegative"), EImageBlitMode::COLORKEY);
+	amountEffNeutral = GH.renderHandler().loadImage(ImagePath::builtin("combatUnitNumberWindowNeutral"), EImageBlitMode::COLORKEY);
 
 	std::vector<const CStack*> stacks = owner.getBattle()->battleGetAllStacks(true);
 	for(const CStack * s : stacks)

+ 3 - 1
client/eventsSDL/InputSourceMouse.cpp

@@ -34,7 +34,7 @@ InputSourceMouse::InputSourceMouse()
 void InputSourceMouse::handleEventMouseMotion(const SDL_MouseMotionEvent & motion)
 {
 	Point newPosition = Point(motion.x, motion.y) / GH.screenHandler().getScalingFactor();
-	Point distance= Point(-motion.xrel, -motion.yrel) / GH.screenHandler().getScalingFactor();
+	Point distance = Point(-motion.xrel, -motion.yrel) / GH.screenHandler().getScalingFactor();
 
 	mouseButtonsMask = motion.state;
 
@@ -42,6 +42,8 @@ void InputSourceMouse::handleEventMouseMotion(const SDL_MouseMotionEvent & motio
 		GH.events().dispatchGesturePanning(middleClickPosition, newPosition, distance);
 	else if (mouseButtonsMask & SDL_BUTTON(SDL_BUTTON_LEFT))
 		GH.events().dispatchMouseDragged(newPosition, distance);
+	else if (mouseButtonsMask & SDL_BUTTON(SDL_BUTTON_RIGHT))
+		GH.events().dispatchMouseDraggedPopup(newPosition, distance);
 	else
 		GH.input().setCursorPosition(newPosition);
 }

+ 1 - 1
client/globalLobby/GlobalLobbyInviteWindow.cpp

@@ -78,7 +78,7 @@ GlobalLobbyInviteWindow::GlobalLobbyInviteWindow()
 	pos.w = 236;
 	pos.h = 420;
 
-	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
 	filledBackground->setPlayerColor(PlayerColor(1));
 	labelTitle = std::make_shared<CLabel>(
 		pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.invite.header").toString()

+ 1 - 1
client/globalLobby/GlobalLobbyLoginWindow.cpp

@@ -40,7 +40,7 @@ GlobalLobbyLoginWindow::GlobalLobbyLoginWindow()
 	loginAs.appendTextID("vcmi.lobby.login.as");
 	loginAs.replaceRawString(CSH->getGlobalLobby().getAccountDisplayName());
 
-	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
 	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.login.title"));
 	labelUsernameTitle = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, CGI->generaltexth->translate("vcmi.lobby.login.username"));
 	labelUsername = std::make_shared<CLabel>( 10, 65, FONT_MEDIUM, ETextAlignment::TOPLEFT, Colors::WHITE, loginAs.toString(), 265);

+ 1 - 1
client/globalLobby/GlobalLobbyRoomWindow.cpp

@@ -152,7 +152,7 @@ GlobalLobbyRoomWindow::GlobalLobbyRoomWindow(GlobalLobbyWindow * window, const s
 	subtitleText.replaceRawString(roomDescription.description);
 	subtitleText.replaceRawString(roomDescription.hostAccountDisplayName);
 
-	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
 	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, MetaString::createFromTextID("vcmi.lobby.preview.title").toString());
 	labelSubtitle = std::make_shared<CLabel>( pos.w / 2, 40, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, subtitleText.toString(), 400);
 

+ 1 - 1
client/globalLobby/GlobalLobbyServerSetup.cpp

@@ -34,7 +34,7 @@ GlobalLobbyServerSetup::GlobalLobbyServerSetup()
 	pos.w = 284;
 	pos.h = 340;
 
-	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
 	labelTitle = std::make_shared<CLabel>( pos.w / 2, 20, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.create"));
 	labelPlayerLimit = std::make_shared<CLabel>( pos.w / 2, 48, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.players.limit"));
 	labelRoomType = std::make_shared<CLabel>( pos.w / 2, 108, FONT_MEDIUM, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.room.type"));

+ 6 - 5
client/gui/CGuiHandler.cpp

@@ -181,17 +181,18 @@ Point CGuiHandler::screenDimensions() const
 
 void CGuiHandler::drawFPSCounter()
 {
-	int x = 7;
-	int y = screen->h-20;
-	int width3digitFPSIncludingPadding = 48;
-	int heightFPSTextIncludingPadding = 11;
+	int scaling = screenHandlerInstance->getScalingFactor();
+	int x = 7 * scaling;
+	int y = screen->h-20 * scaling;
+	int width3digitFPSIncludingPadding = 48 * scaling;
+	int heightFPSTextIncludingPadding = 11 * scaling;
 	SDL_Rect overlay = { x, y, width3digitFPSIncludingPadding, heightFPSTextIncludingPadding};
 	uint32_t black = SDL_MapRGB(screen->format, 10, 10, 10);
 	SDL_FillRect(screen, &overlay, black);
 
 	std::string fps = std::to_string(framerate().getFramerate())+" FPS";
 
-	graphics->fonts[FONT_SMALL]->renderTextLeft(screen, fps, Colors::WHITE, Point(8, screen->h-22));
+	graphics->fonts[FONT_SMALL]->renderTextLeft(screen, fps, Colors::WHITE, Point(8 * scaling, screen->h-22 * scaling));
 }
 
 bool CGuiHandler::amIGuiThread()

+ 4 - 3
client/gui/CursorHandler.cpp

@@ -17,6 +17,7 @@
 #include "../renderSDL/CursorHardware.h"
 #include "../render/CAnimation.h"
 #include "../render/IImage.h"
+#include "../render/IScreenHandler.h"
 #include "../render/IRenderHandler.h"
 
 #include "../../lib/CConfigHandler.h"
@@ -175,7 +176,7 @@ Point CursorHandler::getPivotOffsetMap(size_t index)
 
 	assert(offsets.size() == size_t(Cursor::Map::COUNT)); //Invalid number of pivot offsets for cursor
 	assert(index < offsets.size());
-	return offsets[index];
+	return offsets[index] * GH.screenHandler().getScalingFactor();
 }
 
 Point CursorHandler::getPivotOffsetCombat(size_t index)
@@ -205,12 +206,12 @@ Point CursorHandler::getPivotOffsetCombat(size_t index)
 
 	assert(offsets.size() == size_t(Cursor::Combat::COUNT)); //Invalid number of pivot offsets for cursor
 	assert(index < offsets.size());
-	return offsets[index];
+	return offsets[index] * GH.screenHandler().getScalingFactor();
 }
 
 Point CursorHandler::getPivotOffsetSpellcast()
 {
-	return { 18, 28};
+	return Point(18, 28) * GH.screenHandler().getScalingFactor();
 }
 
 Point CursorHandler::getPivotOffset()

+ 8 - 0
client/gui/EventDispatcher.cpp

@@ -35,6 +35,7 @@ void EventDispatcher::processLists(ui16 activityFlag, const Functor & cb)
 	processList(AEventsReceiver::HOVER, hoverable);
 	processList(AEventsReceiver::MOVE, motioninterested);
 	processList(AEventsReceiver::DRAG, draginterested);
+	processList(AEventsReceiver::DRAG_POPUP, dragPopupInterested);
 	processList(AEventsReceiver::KEYBOARD, keyinterested);
 	processList(AEventsReceiver::TIME, timeinterested);
 	processList(AEventsReceiver::WHEEL, wheelInterested);
@@ -433,3 +434,10 @@ void EventDispatcher::dispatchMouseDragged(const Point & currentPosition, const
 			elem->mouseDragged(currentPosition, lastUpdateDistance);
 	}
 }
+
+void EventDispatcher::dispatchMouseDraggedPopup(const Point & currentPosition, const Point & lastUpdateDistance)
+{
+	EventReceiversList diCopy = dragPopupInterested;
+	for(auto & elem : diCopy)
+		elem->mouseDraggedPopup(currentPosition, lastUpdateDistance);
+}

+ 2 - 0
client/gui/EventDispatcher.h

@@ -30,6 +30,7 @@ class EventDispatcher
 	EventReceiversList keyinterested;
 	EventReceiversList motioninterested;
 	EventReceiversList draginterested;
+	EventReceiversList dragPopupInterested;
 	EventReceiversList timeinterested;
 	EventReceiversList wheelInterested;
 	EventReceiversList doubleClickInterested;
@@ -66,6 +67,7 @@ public:
 	void dispatchMouseMoved(const Point & distance, const Point & position);
 
 	void dispatchMouseDragged(const Point & currentPosition, const Point & lastUpdateDistance);
+	void dispatchMouseDraggedPopup(const Point & currentPosition, const Point & lastUpdateDistance);
 
 	void dispatchShowPopup(const Point & position, int tolerance);
 	void dispatchClosePopup(const Point & position);

+ 3 - 1
client/gui/EventsReceiver.h

@@ -61,6 +61,7 @@ public:
 	virtual void wheelScrolled(int distance) {}
 	virtual void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) {}
 	virtual void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) {}
+	virtual void mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) {}
 
 	/// Called when UI element hover status changes
 	virtual void hover(bool on) {}
@@ -97,7 +98,8 @@ public:
 		TEXTINPUT = 512,
 		GESTURE = 1024,
 		DRAG = 2048,
-		INPUT_MODE_CHANGE = 4096
+		INPUT_MODE_CHANGE = 4096,
+		DRAG_POPUP = 8192
 	};
 
 	/// Returns true if element is currently hovered by mouse

+ 6 - 3
client/gui/InterfaceObjectConfigurable.cpp

@@ -566,16 +566,19 @@ std::shared_ptr<CAnimImage> InterfaceObjectConfigurable::buildImage(const JsonNo
 std::shared_ptr<CFilledTexture> InterfaceObjectConfigurable::buildTexture(const JsonNode & config) const
 {
 	logGlobal->debug("Building widget CFilledTexture");
-	auto image = ImagePath::fromJson(config["image"]);
 	auto rect = readRect(config["rect"]);
 	auto playerColor = readPlayerColor(config["color"]);
 	if(playerColor.isValidPlayer())
 	{
-		auto result = std::make_shared<FilledTexturePlayerColored>(image, rect);
+		auto result = std::make_shared<FilledTexturePlayerColored>(rect);
 		result->setPlayerColor(playerColor);
 		return result;
 	}
-	return std::make_shared<CFilledTexture>(image, rect);
+	else
+	{
+		auto image = ImagePath::fromJson(config["image"]);
+		return std::make_shared<CFilledTexture>(image, rect);
+	}
 }
 
 std::shared_ptr<ComboBox> InterfaceObjectConfigurable::buildComboBox(const JsonNode & config)

+ 0 - 31
client/lobby/CBonusSelection.cpp

@@ -58,37 +58,6 @@
 
 #include "../../lib/mapObjects/CGHeroInstance.h"
 
-
-CampaignRimVideo::CampaignRimVideo(VideoPath video, ImagePath rim, std::function<void()> closeCb)
-	: CWindowObject(BORDERED), closeCb(closeCb)
-{
-	OBJECT_CONSTRUCTION;
-
-	addUsedEvents(LCLICK | KEYBOARD);
-
-	pos = center(Rect(0, 0, 800, 600));
-
-	videoPlayer = std::make_shared<VideoWidgetOnce>(Point(80, 186), video, true, [this](){ exit(); });
-	setBackground(rim);
-}
-
-void CampaignRimVideo::exit()
-{
-	close();
-	if(closeCb)
-		closeCb();
-}
-
-void CampaignRimVideo::clickPressed(const Point & cursorPosition)
-{
-	exit();
-}
-
-void CampaignRimVideo::keyPressed(EShortcut key)
-{
-	exit();
-}
-
 std::shared_ptr<CampaignState> CBonusSelection::getCampaign()
 {
 	return CSH->si->campState;

+ 0 - 14
client/lobby/CBonusSelection.h

@@ -33,20 +33,6 @@ class VideoWidgetOnce;
 class CBonusSelection;
 
 
-class CampaignRimVideo : public CWindowObject
-{
-	std::shared_ptr<VideoWidgetOnce> videoPlayer;
-
-	std::function<void()> closeCb;
-
-	void exit();
-public:
-	CampaignRimVideo(VideoPath video, ImagePath rim, std::function<void()> closeCb);
-
-	void clickPressed(const Point & cursorPosition) override;
-	void keyPressed(EShortcut key) override;
-};
-
 /// Campaign screen where you can choose one out of three starting bonuses
 class CBonusSelection : public CWindowObject
 {

+ 1 - 1
client/lobby/CSelectionBase.cpp

@@ -399,7 +399,7 @@ PvPBox::PvPBox(const Rect & rect)
 	pos += rect.topLeft();
 	setRedrawParent(true);
 
-	backgroundTexture = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, rect.w, rect.h));
+	backgroundTexture = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, rect.w, rect.h));
 	backgroundTexture->setPlayerColor(PlayerColor(1));
 	backgroundBorder = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, rect.w, rect.h), ColorRGBA(0, 0, 0, 64), ColorRGBA(96, 96, 96, 255), 1);
 

+ 2 - 2
client/lobby/OptionsTab.cpp

@@ -521,7 +521,7 @@ void OptionsTab::SelectionWindow::recreate(int sliderPos)
 	int sliderWidth = ((amountLines > MAX_LINES) ? 16 : 0);
 
 	pos = Rect(pos.x, pos.y, x + sliderWidth, y);
-	backgroundTexture = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w - sliderWidth, pos.h));
+	backgroundTexture = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w - sliderWidth, pos.h));
 	backgroundTexture->setPlayerColor(PlayerColor(1));
 	updateShadow();
 
@@ -803,7 +803,7 @@ OptionsTab::HandicapWindow::HandicapWindow()
 
 	pos = Rect(0, 0, 660, 100 + SEL->getStartInfo()->playerInfos.size() * 30);
 
-	backgroundTexture = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), pos);
+	backgroundTexture = std::make_shared<FilledTexturePlayerColored>(pos);
 	backgroundTexture->setPlayerColor(PlayerColor(1));
 
 	labels.push_back(std::make_shared<CLabel>(pos.w / 2 + 8, 15, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW, CGI->generaltexth->translate("vcmi.lobby.handicap")));

+ 2 - 0
client/mainmenu/CHighScoreScreen.cpp

@@ -12,6 +12,7 @@
 
 #include "CHighScoreScreen.h"
 #include "CStatisticScreen.h"
+#include "CMainMenu.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../gui/Shortcut.h"
@@ -170,6 +171,7 @@ void CHighScoreScreen::buttonResetClick()
 void CHighScoreScreen::buttonExitClick()
 {
 	close();
+	CMM->playMusic();
 }
 
 CHighScoreInputScreen::CHighScoreInputScreen(bool won, HighScoreCalculation calc, const StatisticDataSet & statistic)

+ 30 - 1
client/mainmenu/CMainMenu.cpp

@@ -18,6 +18,7 @@
 #include "../lobby/CSelectionBase.h"
 #include "../lobby/CLobbyScreen.h"
 #include "../media/IMusicPlayer.h"
+#include "../media/IVideoPlayer.h"
 #include "../gui/CursorHandler.h"
 #include "../windows/GUIClasses.h"
 #include "../gui/CGuiHandler.h"
@@ -117,7 +118,6 @@ void CMenuScreen::show(Canvas & to)
 
 void CMenuScreen::activate()
 {
-	CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true);
 	CIntObject::activate();
 }
 
@@ -300,6 +300,35 @@ CMainMenu::~CMainMenu()
 		GH.curInt = nullptr;
 }
 
+void CMainMenu::playIntroVideos()
+{
+	auto playVideo = [](std::string video, bool rim, float scaleFactor, std::function<void(bool)> cb){
+		if(CCS->videoh->open(VideoPath::builtin(video), scaleFactor))
+			GH.windows().createAndPushWindow<VideoWindow>(VideoPath::builtin(video), rim ? ImagePath::builtin("INTRORIM") : ImagePath::builtin(""), true, scaleFactor, [cb](bool skipped){ cb(skipped); });
+		else
+			cb(true);
+	};
+
+	playVideo("3DOLOGO.SMK", false, 1.25, [playVideo, this](bool skipped){
+		if(!skipped)
+			playVideo("NWCLOGO.SMK", false, 2, [playVideo, this](bool skipped){
+				if(!skipped)
+					playVideo("H3INTRO.SMK", true, 1, [this](bool skipped){
+						playMusic();
+					});
+				else
+					playMusic();
+			});
+		else
+			playMusic();
+	});
+}
+
+void CMainMenu::playMusic()
+{
+	CCS->musich->playMusic(AudioPath::builtin("Music/MainMenu"), true, true);
+}
+
 void CMainMenu::activate()
 {
 	// check if screen was resized while main menu was inactive - e.g. in gameplay mode

+ 4 - 0
client/mainmenu/CMainMenu.h

@@ -142,6 +142,8 @@ class CMainMenu : public CIntObject, public IUpdateable, public std::enable_shar
 {
 	std::shared_ptr<CFilledTexture> backgroundAroundMenu;
 
+	std::vector<VideoPath> videoPlayList;
+
 	CMainMenu(); //Use CMainMenu::create
 
 public:
@@ -162,6 +164,8 @@ public:
 
 	static std::shared_ptr<CPicture> createPicture(const JsonNode & config);
 
+	void playIntroVideos();
+	void playMusic();
 };
 
 /// Simple window to enter the server's address.

+ 2 - 2
client/mainmenu/CStatisticScreen.cpp

@@ -47,7 +47,7 @@ CStatisticScreen::CStatisticScreen(const StatisticDataSet & stat)
 {
 	OBJECT_CONSTRUCTION;
 	pos = center(Rect(0, 0, 800, 600));
-	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
 	filledBackground->setPlayerColor(PlayerColor(1));
 
 	contentArea = Rect(10, 40, 780, 510);
@@ -225,7 +225,7 @@ StatisticSelector::StatisticSelector(const std::vector<std::string> & texts, con
 {
 	OBJECT_CONSTRUCTION;
 	pos = center(Rect(0, 0, 128 + 16, std::min(static_cast<int>(texts.size()), LINES) * 40));
-	filledBackground = std::make_shared<FilledTexturePlayerColored>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	filledBackground = std::make_shared<FilledTexturePlayerColored>(Rect(0, 0, pos.w, pos.h));
 	filledBackground->setPlayerColor(PlayerColor(1));
 
 	slider = std::make_shared<CSlider>(Point(pos.w - 16, 0), pos.h, [this](int to){ update(to); redraw(); }, LINES, texts.size(), 0, Orientation::VERTICAL, CSlider::BLUE);

+ 13 - 5
client/mapView/MapRenderer.cpp

@@ -479,13 +479,21 @@ void MapRendererObjects::renderImage(IMapRendererContext & context, Canvas & tar
 
 	image->setAlpha(transparency);
 	image->setShadowEnabled(true);
-	image->setOverlayEnabled(object->getOwner().isValidPlayer() || object->getOwner() == PlayerColor::NEUTRAL);
+	if (object->ID != Obj::HERO)
+	{
+		image->setOverlayEnabled(object->getOwner().isValidPlayer() || object->getOwner() == PlayerColor::NEUTRAL);
 
-	if (object->getOwner().isValidPlayer())
-		image->setOverlayColor(graphics->playerColors[object->getOwner().getNum()]);
+		if (object->getOwner().isValidPlayer())
+			image->setOverlayColor(graphics->playerColors[object->getOwner().getNum()]);
 
-	if (object->getOwner() == PlayerColor::NEUTRAL)
-		image->setOverlayColor(graphics->neutralColor);
+		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);
 

+ 6 - 1
client/mapView/MapViewActions.cpp

@@ -34,7 +34,7 @@ MapViewActions::MapViewActions(MapView & owner, const std::shared_ptr<MapViewMod
 	pos.w = model->getPixelsVisibleDimensions().x;
 	pos.h = model->getPixelsVisibleDimensions().y;
 
-	addUsedEvents(LCLICK | SHOW_POPUP | DRAG | GESTURE | HOVER | MOVE | WHEEL);
+	addUsedEvents(LCLICK | SHOW_POPUP | DRAG | DRAG_POPUP | GESTURE | HOVER | MOVE | WHEEL);
 }
 
 void MapViewActions::setContext(const std::shared_ptr<IMapRendererContext> & context)
@@ -101,6 +101,11 @@ void MapViewActions::mouseDragged(const Point & cursorPosition, const Point & la
 		owner.onMapSwiped(lastUpdateDistance);
 }
 
+void MapViewActions::mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance)
+{
+	owner.onMapSwiped(lastUpdateDistance);
+}
+
 void MapViewActions::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance)
 {
 	owner.onMapSwiped(lastUpdateDistance);

+ 1 - 0
client/mapView/MapViewActions.h

@@ -42,6 +42,7 @@ public:
 	void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override;
 	void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override;
 	void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override;
+	void mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) override;
 	void wheelScrolled(int distance) override;
 	
 	bool dragActive;

+ 1 - 7
client/media/CEmptyVideoPlayer.h

@@ -14,18 +14,12 @@
 class CEmptyVideoPlayer final : public IVideoPlayer
 {
 public:
-	/// Plays video on top of the screen, returns only after playback is over
-	bool playIntroVideo(const VideoPath & name) override
-	{
-		return false;
-	};
-
 	void playSpellbookAnimation(const VideoPath & name, const Point & position) override
 	{
 	}
 
 	/// Load video from specified path
-	std::unique_ptr<IVideoInstance> open(const VideoPath & name, bool scaleToScreen) override
+	std::unique_ptr<IVideoInstance> open(const VideoPath & name, float scaleFactor) override
 	{
 		return nullptr;
 	};

+ 8 - 24
client/media/CVideoHandler.cpp

@@ -173,18 +173,10 @@ void CVideoInstance::openVideo()
 	openCodec(findVideoStream());
 }
 
-void CVideoInstance::prepareOutput(bool scaleToScreenSize, bool useTextureOutput)
+void CVideoInstance::prepareOutput(float scaleFactor, bool useTextureOutput)
 {
 	//setup scaling
-	if(scaleToScreenSize)
-	{
-		dimensions.x = screen->w;
-		dimensions.y = screen->h;
-	}
-	else
-	{
-		dimensions = Point(getCodecContext()->width, getCodecContext()->height) * GH.screenHandler().getScalingFactor();
-	}
+	dimensions = Point(getCodecContext()->width * scaleFactor, getCodecContext()->height * scaleFactor) * GH.screenHandler().getScalingFactor();
 
 	// Allocate a place to put our YUV image on that screen
 	if (useTextureOutput)
@@ -352,10 +344,7 @@ FFMpegStream::~FFMpegStream()
 
 Point CVideoInstance::size()
 {
-	if(!getCurrentFrame())
-		throw std::runtime_error("Invalid video frame!");
-
-	return Point(getCurrentFrame()->width, getCurrentFrame()->height);
+	return dimensions / GH.screenHandler().getScalingFactor();
 }
 
 void CVideoInstance::show(const Point & position, Canvas & canvas)
@@ -575,7 +564,7 @@ std::pair<std::unique_ptr<ui8 []>, si64> CAudioInstance::extractAudio(const Vide
 	return dat;
 }
 
-bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey)
+bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool stopOnKey)
 {
 	CVideoInstance instance;
 	CAudioInstance audio;
@@ -587,7 +576,7 @@ bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & po
 		return true;
 
 	instance.openVideo();
-	instance.prepareOutput(scale, true);
+	instance.prepareOutput(1, true);
 
 	auto lastTimePoint = boost::chrono::steady_clock::now();
 
@@ -633,17 +622,12 @@ bool CVideoPlayer::openAndPlayVideoImpl(const VideoPath & name, const Point & po
 	return true;
 }
 
-bool CVideoPlayer::playIntroVideo(const VideoPath & name)
-{
-	return openAndPlayVideoImpl(name, Point(0, 0), true, true, true);
-}
-
 void CVideoPlayer::playSpellbookAnimation(const VideoPath & name, const Point & position)
 {
-	openAndPlayVideoImpl(name, position * GH.screenHandler().getScalingFactor(), false, false, false);
+	openAndPlayVideoImpl(name, position * GH.screenHandler().getScalingFactor(), false, false);
 }
 
-std::unique_ptr<IVideoInstance> CVideoPlayer::open(const VideoPath & name, bool scaleToScreen)
+std::unique_ptr<IVideoInstance> CVideoPlayer::open(const VideoPath & name, float scaleFactor)
 {
 	auto result = std::make_unique<CVideoInstance>();
 
@@ -651,7 +635,7 @@ std::unique_ptr<IVideoInstance> CVideoPlayer::open(const VideoPath & name, bool
 		return nullptr;
 
 	result->openVideo();
-	result->prepareOutput(scaleToScreen, false);
+	result->prepareOutput(scaleFactor, false);
 	result->loadNextFrame(); // prepare 1st frame
 
 	return result;

+ 3 - 4
client/media/CVideoHandler.h

@@ -80,7 +80,7 @@ class CVideoInstance final : public IVideoInstance, public FFMpegStream
 	/// video playback current progress, in seconds
 	double frameTime = 0.0;
 
-	void prepareOutput(bool scaleToScreenSize, bool useTextureOutput);
+	void prepareOutput(float scaleFactor, bool useTextureOutput);
 
 public:
 	~CVideoInstance();
@@ -97,13 +97,12 @@ public:
 
 class CVideoPlayer final : public IVideoPlayer
 {
-	bool openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool scale, bool stopOnKey);
+	bool openAndPlayVideoImpl(const VideoPath & name, const Point & position, bool useOverlay, bool stopOnKey);
 	void openVideoFile(CVideoInstance & state, const VideoPath & fname);
 
 public:
-	bool playIntroVideo(const VideoPath & name) final;
 	void playSpellbookAnimation(const VideoPath & name, const Point & position) final;
-	std::unique_ptr<IVideoInstance> open(const VideoPath & name, bool scaleToScreen) final;
+	std::unique_ptr<IVideoInstance> open(const VideoPath & name, float scaleFactor) final;
 	std::pair<std::unique_ptr<ui8[]>, si64> getAudio(const VideoPath & videoToOpen) final;
 };
 

+ 1 - 4
client/media/IVideoPlayer.h

@@ -38,14 +38,11 @@ public:
 class IVideoPlayer : boost::noncopyable
 {
 public:
-	/// Plays video on top of the screen, returns only after playback is over, aborts on input event
-	virtual bool playIntroVideo(const VideoPath & name) = 0;
-
 	/// Plays video on top of the screen, returns only after playback is over
 	virtual void playSpellbookAnimation(const VideoPath & name, const Point & position) = 0;
 
 	/// Load video from specified path. Returns nullptr on failure
-	virtual std::unique_ptr<IVideoInstance> open(const VideoPath & name, bool scaleToScreen) = 0;
+	virtual std::unique_ptr<IVideoInstance> open(const VideoPath & name, float scaleFactor) = 0;
 
 	/// Extracts audio data from provided video in wav format
 	virtual std::pair<std::unique_ptr<ui8[]>, si64> getAudio(const VideoPath & videoToOpen) = 0;

+ 93 - 6
client/render/AssetGenerator.cpp

@@ -14,6 +14,7 @@
 #include "../render/IImage.h"
 #include "../render/IImageLoader.h"
 #include "../render/Canvas.h"
+#include "../render/ColorFilter.h"
 #include "../render/IRenderHandler.h"
 
 #include "../lib/filesystem/Filesystem.h"
@@ -22,11 +23,14 @@ void AssetGenerator::generateAll()
 {
 	createBigSpellBook();
 	createAdventureOptionsCleanBackground();
+	for (int i = 0; i < PlayerColor::PLAYER_LIMIT_I; ++i)
+		createPlayerColoredBackground(PlayerColor(i));
+	createCombatUnitNumberWindow();
 }
 
 void AssetGenerator::createAdventureOptionsCleanBackground()
 {
-	std::string filename = "data/AdventureOptionsBackgroundClear.bmp";
+	std::string filename = "data/AdventureOptionsBackgroundClear.png";
 
 	if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation
 		return;
@@ -35,9 +39,10 @@ void AssetGenerator::createAdventureOptionsCleanBackground()
 		return;
 	ResourcePath savePath(filename, EResType::IMAGE);
 
-	auto res = ImagePath::builtin("ADVOPTBK");
+	auto locator = ImageLocator(ImagePath::builtin("ADVOPTBK"));
+	locator.scalingFactor = 1;
 
-	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(res, EImageBlitMode::OPAQUE);
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
 
 	Canvas canvas = Canvas(Point(575, 585), CanvasScalingPolicy::IGNORE);
 	canvas.draw(img, Point(0, 0), Rect(0, 0, 575, 585));
@@ -55,7 +60,7 @@ void AssetGenerator::createAdventureOptionsCleanBackground()
 
 void AssetGenerator::createBigSpellBook()
 {
-	std::string filename = "data/SpellBookLarge.bmp";
+	std::string filename = "data/SpellBookLarge.png";
 
 	if(CResourceHandler::get()->existsResource(ResourcePath(filename))) // overridden by mod, no generation
 		return;
@@ -64,9 +69,10 @@ void AssetGenerator::createBigSpellBook()
 		return;
 	ResourcePath savePath(filename, EResType::IMAGE);
 
-	auto res = ImagePath::builtin("SpelBack");
+	auto locator = ImageLocator(ImagePath::builtin("SpelBack"));
+	locator.scalingFactor = 1;
 
-	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(res, EImageBlitMode::OPAQUE);
+	std::shared_ptr<IImage> img = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
 	Canvas canvas = Canvas(Point(800, 600), CanvasScalingPolicy::IGNORE);
 	// edges
 	canvas.draw(img, Point(0, 0), Rect(15, 38, 90, 45));
@@ -114,3 +120,84 @@ void AssetGenerator::createBigSpellBook()
 
 	image->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
 }
+
+void AssetGenerator::createPlayerColoredBackground(const PlayerColor & player)
+{
+	std::string filename = "data/DialogBoxBackground_" + player.toString() + ".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("DiBoxBck"));
+	locator.scalingFactor = 1;
+
+	std::shared_ptr<IImage> texture = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
+
+	// Color transform to make color of brown DIBOX.PCX texture match color of specified player
+	static const std::array<ColorFilter, PlayerColor::PLAYER_LIMIT_I> filters = {
+		ColorFilter::genRangeShifter(  0.25,  0,     0,     1.25, 0.00, 0.00 ), // red
+		ColorFilter::genRangeShifter(  0,     0,     0,     0.45, 1.20, 4.50 ), // blue
+		ColorFilter::genRangeShifter(  0.40,  0.27,  0.23,  1.10, 1.20, 1.15 ), // tan
+		ColorFilter::genRangeShifter( -0.27,  0.10, -0.27,  0.70, 1.70, 0.70 ), // green
+		ColorFilter::genRangeShifter(  0.47,  0.17, -0.27,  1.60, 1.20, 0.70 ), // orange
+		ColorFilter::genRangeShifter(  0.12, -0.1,   0.25,  1.15, 1.20, 2.20 ), // purple
+		ColorFilter::genRangeShifter( -0.13,  0.23,  0.23,  0.90, 1.20, 2.20 ), // teal
+		ColorFilter::genRangeShifter(  0.44,  0.15,  0.25,  1.00, 1.00, 1.75 )  // pink
+	};
+
+	assert(player.isValidPlayer());
+	if (!player.isValidPlayer())
+	{
+		logGlobal->error("Unable to colorize to invalid player color %d!", static_cast<int>(player.getNum()));
+		return;
+	}
+
+	texture->adjustPalette(filters[player.getNum()], 0);
+	texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePath));
+}
+
+void AssetGenerator::createCombatUnitNumberWindow()
+{
+	std::string filenameToSave = "data/combatUnitNumberWindow";
+
+	ResourcePath savePathDefault(filenameToSave + "Default.png", EResType::IMAGE);
+	ResourcePath savePathNeutral(filenameToSave + "Neutral.png", EResType::IMAGE);
+	ResourcePath savePathPositive(filenameToSave + "Positive.png", EResType::IMAGE);
+	ResourcePath savePathNegative(filenameToSave + "Negative.png", EResType::IMAGE);
+
+	if(CResourceHandler::get()->existsResource(savePathDefault)) // overridden by mod, no generation
+		return;
+
+	if(!CResourceHandler::get("local")->createResource(savePathDefault.getOriginalName() + ".png") ||
+	   !CResourceHandler::get("local")->createResource(savePathNeutral.getOriginalName() + ".png") ||
+	   !CResourceHandler::get("local")->createResource(savePathPositive.getOriginalName() + ".png") ||
+	   !CResourceHandler::get("local")->createResource(savePathNegative.getOriginalName() + ".png"))
+		return;
+
+	auto locator = ImageLocator(ImagePath::builtin("CMNUMWIN"));
+	locator.scalingFactor = 1;
+
+	std::shared_ptr<IImage> texture = GH.renderHandler().loadImage(locator, EImageBlitMode::OPAQUE);
+
+	static const auto shifterNormal   = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.6f, 0.2f, 1.0f );
+	static const auto shifterPositive = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 0.2f, 1.0f, 0.2f );
+	static const auto shifterNegative = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 0.2f, 0.2f );
+	static const auto shifterNeutral  = ColorFilter::genRangeShifter( 0.f, 0.f, 0.f, 1.0f, 1.0f, 0.2f );
+
+	// do not change border color
+	static const int32_t ignoredMask = 1 << 26;
+
+	texture->adjustPalette(shifterNormal, ignoredMask);
+	texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePathDefault));
+	texture->adjustPalette(shifterPositive, ignoredMask);
+	texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePathPositive));
+	texture->adjustPalette(shifterNegative, ignoredMask);
+	texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePathNegative));
+	texture->adjustPalette(shifterNeutral, ignoredMask);
+	texture->exportBitmap(*CResourceHandler::get("local")->getResourceName(savePathNeutral));
+}

+ 9 - 3
client/render/AssetGenerator.h

@@ -9,10 +9,16 @@
  */
 #pragma once
 
+VCMI_LIB_NAMESPACE_BEGIN
+class PlayerColor;
+VCMI_LIB_NAMESPACE_END
+
 class AssetGenerator
 {
 public:
-    static void generateAll();
-    static void createAdventureOptionsCleanBackground();
-    static void createBigSpellBook();
+	static void generateAll();
+	static void createAdventureOptionsCleanBackground();
+	static void createBigSpellBook();
+	static void createPlayerColoredBackground(const PlayerColor & player);
+	static void createCombatUnitNumberWindow();
 };

+ 2 - 2
client/render/Canvas.cpp

@@ -191,14 +191,14 @@ void Canvas::drawText(const Point & position, const EFonts & font, const ColorRG
 
 void Canvas::drawColor(const Rect & target, const ColorRGBA & color)
 {
-	Rect realTarget = (target + renderArea.topLeft()) * getScalingFactor();
+	Rect realTarget = target * getScalingFactor() + renderArea.topLeft();
 
 	CSDL_Ext::fillRect(surface, realTarget, CSDL_Ext::toSDL(color));
 }
 
 void Canvas::drawColorBlended(const Rect & target, const ColorRGBA & color)
 {
-	Rect realTarget = (target + renderArea.topLeft()) * getScalingFactor();
+	Rect realTarget = target * getScalingFactor() + renderArea.topLeft();
 
 	CSDL_Ext::fillRectBlended(surface, realTarget, CSDL_Ext::toSDL(color));
 }

+ 1 - 1
client/render/IImage.h

@@ -90,7 +90,7 @@ class ISharedImage
 {
 public:
 	virtual Point dimensions() const = 0;
-	virtual void exportBitmap(const boost::filesystem::path & path) const = 0;
+	virtual void exportBitmap(const boost::filesystem::path & path, SDL_Palette * palette) const = 0;
 	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;
 

+ 39 - 0
client/render/ImageLocator.cpp

@@ -70,6 +70,7 @@ bool ImageLocator::empty() const
 ImageLocator ImageLocator::copyFile() const
 {
 	ImageLocator result;
+	result.scalingFactor = 1;
 	result.image = image;
 	result.defFile = defFile;
 	result.defFrame = defFrame;
@@ -89,3 +90,41 @@ ImageLocator ImageLocator::copyFileTransformScale() const
 {
 	return *this; // full copy
 }
+
+std::string ImageLocator::toString() const
+{
+	std::string result;
+	if (empty())
+		return "invalid";
+
+	if (image)
+	{
+		result += image->getOriginalName();
+		assert(!result.empty());
+	}
+
+	if (defFile)
+	{
+		result += defFile->getOriginalName();
+		assert(!result.empty());
+		result += "-" + std::to_string(defGroup);
+		result += "-" + std::to_string(defFrame);
+	}
+
+	if (verticalFlip)
+		result += "-vflip";
+
+	if (horizontalFlip)
+		result += "-hflip";
+
+	if (scalingFactor > 1)
+		result += "-scale" + std::to_string(scalingFactor);
+
+	if (playerColored.isValidPlayer())
+		result += "-player" + playerColored.toString();
+
+	if (layer != EImageLayer::ALL)
+		result += "-layer" + std::to_string(static_cast<int>(layer));
+
+	return result;
+}

+ 6 - 1
client/render/ImageLocator.h

@@ -32,7 +32,7 @@ struct ImageLocator
 
 	bool verticalFlip = false;
 	bool horizontalFlip = false;
-	int8_t scalingFactor = 1;
+	int8_t scalingFactor = 0; // 0 = auto / use default scaling
 	EImageLayer layer = EImageLayer::ALL;
 
 	ImageLocator() = default;
@@ -46,4 +46,9 @@ struct ImageLocator
 	ImageLocator copyFile() const;
 	ImageLocator copyFileTransform() const;
 	ImageLocator copyFileTransformScale() const;
+
+	// generates string representation of this image locator
+	// guaranteed to be a valid file path with no extension
+	// but may contain '/' if source file is in directory
+	std::string toString() const;
 };

+ 1 - 2
client/renderSDL/ImageScaled.cpp

@@ -28,7 +28,6 @@ ImageScaled::ImageScaled(const ImageLocator & inputLocator, const std::shared_pt
 	, alphaValue(SDL_ALPHA_OPAQUE)
 	, blitMode(mode)
 {
-	locator.scalingFactor = GH.screenHandler().getScalingFactor();
 	setBodyEnabled(true);
 	if (mode == EImageBlitMode::ALPHA)
 		setShadowEnabled(true);
@@ -52,7 +51,7 @@ void ImageScaled::scaleTo(const Point & size)
 
 void ImageScaled::exportBitmap(const boost::filesystem::path &path) const
 {
-	source->exportBitmap(path);
+	source->exportBitmap(path, nullptr);
 }
 
 bool ImageScaled::isTransparent(const Point &coords) const

+ 49 - 17
client/renderSDL/RenderHandler.cpp

@@ -23,6 +23,7 @@
 
 #include "../../lib/json/JsonUtils.h"
 #include "../../lib/filesystem/Filesystem.h"
+#include "../../lib/VCMIDirs.h"
 
 #include <vcmi/ArtifactService.h>
 #include <vcmi/CreatureService.h>
@@ -136,14 +137,6 @@ int RenderHandler::getScalingFactor() const
 	return GH.screenHandler().getScalingFactor();
 }
 
-std::shared_ptr<IImage> RenderHandler::createImageReference(const ImageLocator & locator, std::shared_ptr<ISharedImage> input, EImageBlitMode mode)
-{
-	if (getScalingFactor() == 1 || locator.scalingFactor != 1 || locator.empty())
-		return input->createImageReference(mode);
-	else
-		return std::make_shared<ImageScaled>(locator, input, mode);
-}
-
 ImageLocator RenderHandler::getLocatorForAnimationFrame(const AnimationPath & path, int frame, int group)
 {
 	const auto & layout = getAnimationLayout(path);
@@ -196,13 +189,26 @@ std::shared_ptr<ISharedImage> RenderHandler::loadImageFromFileUncached(const Ima
 	throw std::runtime_error("Invalid image locator received!");
 }
 
+void RenderHandler::storeCachedImage(const ImageLocator & locator, std::shared_ptr<ISharedImage> image)
+{
+	imageFiles[locator] = image;
+
+#if 0
+	const boost::filesystem::path outPath = VCMIDirs::get().userExtractedPath() / "imageCache" / (locator.toString() + ".png");
+	boost::filesystem::path outDir = outPath;
+	outDir.remove_filename();
+	boost::filesystem::create_directories(outDir);
+	image->exportBitmap(outPath , nullptr);
+#endif
+}
+
 std::shared_ptr<ISharedImage> RenderHandler::loadImageFromFile(const ImageLocator & locator)
 {
 	if (imageFiles.count(locator))
 		return imageFiles.at(locator);
 
 	auto result = loadImageFromFileUncached(locator);
-	imageFiles[locator] = result;
+	storeCachedImage(locator, result);
 	return result;
 }
 
@@ -219,7 +225,7 @@ std::shared_ptr<ISharedImage> RenderHandler::transformImage(const ImageLocator &
 	if (locator.horizontalFlip)
 		result = result->horizontalFlip();
 
-	imageFiles[locator] = result;
+	storeCachedImage(locator, result);
 	return result;
 }
 
@@ -232,9 +238,12 @@ std::shared_ptr<ISharedImage> RenderHandler::scaleImage(const ImageLocator & loc
 
 	assert(locator.scalingFactor != 1); // should be filtered-out before
 
-	handle->setOverlayEnabled(locator.layer == EImageLayer::ALL || locator.layer == EImageLayer::OVERLAY);
 	handle->setBodyEnabled(locator.layer == EImageLayer::ALL || locator.layer == EImageLayer::BODY);
-	handle->setShadowEnabled(locator.layer == EImageLayer::ALL || locator.layer == EImageLayer::SHADOW);
+	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)
 		handle->playerColored(locator.playerColored);
 
@@ -242,29 +251,52 @@ std::shared_ptr<ISharedImage> RenderHandler::scaleImage(const ImageLocator & loc
 
 	// TODO: try to optimize image size (possibly even before scaling?) - trim image borders if they are completely transparent
 	auto result = handle->getSharedImage();
-	imageFiles[locator] = result;
+	storeCachedImage(locator, result);
 	return result;
 }
 
 std::shared_ptr<IImage> RenderHandler::loadImage(const ImageLocator & locator, EImageBlitMode mode)
 {
-	return createImageReference(locator, loadImageImpl(locator), mode);
+	if (locator.scalingFactor == 0 && getScalingFactor() != 1 )
+	{
+		auto unscaledLocator = locator;
+		auto scaledLocator = locator;
+
+		unscaledLocator.scalingFactor = 1;
+		scaledLocator.scalingFactor = getScalingFactor();
+		auto unscaledImage = loadImageImpl(unscaledLocator);
+
+		return std::make_shared<ImageScaled>(scaledLocator, unscaledImage, mode);
+	}
+
+	if (locator.scalingFactor == 0)
+	{
+		auto scaledLocator = locator;
+		scaledLocator.scalingFactor = getScalingFactor();
+
+		return loadImageImpl(scaledLocator)->createImageReference(mode);
+	}
+	else
+	{
+		return loadImageImpl(locator)->createImageReference(mode);
+	}
 }
 
 std::shared_ptr<IImage> RenderHandler::loadImage(const AnimationPath & path, int frame, int group, EImageBlitMode mode)
 {
-	auto locator = getLocatorForAnimationFrame(path, frame, group);
+	ImageLocator locator = getLocatorForAnimationFrame(path, frame, group);
 	return loadImage(locator, mode);
 }
 
 std::shared_ptr<IImage> RenderHandler::loadImage(const ImagePath & path, EImageBlitMode mode)
 {
-	return loadImage(ImageLocator(path), mode);
+	ImageLocator locator(path);
+	return loadImage(locator, mode);
 }
 
 std::shared_ptr<IImage> RenderHandler::createImage(SDL_Surface * source)
 {
-	return createImageReference(ImageLocator(), std::make_shared<SDLImageShared>(source), EImageBlitMode::ALPHA);
+	return std::make_shared<SDLImageShared>(source)->createImageReference(EImageBlitMode::ALPHA);
 }
 
 std::shared_ptr<CAnimation> RenderHandler::loadAnimation(const AnimationPath & path, EImageBlitMode mode)

+ 1 - 1
client/renderSDL/RenderHandler.h

@@ -33,6 +33,7 @@ class RenderHandler : public IRenderHandler
 
 	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);
 
 	std::shared_ptr<ISharedImage> loadImageImpl(const ImageLocator & config);
 
@@ -46,7 +47,6 @@ class RenderHandler : public IRenderHandler
 
 	int getScalingFactor() const;
 
-	std::shared_ptr<IImage> createImageReference(const ImageLocator & locator, std::shared_ptr<ISharedImage> input, EImageBlitMode mode);
 public:
 
 	// IRenderHandler implementation

+ 30 - 7
client/renderSDL/SDLImage.cpp

@@ -22,6 +22,7 @@
 
 #include <tbb/parallel_for.h>
 #include <SDL_surface.h>
+#include <SDL_image.h>
 
 class SDLImageLoader;
 
@@ -327,9 +328,16 @@ std::shared_ptr<ISharedImage> SDLImageShared::scaleTo(const Point & size, SDL_Pa
 	return ret;
 }
 
-void SDLImageShared::exportBitmap(const boost::filesystem::path& path) const
+void SDLImageShared::exportBitmap(const boost::filesystem::path& path, SDL_Palette * palette) const
 {
-	SDL_SaveBMP(surf, path.string().c_str());
+	if (!surf)
+		return;
+
+	if (palette && surf->format->palette)
+		SDL_SetSurfacePalette(surf, palette);
+	IMG_SavePNG(surf, path.string().c_str());
+	if (palette && surf->format->palette)
+		SDL_SetSurfacePalette(surf, originalPalette);
 }
 
 void SDLImageIndexed::playerColored(PlayerColor player)
@@ -407,6 +415,10 @@ 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)
+		colorsToSkipMask |= (1 << 0) + (1 << 1) + (1 << 4);
+
 	// Note: here we skip first colors in the palette that are predefined in H3 images
 	for(int i = 0; i < currentPalette->ncolors; i++)
 	{
@@ -428,9 +440,11 @@ SDLImageIndexed::SDLImageIndexed(const std::shared_ptr<ISharedImage> & image, SD
 	currentPalette = SDL_AllocPalette(originalPalette->ncolors);
 	SDL_SetPaletteColors(currentPalette, originalPalette->colors, 0, originalPalette->ncolors);
 
-	setOverlayColor(Colors::TRANSPARENCY);
 	if (mode == EImageBlitMode::ALPHA)
+	{
+		setOverlayColor(Colors::TRANSPARENCY);
 		setShadowTransparency(1.0);
+	}
 }
 
 SDLImageIndexed::~SDLImageIndexed()
@@ -468,7 +482,9 @@ void SDLImageIndexed::setShadowTransparency(float factor)
 
 void SDLImageIndexed::setOverlayColor(const ColorRGBA & color)
 {
-	for (int i : {5,6,7})
+	currentPalette->colors[5] = CSDL_Ext::toSDL(addColors(targetPalette[5], color));
+
+	for (int i : {6,7})
 	{
 		if (colorsSimilar(originalPalette->colors[i], sourcePalette[i]))
 			currentPalette->colors[i] = CSDL_Ext::toSDL(addColors(targetPalette[i], color));
@@ -500,8 +516,10 @@ void SDLImageIndexed::setOverlayEnabled(bool on)
 {
 	if (on)
 		setOverlayColor(Colors::WHITE_TRUE);
-	else
+
+	if (!on && blitMode == EImageBlitMode::ALPHA)
 		setOverlayColor(Colors::TRANSPARENCY);
+
 	overlayEnabled = on;
 }
 
@@ -532,6 +550,11 @@ void SDLImageIndexed::draw(SDL_Surface * where, const Point & pos, const Rect *
 	image->draw(where, currentPalette, pos, src, Colors::WHITE_TRUE, alphaValue, blitMode);
 }
 
+void SDLImageIndexed::exportBitmap(const boost::filesystem::path & path) const
+{
+	image->exportBitmap(path, currentPalette);
+}
+
 void SDLImageIndexed::scaleTo(const Point & size)
 {
 	image = image->scaleTo(size, currentPalette);
@@ -552,9 +575,9 @@ void SDLImageRGB::scaleInteger(int factor)
 	image = image->scaleInteger(factor, nullptr);
 }
 
-void SDLImageBase::exportBitmap(const boost::filesystem::path & path) const
+void SDLImageRGB::exportBitmap(const boost::filesystem::path & path) const
 {
-	image->exportBitmap(path);
+	image->exportBitmap(path, nullptr);
 }
 
 bool SDLImageBase::isTransparent(const Point & coords) const

+ 3 - 2
client/renderSDL/SDLImage.h

@@ -51,7 +51,7 @@ public:
 
 	void draw(SDL_Surface * where, SDL_Palette * palette, const Point & dest, const Rect * src, const ColorRGBA & colorMultiplier, uint8_t alpha, EImageBlitMode mode) const override;
 
-	void exportBitmap(const boost::filesystem::path & path) const override;
+	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;
@@ -74,7 +74,6 @@ protected:
 public:
 	SDLImageBase(const std::shared_ptr<ISharedImage> & image, EImageBlitMode mode);
 
-	void exportBitmap(const boost::filesystem::path & path) const override;
 	bool isTransparent(const Point & coords) const override;
 	Point dimensions() const override;
 	void setAlpha(uint8_t value) override;
@@ -103,6 +102,7 @@ public:
 	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;
 	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;
@@ -121,6 +121,7 @@ public:
 	void adjustPalette(const ColorFilter & shifter, uint32_t colorsToSkipMask) override;
 	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;

+ 9 - 18
client/widgets/Images.cpp

@@ -14,6 +14,7 @@
 
 #include "../gui/CGuiHandler.h"
 #include "../renderSDL/SDL_Extensions.h"
+#include "../render/AssetGenerator.h"
 #include "../render/IImage.h"
 #include "../render/IRenderHandler.h"
 #include "../render/CAnimation.h"
@@ -172,28 +173,18 @@ void FilledTexturePlayerIndexed::setPlayerColor(PlayerColor player)
 	texture->playerColored(player);
 }
 
+FilledTexturePlayerColored::FilledTexturePlayerColored(Rect position)
+	:CFilledTexture(ImagePath::builtin("DiBoxBck"), position)
+{
+}
+
 void FilledTexturePlayerColored::setPlayerColor(PlayerColor player)
 {
-	// Color transform to make color of brown DIBOX.PCX texture match color of specified player
-	std::array<ColorFilter, PlayerColor::PLAYER_LIMIT_I> filters = {
-		ColorFilter::genRangeShifter(  0.25,  0,     0,     1.25, 0.00, 0.00 ), // red
-		ColorFilter::genRangeShifter(  0,     0,     0,     0.45, 1.20, 4.50 ), // blue
-		ColorFilter::genRangeShifter(  0.40,  0.27,  0.23,  1.10, 1.20, 1.15 ), // tan
-		ColorFilter::genRangeShifter( -0.27,  0.10, -0.27,  0.70, 1.70, 0.70 ), // green
-		ColorFilter::genRangeShifter(  0.47,  0.17, -0.27,  1.60, 1.20, 0.70 ), // orange
-		ColorFilter::genRangeShifter(  0.12, -0.1,   0.25,  1.15, 1.20, 2.20 ), // purple
-		ColorFilter::genRangeShifter( -0.13,  0.23,  0.23,  0.90, 1.20, 2.20 ), // teal
-		ColorFilter::genRangeShifter(  0.44,  0.15,  0.25,  1.00, 1.00, 1.75 )  // pink
-	};
+	AssetGenerator::createPlayerColoredBackground(player);
 
-	assert(player.isValidPlayer());
-	if (!player.isValidPlayer())
-	{
-		logGlobal->error("Unable to colorize to invalid player color %d!", static_cast<int>(player.getNum()));
-		return;
-	}
+	ImagePath imagePath = ImagePath::builtin("DialogBoxBackground_" + player.toString() + ".bmp");
 
-	texture->adjustPalette(filters[player.getNum()], 0);
+	texture = GH.renderHandler().loadImage(imagePath, EImageBlitMode::COLORKEY);
 }
 
 CAnimImage::CAnimImage(const AnimationPath & name, size_t Frame, size_t Group, int x, int y, ui8 Flags):

+ 1 - 1
client/widgets/Images.h

@@ -92,7 +92,7 @@ public:
 class FilledTexturePlayerColored : public CFilledTexture
 {
 public:
-	using CFilledTexture::CFilledTexture;
+	FilledTexturePlayerColored(Rect position);
 
 	void setPlayerColor(PlayerColor player);
 };

+ 13 - 2
client/widgets/VideoWidget.cpp

@@ -17,7 +17,12 @@
 #include "../render/Canvas.h"
 
 VideoWidgetBase::VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio)
-	: playAudio(playAudio)
+	: VideoWidgetBase(position, video, playAudio, 1.0)
+{
+}
+
+VideoWidgetBase::VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor)
+	: playAudio(playAudio), scaleFactor(scaleFactor)
 {
 	addUsedEvents(TIME);
 	pos += position;
@@ -28,7 +33,7 @@ VideoWidgetBase::~VideoWidgetBase() = default;
 
 void VideoWidgetBase::playVideo(const VideoPath & fileToPlay)
 {
-	videoInstance = CCS->videoh->open(fileToPlay, false);
+	videoInstance = CCS->videoh->open(fileToPlay, scaleFactor);
 	if (videoInstance)
 	{
 		pos.w = videoInstance->size().x;
@@ -142,6 +147,12 @@ VideoWidgetOnce::VideoWidgetOnce(const Point & position, const VideoPath & video
 {
 }
 
+VideoWidgetOnce::VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor, const std::function<void()> & callback)
+	: VideoWidgetBase(position, video, playAudio, scaleFactor)
+	, callback(callback)
+{
+}
+
 void VideoWidgetOnce::onPlaybackFinished()
 {
 	callback();

+ 3 - 0
client/widgets/VideoWidget.h

@@ -22,6 +22,7 @@ class VideoWidgetBase : public CIntObject
 	std::pair<std::unique_ptr<ui8[]>, si64> audioData = {nullptr, 0};
 	int audioHandle = -1;
 	bool playAudio = false;
+	float scaleFactor = 1.0;
 
 	void loadAudio(const VideoPath & file);
 	void startAudio();
@@ -29,6 +30,7 @@ class VideoWidgetBase : public CIntObject
 
 protected:
 	VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio);
+	VideoWidgetBase(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor);
 
 	virtual void onPlaybackFinished() = 0;
 	void playVideo(const VideoPath & video);
@@ -62,4 +64,5 @@ class VideoWidgetOnce final: public VideoWidgetBase
 	void onPlaybackFinished() final;
 public:
 	VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, const std::function<void()> & callback);
+	VideoWidgetOnce(const Point & position, const VideoPath & video, bool playAudio, float scaleFactor, const std::function<void()> & callback);
 };

+ 16 - 1
client/windows/CCastleInterface.cpp

@@ -54,6 +54,7 @@
 #include "../../lib/entities/building/CBuilding.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/mapObjects/TownBuildingInstance.h"
 
 
 static bool useCompactCreatureBox()
@@ -845,7 +846,21 @@ bool CCastleBuildings::buildingTryActivateCustomUI(BuildingID buildingToTest, Bu
 
 void CCastleBuildings::enterRewardable(BuildingID building)
 {
-	LOCPLINT->cb->visitTownBuilding(town, building);
+	if (town->visitingHero == nullptr)
+	{
+		MetaString message;
+		message.appendTextID("core.genrltxt.273"); // only visiting heroes may visit %s
+		message.replaceTextID(town->town->buildings.at(building)->getNameTextID());
+
+		LOCPLINT->showInfoDialog(message.toString());
+	}
+	else
+	{
+		if (town->rewardableBuildings.at(building)->wasVisited(town->visitingHero))
+			enterBuilding(building);
+		else
+			LOCPLINT->cb->visitTownBuilding(town, building);
+	}
 }
 
 void CCastleBuildings::enterBlacksmith(BuildingID building, ArtifactID artifactID)

+ 1 - 1
client/windows/CHeroOverview.cpp

@@ -125,7 +125,7 @@ void CHeroOverview::genControls()
     r = Rect(302, 3 * borderOffset + yOffset + 62, 292, 32);
     backgroundRectangles.push_back(std::make_shared<TransparentFilledRectangle>(r.resize(1), rectangleColor, borderColor));
 
-    auto stacksCountChances = LOCPLINT->cb->getSettings().getVector(EGameSettings::HEROES_STARTING_STACKS_CHANCES);
+    auto stacksCountChances = CGI->engineSettings()->getVector(EGameSettings::HEROES_STARTING_STACKS_CHANCES);
 
     // army
     int space = (260 - 7 * 32) / 6;

+ 7 - 7
client/windows/CKingdomInterface.cpp

@@ -879,9 +879,9 @@ public:
 	std::shared_ptr<CAnimImage> background;
 	std::vector<std::shared_ptr<CArtPlace>> arts;
 
-	ArtSlotsTab()
+	ArtSlotsTab(CIntObject * parent)
 	{
-		OBJECT_CONSTRUCTION;
+		OBJECT_CONSTRUCTION_TARGETED(parent);
 		background = std::make_shared<CAnimImage>(AnimationPath::builtin("OVSLOT"), 4);
 		pos = background->pos;
 		for(int i=0; i<9; i++)
@@ -897,9 +897,9 @@ public:
 	std::shared_ptr<CButton> btnLeft;
 	std::shared_ptr<CButton> btnRight;
 
-	BackpackTab()
+	BackpackTab(CIntObject * parent)
 	{
-		OBJECT_CONSTRUCTION;
+		OBJECT_CONSTRUCTION_TARGETED(parent);
 		background = std::make_shared<CAnimImage>(AnimationPath::builtin("OVSLOT"), 5);
 		pos = background->pos;
 		btnLeft = std::make_shared<CButton>(Point(269, 66), AnimationPath::builtin("HSBTNS3"), CButton::tooltip(), 0);
@@ -915,9 +915,9 @@ CHeroItem::CHeroItem(const CGHeroInstance * Hero)
 	OBJECT_CONSTRUCTION;
 
 	artTabs.resize(3);
-	auto arts1 = std::make_shared<ArtSlotsTab>();
-	auto arts2 = std::make_shared<ArtSlotsTab>();
-	auto backpack = std::make_shared<BackpackTab>();
+	auto arts1 = std::make_shared<ArtSlotsTab>(this);
+	auto arts2 = std::make_shared<ArtSlotsTab>(this);
+	auto backpack = std::make_shared<BackpackTab>(this);
 	artTabs[0] = arts1;
 	artTabs[1] = arts2;
 	artTabs[2] = backpack;

+ 7 - 5
client/windows/CWindowObject.cpp

@@ -20,6 +20,7 @@
 #include "../windows/CMessage.h"
 #include "../renderSDL/SDL_PixelAccess.h"
 #include "../render/IImage.h"
+#include "../render/IScreenHandler.h"
 #include "../render/IRenderHandler.h"
 #include "../render/Canvas.h"
 
@@ -115,7 +116,8 @@ void CWindowObject::updateShadow()
 void CWindowObject::setShadow(bool on)
 {
 	//size of shadow
-	static const int size = 8;
+	int sizeOriginal = 8;
+	int size = sizeOriginal * GH.screenHandler().getScalingFactor();
 
 	if(on == !shadowParts.empty())
 		return;
@@ -180,9 +182,9 @@ void CWindowObject::setShadow(bool on)
 		//FIXME: do something with this points
 		Point shadowStart;
 		if (options & BORDERED)
-			shadowStart = Point(size - 14, size - 14);
+			shadowStart = Point(sizeOriginal - 14, sizeOriginal - 14);
 		else
-			shadowStart = Point(size, size);
+			shadowStart = Point(sizeOriginal, sizeOriginal);
 
 		Point shadowPos;
 		if (options & BORDERED)
@@ -198,8 +200,8 @@ void CWindowObject::setShadow(bool on)
 
 		//create base 8x8 piece of shadow
 		SDL_Surface * shadowCorner = CSDL_Ext::copySurface(shadowCornerTempl);
-		SDL_Surface * shadowBottom = CSDL_Ext::scaleSurface(shadowBottomTempl, fullsize.x - size, size);
-		SDL_Surface * shadowRight  = CSDL_Ext::scaleSurface(shadowRightTempl,  size, fullsize.y - size);
+		SDL_Surface * shadowBottom = CSDL_Ext::scaleSurface(shadowBottomTempl, (fullsize.x - sizeOriginal) * GH.screenHandler().getScalingFactor(), size);
+		SDL_Surface * shadowRight  = CSDL_Ext::scaleSurface(shadowRightTempl,  size, (fullsize.y - sizeOriginal) * GH.screenHandler().getScalingFactor());
 
 		blitAlphaCol(shadowBottom, 0);
 		blitAlphaRow(shadowRight, 0);

+ 49 - 1
client/windows/GUIClasses.cpp

@@ -744,8 +744,9 @@ CShipyardWindow::CShipyardWindow(const TResources & cost, int state, BoatId boat
 		AnimationPath boatFilename = boatConstructor->getBoatAnimationName();
 
 		Point waterCenter = Point(bgWater->pos.x+bgWater->pos.w/2, bgWater->pos.y+bgWater->pos.h/2);
-		bgShip = std::make_shared<CAnimImage>(boatFilename, 0, 7, 120, 96, 0);
+		bgShip = std::make_shared<CShowableAnim>(120, 96, boatFilename, CShowableAnim::CREATURE_MODE, 100, 7);
 		bgShip->center(waterCenter);
+		bgWater->needRefresh = true;
 	}
 
 	// Create resource icons and costs.
@@ -1607,3 +1608,50 @@ void CObjectListWindow::keyPressed(EShortcut key)
 	list->scrollTo(sel);
 	changeSelection(sel);
 }
+
+VideoWindow::VideoWindow(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function<void(bool skipped)> closeCb)
+	: CWindowObject(BORDERED | SHADOW_DISABLED | NEEDS_ANIMATED_BACKGROUND), closeCb(closeCb)
+{
+	OBJECT_CONSTRUCTION;
+
+	addUsedEvents(LCLICK | KEYBOARD);
+
+	if(!rim.empty())
+	{
+		videoPlayer = std::make_shared<VideoWidgetOnce>(Point(80, 186), video, true, [this](){ exit(false); });
+		pos = center(Rect(0, 0, 800, 600));
+	}
+	else
+	{
+		videoPlayer = std::make_shared<VideoWidgetOnce>(Point(0, 0), video, true, scaleFactor, [this](){ exit(false); });
+		pos = center(Rect(0, 0, videoPlayer->pos.w, videoPlayer->pos.h));
+	}
+
+	if(showBackground)
+		backgroundAroundWindow = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(-pos.x, -pos.y, GH.screenDimensions().x, GH.screenDimensions().y));
+
+	if(!rim.empty())
+		setBackground(rim);
+}
+
+void VideoWindow::exit(bool skipped)
+{
+	close();
+	if(closeCb)
+		closeCb(skipped);
+}
+
+void VideoWindow::clickPressed(const Point & cursorPosition)
+{
+	exit(true);
+}
+
+void VideoWindow::keyPressed(EShortcut key)
+{
+	exit(true);
+}
+
+bool VideoWindow::receiveEvent(const Point & position, int eventType) const
+{
+	return true;  // capture click also outside of window
+}

+ 17 - 1
client/windows/GUIClasses.h

@@ -43,6 +43,7 @@ class CAnimImage;
 class CFilledTexture;
 class IImage;
 class VideoWidget;
+class VideoWidgetOnce;
 
 enum class EUserEvent;
 
@@ -296,7 +297,7 @@ public:
 class CShipyardWindow : public CStatusbarWindow
 {
 	std::shared_ptr<CPicture> bgWater;
-	std::shared_ptr<CAnimImage> bgShip;
+	std::shared_ptr<CShowableAnim> bgShip;
 
 	std::shared_ptr<CLabel> title;
 	std::shared_ptr<CLabel> costLabel;
@@ -501,3 +502,18 @@ public:
 	CThievesGuildWindow(const CGObjectInstance * _owner);
 };
 
+class VideoWindow : public CWindowObject
+{
+	std::shared_ptr<VideoWidgetOnce> videoPlayer;
+	std::shared_ptr<CFilledTexture> backgroundAroundWindow;
+
+	std::function<void(bool)> closeCb;
+
+	void exit(bool skipped);
+public:
+	VideoWindow(VideoPath video, ImagePath rim, bool showBackground, float scaleFactor, std::function<void(bool)> closeCb);
+
+	void clickPressed(const Point & cursorPosition) override;
+	void keyPressed(EShortcut key) override;
+	bool receiveEvent(const Point & position, int eventType) const override;
+};

+ 35 - 1
client/windows/InfoWindows.cpp

@@ -245,8 +245,11 @@ void CRClickPopup::createAndPush(const CGObjectInstance * obj, const Point & p,
 	}
 }
 
-CRClickPopupInt::CRClickPopupInt(const std::shared_ptr<CIntObject> & our)
+CRClickPopupInt::CRClickPopupInt(const std::shared_ptr<CIntObject> & our) :
+	dragDistance(Point(0, 0))
 {
+	addUsedEvents(DRAG_POPUP);
+
 	CCS->curh->hide();
 	inner = our;
 	addChild(our.get(), false);
@@ -257,6 +260,17 @@ CRClickPopupInt::~CRClickPopupInt()
 	CCS->curh->show();
 }
 
+void CRClickPopupInt::mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance)
+{
+	if(!settings["adventure"]["rightButtonDrag"].Bool())
+		return;
+	
+	dragDistance += lastUpdateDistance;
+	
+	if(dragDistance.length() > 16)
+		close();
+}
+
 Point CInfoBoxPopup::toScreen(Point p)
 {
 	auto bounds = adventureInt->terrainAreaPixels();
@@ -267,6 +281,18 @@ Point CInfoBoxPopup::toScreen(Point p)
 	return p;
 }
 
+void CInfoBoxPopup::mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance)
+{
+	if(!settings["adventure"]["rightButtonDrag"].Bool())
+		return;
+	
+	dragDistance += lastUpdateDistance;
+	
+	if(dragDistance.length() > 16)
+		close();
+}
+
+
 CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town)
 	: CWindowObject(RCLICK_POPUP | PLAYER_COLORED, ImagePath::builtin("TOWNQVBK"), toScreen(position))
 {
@@ -275,6 +301,8 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGTownInstance * town)
 
 	OBJECT_CONSTRUCTION;
 	tooltip = std::make_shared<CTownTooltip>(Point(9, 10), iah);
+
+	addUsedEvents(DRAG_POPUP);
 }
 
 CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero)
@@ -285,6 +313,8 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGHeroInstance * hero)
 
 	OBJECT_CONSTRUCTION;
 	tooltip = std::make_shared<CHeroTooltip>(Point(9, 10), iah);
+	
+	addUsedEvents(DRAG_POPUP);
 }
 
 CInfoBoxPopup::CInfoBoxPopup(Point position, const CGGarrison * garr)
@@ -295,6 +325,8 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGGarrison * garr)
 
 	OBJECT_CONSTRUCTION;
 	tooltip = std::make_shared<CArmyTooltip>(Point(9, 10), iah);
+	
+	addUsedEvents(DRAG_POPUP);
 }
 
 CInfoBoxPopup::CInfoBoxPopup(Point position, const CGCreature * creature)
@@ -302,6 +334,8 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGCreature * creature)
 {
 	OBJECT_CONSTRUCTION;
 	tooltip = std::make_shared<CreatureTooltip>(Point(9, 10), creature);
+	
+	addUsedEvents(DRAG_POPUP);
 }
 
 std::shared_ptr<WindowBase>

+ 8 - 0
client/windows/InfoWindows.h

@@ -78,9 +78,13 @@ class CRClickPopupInt : public CRClickPopup
 {
 	std::shared_ptr<CIntObject> inner;
 
+	Point dragDistance;
+
 public:
 	CRClickPopupInt(const std::shared_ptr<CIntObject> & our);
 	~CRClickPopupInt();
+
+	void mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) override;
 };
 
 /// popup on adventure map for town\hero and other objects with customized popup content
@@ -89,11 +93,15 @@ class CInfoBoxPopup : public CWindowObject
 	std::shared_ptr<CIntObject> tooltip;
 	Point toScreen(Point pos);
 
+	Point dragDistance;
+
 public:
 	CInfoBoxPopup(Point position, const CGTownInstance * town);
 	CInfoBoxPopup(Point position, const CGHeroInstance * hero);
 	CInfoBoxPopup(Point position, const CGGarrison * garr);
 	CInfoBoxPopup(Point position, const CGCreature * creature);
+
+	void mouseDraggedPopup(const Point & cursorPosition, const Point & lastUpdateDistance) override;
 };
 
 /// component selection window

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

@@ -11,6 +11,7 @@
 
 #include "AdventureOptionsTab.h"
 
+#include "../../eventsSDL/InputHandler.h"
 #include "../../../lib/filesystem/ResourcePath.h"
 #include "../../gui/CGuiHandler.h"
 #include "../../widgets/Buttons.h"
@@ -36,6 +37,9 @@ AdventureOptionsTab::AdventureOptionsTab()
 	OBJECT_CONSTRUCTION;
 	setRedrawParent(true);
 
+	addConditional("touchscreen", GH.input().getCurrentInputMode() == InputMode::TOUCH);
+	addConditional("keyboardMouse", GH.input().getCurrentInputMode() == InputMode::KEYBOARD_AND_MOUSE);
+	addConditional("controller", GH.input().getCurrentInputMode() == InputMode::CONTROLLER);
 #ifdef VCMI_MOBILE
 	addConditional("mobile", true);
 	addConditional("desktop", false);
@@ -126,6 +130,10 @@ AdventureOptionsTab::AdventureOptionsTab()
 	{
 		return setBoolSetting("adventure", "leftButtonDrag", value);
 	});
+	addCallback("rightButtonDragChanged", [](bool value)
+	{
+		return setBoolSetting("adventure", "rightButtonDrag", value);
+	});
 	addCallback("smoothDraggingChanged", [](bool value)
 	{
 		return setBoolSetting("adventure", "smoothDragging", value);
@@ -177,6 +185,10 @@ AdventureOptionsTab::AdventureOptionsTab()
 	if (leftButtonDragCheckbox)
 		leftButtonDragCheckbox->setSelected(settings["adventure"]["leftButtonDrag"].Bool());
 
+	std::shared_ptr<CToggleButton> rightButtonDragCheckbox = widget<CToggleButton>("rightButtonDragCheckbox");
+	if (rightButtonDragCheckbox)
+		rightButtonDragCheckbox->setSelected(settings["adventure"]["rightButtonDrag"].Bool());
+
 	std::shared_ptr<CToggleButton> smoothDraggingCheckbox = widget<CToggleButton>("smoothDraggingCheckbox");
 	if (smoothDraggingCheckbox)
 		smoothDraggingCheckbox->setSelected(settings["adventure"]["smoothDragging"].Bool());

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

@@ -97,7 +97,9 @@ GeneralOptionsTab::GeneralOptionsTab()
 	OBJECT_CONSTRUCTION;
 	setRedrawParent(true);
 
-	addConditional("touchscreen", GH.input().hasTouchInputDevice());
+	addConditional("touchscreen", GH.input().getCurrentInputMode() == InputMode::TOUCH);
+	addConditional("keyboardMouse", GH.input().getCurrentInputMode() == InputMode::KEYBOARD_AND_MOUSE);
+	addConditional("controller", GH.input().getCurrentInputMode() == InputMode::CONTROLLER);
 #ifdef VCMI_MOBILE
 	addConditional("mobile", true);
 	addConditional("desktop", false);

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

@@ -45,6 +45,8 @@ SettingsMainWindow::SettingsMainWindow(BattleInterface * parentBattleUi) : Inter
 	addCallback("closeWindow", [this](int) { backButtonCallback(); });
 	build(config);
 
+	addUsedEvents(INPUT_MODE_CHANGE);
+
 	std::shared_ptr<CIntObject> background = widget<CIntObject>("background");
 	pos.w = background->pos.w;
 	pos.h = background->pos.h;
@@ -196,3 +198,8 @@ void SettingsMainWindow::onScreenResize()
 	if (tab)
 		tab->updateResolutionSelector();
 }
+
+void SettingsMainWindow::inputModeChanged(InputMode mode)
+{
+	tabContentArea->reset();
+}

+ 1 - 0
client/windows/settings/SettingsMainWindow.h

@@ -42,5 +42,6 @@ public:
 
 	void showAll(Canvas & to) override;
 	void onScreenResize() override;
+	void inputModeChanged(InputMode mode) override;
 };
 

+ 0 - 1
clientapp/CMakeLists.txt

@@ -119,7 +119,6 @@ endif()
 
 #install icons and desktop file on Linux
 if(NOT WIN32 AND NOT APPLE AND NOT ANDROID)
-	#FIXME: move to client makefile?
 	foreach(iconSize 16 22 32 48 64 128 256 512 1024 2048)
 		install(FILES "icons/vcmiclient.${iconSize}x${iconSize}.png"
 			DESTINATION "share/icons/hicolor/${iconSize}x${iconSize}/apps"

+ 7 - 20
clientapp/EntryPoint.cpp

@@ -30,6 +30,7 @@
 #include "../client/render/Graphics.h"
 #include "../client/render/IRenderHandler.h"
 #include "../client/render/IScreenHandler.h"
+#include "../client/lobby/CBonusSelection.h"
 #include "../client/windows/CMessage.h"
 #include "../client/windows/InfoWindows.h"
 
@@ -65,7 +66,6 @@ static std::optional<std::string> criticalInitializationError;
 #ifndef VCMI_IOS
 void processCommand(const std::string &message);
 #endif
-void playIntro();
 [[noreturn]] static void quitApplication();
 static void mainLoop();
 
@@ -319,13 +319,6 @@ int main(int argc, char * argv[])
 	init();
 #endif
 
-	if(!settings["session"]["headless"].Bool())
-	{
-		if(!vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool())
-			playIntro();
-		GH.screenHandler().clearScreen();
-	}
-
 #ifndef VCMI_NO_THREADED_LOAD
 	#ifdef VCMI_ANDROID // android loads the data quite slowly so we display native progressbar to prevent having only black screen for few seconds
 	{
@@ -381,6 +374,12 @@ int main(int argc, char * argv[])
 	{
 		auto mmenu = CMainMenu::create();
 		GH.curInt = mmenu.get();
+
+		bool playIntroVideo = !settings["session"]["headless"].Bool() && !vm.count("battle") && !vm.count("nointro") && settings["video"]["showIntro"].Bool();
+		if(playIntroVideo)
+			mmenu->playIntroVideos();
+		else
+			mmenu->playMusic();
 	}
 	
 	std::vector<std::string> names;
@@ -402,18 +401,6 @@ int main(int argc, char * argv[])
 	return 0;
 }
 
-//plays intro, ends when intro is over or button has been pressed (handles events)
-void playIntro()
-{
-	if(!CCS->videoh->playIntroVideo(VideoPath::builtin("3DOLOGO.SMK")))
-		return;
-
-	if (!CCS->videoh->playIntroVideo(VideoPath::builtin("NWCLOGO.SMK")))
-		return;
-
-	CCS->videoh->playIntroVideo(VideoPath::builtin("H3INTRO.SMK"));
-}
-
 static void mainLoop()
 {
 #ifndef VCMI_UNIX

+ 1 - 1
clientapp/VCMI_client.rc

@@ -1 +1 @@
-IDI_ICON1   ICON  "vcmi.ico"
+IDI_ICON1   ICON  "icons/vcmi.ico"

+ 0 - 0
client/icons/generate_icns.py → clientapp/icons/generate_icns.py


+ 0 - 0
client/vcmi.ico → clientapp/icons/vcmi.ico


+ 0 - 0
client/icons/vcmiclient.1024x1024.png → clientapp/icons/vcmiclient.1024x1024.png


+ 0 - 0
client/icons/vcmiclient.128x128.png → clientapp/icons/vcmiclient.128x128.png


+ 0 - 0
client/icons/vcmiclient.16x16.png → clientapp/icons/vcmiclient.16x16.png


+ 0 - 0
client/icons/vcmiclient.2048x2048.png → clientapp/icons/vcmiclient.2048x2048.png


+ 0 - 0
client/icons/vcmiclient.22x22.png → clientapp/icons/vcmiclient.22x22.png


+ 0 - 0
client/icons/vcmiclient.256x256.png → clientapp/icons/vcmiclient.256x256.png


+ 0 - 0
client/icons/vcmiclient.32x32.png → clientapp/icons/vcmiclient.32x32.png


+ 0 - 0
client/icons/vcmiclient.48x48.png → clientapp/icons/vcmiclient.48x48.png


+ 0 - 0
client/icons/vcmiclient.512x512.png → clientapp/icons/vcmiclient.512x512.png


+ 0 - 0
client/icons/vcmiclient.64x64.png → clientapp/icons/vcmiclient.64x64.png


+ 0 - 0
client/icons/vcmiclient.desktop → clientapp/icons/vcmiclient.desktop


+ 0 - 0
client/icons/vcmiclient.svg → clientapp/icons/vcmiclient.svg


+ 1 - 2
conanfile.py

@@ -19,7 +19,7 @@ class VCMI(ConanFile):
         "sdl_image/[~2.0.5]",
         "sdl_mixer/[~2.0.4]",
         "sdl_ttf/[~2.0.18]",
-        "onetbb/[^2021.3]",
+        "onetbb/[^2021.7 <2021.10]",  # 2021.10+ breaks mobile builds due to added hwloc dependency
         "xz_utils/[>=5.2.5]", # Required for innoextract
     ]
 
@@ -39,7 +39,6 @@ class VCMI(ConanFile):
 
         "boost/*:shared": True,
         "minizip/*:shared": True,
-        "onetbb/*:shared": True,
     }
 
     def configure(self):

+ 9 - 0
config/bonuses.json

@@ -600,6 +600,15 @@
 			"icon":  "zvs/Lib1.res/DISINTEGRATE"
 		}
 
+	},
+
+	"INVINCIBLE":
+	{
+		"graphics":
+		{
+			"icon":  "zvs/Lib1.res/INVINCIBLE"
+		}
+
 	}
 }
 

+ 4 - 0
config/creatures/castle.json

@@ -10,6 +10,10 @@
 			"cavalryChargeImmunity" :
 			{
 				"type" : "CHARGE_IMMUNITY"
+			},
+			"invincible" :
+			{
+				"type" : "INVINCIBLE"
 			}
 		},
 		"graphics" :

+ 5 - 1
config/schemas/settings.json

@@ -322,7 +322,7 @@
 			"type" : "object",
 			"additionalProperties" : false,
 			"default" : {},
-			"required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "smoothDragging", "backgroundDimLevel", "hideBackground", "backgroundDimSmallWindows" ],
+			"required" : [ "heroMoveTime", "enemyMoveTime", "scrollSpeedPixels", "heroReminder", "quickCombat", "objectAnimation", "terrainAnimation", "forceQuickCombat", "borderScroll", "leftButtonDrag", "rightButtonDrag", "smoothDragging", "backgroundDimLevel", "hideBackground", "backgroundDimSmallWindows" ],
 			"properties" : {
 				"heroMoveTime" : {
 					"type" : "number",
@@ -367,6 +367,10 @@
 					"type" : "boolean",
 					"default" : false
 				},
+				"rightButtonDrag" : {
+					"type" : "boolean",
+					"default" : false
+				},
 				"smoothDragging" : {
 					"type" : "boolean",
 					"default" : true

+ 4 - 0
config/schemas/spell.json

@@ -171,6 +171,10 @@
 			"type" : "boolean",
 			"description" : "If used as creature spell, unit can cast this spell on itself"
 		},
+		"canCastWithoutSkip" : {
+			"type" : "boolean",
+			"description" : "If used the creature will not skip the turn after casting a spell."
+		},
 		"gainChance" : {
 			"type" : "object",
 			"description" : "Chance for this spell to appear in Mage Guild of a specific faction",

+ 28 - 2
config/widgets/settings/adventureOptionsTab.json

@@ -364,13 +364,25 @@
 				},
 				{
 					"text": "vcmi.adventureOptions.leftButtonDrag.hover",
-					"created" : "desktop"
+					"created" : "keyboardMouse"
 				},
 				{
 					"text": "vcmi.adventureOptions.smoothDragging.hover"
 				}
 			]
 		},
+		{
+			"type": "verticalLayout",
+			"customType": "labelDescription",
+			"position": {"x": 225, "y": 415},
+			"items":
+			[
+				{
+					"text": "vcmi.adventureOptions.rightButtonDrag.hover",
+					"created" : "keyboardMouse"
+				}
+			]
+		},
 		{
 			"type": "verticalLayout",
 			"customType": "checkbox",
@@ -411,7 +423,7 @@
 					"name": "leftButtonDragCheckbox",
 					"help": "vcmi.adventureOptions.leftButtonDrag",
 					"callback": "leftButtonDragChanged",
-					"created" : "desktop"
+					"created" : "keyboardMouse"
 				},
 				{
 					"name": "smoothDraggingCheckbox",
@@ -419,6 +431,20 @@
 					"callback": "smoothDraggingChanged"
 				}
 			]
+		},
+		{
+			"type": "verticalLayout",
+			"customType": "checkbox",
+			"position": {"x": 190, "y": 413},
+			"items":
+			[
+				{
+					"name": "rightButtonDragCheckbox",
+					"help": "vcmi.adventureOptions.rightButtonDrag",
+					"callback": "rightButtonDragChanged",
+					"created" : "keyboardMouse"
+				}
+			]
 		}
 	]
 }

+ 5 - 1
docs/modders/Bonus/Bonus_Types.md

@@ -1022,4 +1022,8 @@ Internal bonus, do not use
 
 ### DISINTEGRATE
 
-After death of unit no corpse remains
+When a unit affected by this bonus dies, no corpse is left behind
+
+### INVINCIBLE
+
+The unit affected by this bonus cannot be target of attacks or spells

+ 3 - 0
docs/modders/Entities_Format/Spell_Format.md

@@ -64,6 +64,9 @@
 		// If true, then creature capable of casting this spell can cast this spell on itself
 		// If false, then creature can only cast this spell on other units
 		"canCastOnSelf" : false,
+
+		// If true the creature will not skip the turn after casting a spell
+		"canCastWithoutSkip": false,
 		
 		// If true, spell won't be available on a map without water
 		"onlyOnWaterMap" : true,

+ 1 - 0
docs/modders/Entities_Format/Town_Building_Format.md

@@ -197,6 +197,7 @@ These are just a couple of examples of what can be done in VCMI. See vcmi config
 	"bonuses" : [ BONUS_FORMAT ]
 	
 	// If set to true, this building will not automatically activate on new day or on entering town and needs to be activated manually on click
+	// Note that such building can only be activated by visiting hero, and not by garrisoned hero.
 	"manualHeroVisit" : false,
 	
 	// Bonuses provided by this special building if this building or any of its upgrades are constructed in town

+ 1 - 0
docs/modders/Translations.md

@@ -82,6 +82,7 @@ VCMI Launcher and Map Editor use translation system provided by Qt framework so
 
 - Install Qt Linguist. You can find find standalone version here: https://download.qt.io/linguist_releases/
 - Open `<VCMI Sources>/launcher/translation/` directory, copy `english.ts` file and rename it to your language
+- Open `<VCMI Sources>/launcher/CMakeLists.txt` file with a text editor. In there you need to find list of existing translation files and add new file to the list.
 - Launch Qt Linguist, select Open and navigate to your copied file
 - Select any untranslated string, enter translation in field below, and click "Done and Next" (Ctrl+Return) to navigate to next untranslated string
 - Once translation has been finished, save resulting file.

+ 1 - 0
include/vcmi/spells/Spell.h

@@ -45,6 +45,7 @@ public:
 
 	virtual bool hasSchool(SpellSchool school) const = 0;
 	virtual bool canCastOnSelf() const = 0;
+	virtual bool canCastWithoutSkip() const = 0;
 	virtual void forEachSchool(const SchoolCallback & cb) const = 0;
 	virtual int32_t getCost(const int32_t skillLevel) const = 0;
 

+ 1 - 0
launcher/CMakeLists.txt

@@ -83,6 +83,7 @@ set(launcher_TS
 	"${translationsDir}/portuguese.ts"
 	"${translationsDir}/russian.ts"
 	"${translationsDir}/spanish.ts"
+	"${translationsDir}/swedish.ts"
 	"${translationsDir}/ukrainian.ts"
 	"${translationsDir}/vietnamese.ts"
 )

+ 6 - 0
launcher/modManager/cmodlistview_moc.cpp

@@ -1080,6 +1080,12 @@ bool CModListView::isModEnabled(const QString & modName)
 	return mod.isEnabled();
 }
 
+bool CModListView::isModInstalled(const QString & modName)
+{
+	auto mod = modModel->getMod(modName);
+	return mod.isInstalled();
+}
+
 QString CModListView::getTranslationModName(const QString & language)
 {
 	for(const auto & modName : modModel->getModList())

Деякі файли не було показано, через те що забагато файлів було змінено