Преглед изворни кода

Merge branch 'develop' into unlimited-autosave

Dydzio пре 2 година
родитељ
комит
bcb061b58f
100 измењених фајлова са 1185 додато и 972 уклоњено
  1. 4 0
      .github/workflows/github.yml
  2. 3 3
      AI/Nullkiller/AIGateway.cpp
  3. 2 2
      AI/Nullkiller/AIUtility.cpp
  4. 2 2
      AI/VCAI/AIUtility.cpp
  5. 2 2
      AI/VCAI/VCAI.cpp
  6. 22 1
      CMakePresets.json
  7. 2 0
      Mods/vcmi/config/vcmi/english.json
  8. 2 0
      Mods/vcmi/config/vcmi/german.json
  9. 2 1
      android/vcmi-app/src/main/AndroidManifest.xml
  10. 14 0
      android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java
  11. 0 13
      client/CMT.cpp
  12. 0 3
      client/CMT.h
  13. 2 1
      client/CPlayerInterface.cpp
  14. 8 11
      client/adventureMap/CInfoBar.cpp
  15. 2 2
      client/adventureMap/CInfoBar.h
  16. 10 13
      client/adventureMap/CList.cpp
  17. 2 2
      client/adventureMap/CList.h
  18. 3 4
      client/adventureMap/CMinimap.cpp
  19. 2 2
      client/adventureMap/CMinimap.h
  20. 6 9
      client/battle/BattleFieldController.cpp
  21. 2 2
      client/battle/BattleFieldController.h
  22. 2 0
      client/eventsSDL/InputHandler.cpp
  23. 6 24
      client/eventsSDL/InputSourceText.cpp
  24. 20 0
      client/eventsSDL/InputSourceTouch.cpp
  25. 4 0
      client/eventsSDL/InputSourceTouch.h
  26. 2 2
      client/gui/CIntObject.cpp
  27. 0 1
      client/gui/CIntObject.h
  28. 20 17
      client/gui/EventDispatcher.cpp
  29. 1 1
      client/gui/EventDispatcher.h
  30. 12 9
      client/gui/EventsReceiver.h
  31. 2 0
      client/ios/utils.h
  32. 6 0
      client/ios/utils.mm
  33. 4 8
      client/lobby/CBonusSelection.cpp
  34. 2 2
      client/lobby/CBonusSelection.h
  35. 4 3
      client/lobby/CSavingScreen.cpp
  36. 1 1
      client/lobby/CSelectionBase.cpp
  37. 1 1
      client/lobby/CSelectionBase.h
  38. 1 1
      client/lobby/OptionsTab.cpp
  39. 1 1
      client/lobby/OptionsTab.h
  40. 19 13
      client/lobby/RandomMapTab.cpp
  41. 4 2
      client/lobby/RandomMapTab.h
  42. 15 15
      client/lobby/SelectionTab.cpp
  43. 2 2
      client/lobby/SelectionTab.h
  44. 3 6
      client/mainmenu/CCampaignScreen.cpp
  45. 1 1
      client/mainmenu/CCampaignScreen.h
  46. 2 2
      client/mainmenu/CPrologEpilogVideo.cpp
  47. 1 1
      client/mainmenu/CPrologEpilogVideo.h
  48. 2 2
      client/mainmenu/CreditsScreen.cpp
  49. 1 1
      client/mainmenu/CreditsScreen.h
  50. 4 10
      client/mapView/MapViewActions.cpp
  51. 2 2
      client/mapView/MapViewActions.h
  52. 4 0
      client/render/IScreenHandler.h
  53. 62 13
      client/renderSDL/ScreenHandler.cpp
  54. 6 2
      client/renderSDL/ScreenHandler.h
  55. 68 35
      client/widgets/Buttons.cpp
  56. 11 3
      client/widgets/Buttons.h
  57. 10 18
      client/widgets/CArtifactHolder.cpp
  58. 4 5
      client/widgets/CArtifactHolder.h
  59. 3 6
      client/widgets/CArtifactsOfHeroAltar.cpp
  60. 3 3
      client/widgets/CArtifactsOfHeroBase.cpp
  61. 9 15
      client/widgets/CComponent.cpp
  62. 2 2
      client/widgets/CComponent.h
  63. 2 5
      client/widgets/CGarrisonInt.cpp
  64. 2 2
      client/widgets/CGarrisonInt.h
  65. 2 2
      client/widgets/CWindowWithArtifacts.cpp
  66. 12 17
      client/widgets/MiscWidgets.cpp
  67. 6 6
      client/widgets/MiscWidgets.h
  68. 5 5
      client/widgets/Slider.cpp
  69. 1 1
      client/widgets/Slider.h
  70. 5 8
      client/widgets/TextControls.cpp
  71. 2 2
      client/widgets/TextControls.h
  72. 42 50
      client/windows/CCastleInterface.cpp
  73. 12 12
      client/windows/CCastleInterface.h
  74. 3 4
      client/windows/CCreatureWindow.cpp
  75. 1 1
      client/windows/CCreatureWindow.h
  76. 10 13
      client/windows/CHeroWindow.cpp
  77. 2 2
      client/windows/CHeroWindow.h
  78. 7 18
      client/windows/CKingdomInterface.cpp
  79. 2 5
      client/windows/CKingdomInterface.h
  80. 4 6
      client/windows/CQuestLog.cpp
  81. 3 3
      client/windows/CQuestLog.h
  82. 6 7
      client/windows/CSpellWindow.cpp
  83. 4 4
      client/windows/CSpellWindow.h
  84. 3 7
      client/windows/CTradeWindow.cpp
  85. 2 2
      client/windows/CTradeWindow.h
  86. 1 1
      client/windows/CreaturePurchaseCard.cpp
  87. 1 1
      client/windows/CreaturePurchaseCard.h
  88. 16 24
      client/windows/GUIClasses.cpp
  89. 9 9
      client/windows/GUIClasses.h
  90. 23 2
      client/windows/settings/GeneralOptionsTab.cpp
  91. 2 0
      cmake_modules/VCMI_lib.cmake
  92. 18 7
      config/schemas/settings.json
  93. 11 1
      config/widgets/settings/generalOptionsTab.json
  94. 60 54
      launcher/translation/polish.ts
  95. 36 23
      lib/ArtifactUtils.cpp
  96. 1 0
      lib/ArtifactUtils.h
  97. 88 249
      lib/CArtHandler.cpp
  98. 68 116
      lib/CArtHandler.h
  99. 192 0
      lib/CArtifactInstance.cpp
  100. 102 0
      lib/CArtifactInstance.h

+ 4 - 0
.github/workflows/github.yml

@@ -76,6 +76,10 @@ jobs:
             os: ubuntu-20.04
             test: 0
             preset: linux-gcc-test
+          - platform: linux
+            os: ubuntu-20.04
+            test: 0
+            preset: linux-gcc-debug
           - platform: mac-intel
             os: macos-12
             test: 0

+ 3 - 3
AI/Nullkiller/AIGateway.cpp

@@ -705,7 +705,7 @@ void AIGateway::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstan
 	//you can't request action from action-response thread
 	requestActionASAP([=]()
 	{
-		if(removableUnits)
+		if(removableUnits && up->tempOwner == down->tempOwner)
 			pickBestCreatures(down, up);
 
 		answerQuery(queryID, 0);
@@ -1006,7 +1006,7 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 				//FIXME: why are the above possible to be null?
 
 				bool emptySlotFound = false;
-				for(auto slot : artifact->artType->possibleSlots.at(target->bearerType()))
+				for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
 				{
 					ArtifactLocation destLocation(target, slot);
 					if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move
@@ -1019,7 +1019,7 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
 				}
 				if(!emptySlotFound) //try to put that atifact in already occupied slot
 				{
-					for(auto slot : artifact->artType->possibleSlots.at(target->bearerType()))
+					for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
 					{
 						auto otherSlot = target->getSlot(slot);
 						if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one

+ 2 - 2
AI/Nullkiller/AIUtility.cpp

@@ -306,10 +306,10 @@ bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2
 	auto art1 = a1->artType;
 	auto art2 = a2->artType;
 
-	if(art1->price == art2->price)
+	if(art1->getPrice() == art2->getPrice())
 		return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);
 	else
-		return art1->price > art2->price;
+		return art1->getPrice() > art2->getPrice();
 }
 
 bool isWeeklyRevisitable(const CGObjectInstance * obj)

+ 2 - 2
AI/VCAI/AIUtility.cpp

@@ -256,8 +256,8 @@ bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2
 	auto art1 = a1->artType;
 	auto art2 = a2->artType;
 
-	if(art1->price == art2->price)
+	if(art1->getPrice() == art2->getPrice())
 		return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);
 	else
-		return art1->price > art2->price;
+		return art1->getPrice() > art2->getPrice();
 }

+ 2 - 2
AI/VCAI/VCAI.cpp

@@ -1192,7 +1192,7 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot
 				//FIXME: why are the above possible to be null?
 
 				bool emptySlotFound = false;
-				for(auto slot : artifact->artType->possibleSlots.at(target->bearerType()))
+				for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
 				{
 					ArtifactLocation destLocation(target, slot);
 					if(target->isPositionFree(slot) && artifact->canBePutAt(destLocation, true)) //combined artifacts are not always allowed to move
@@ -1205,7 +1205,7 @@ void VCAI::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance * ot
 				}
 				if(!emptySlotFound) //try to put that atifact in already occupied slot
 				{
-					for(auto slot : artifact->artType->possibleSlots.at(target->bearerType()))
+					for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
 					{
 						auto otherSlot = target->getSlot(slot);
 						if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one

+ 22 - 1
CMakePresets.json

@@ -70,6 +70,18 @@
             "description": "VCMI Linux GCC",
             "inherits": "linux-release",
             "cacheVariables": {
+                "ENABLE_LUA" : "ON",
+                "CMAKE_C_COMPILER": "/usr/bin/gcc",
+                "CMAKE_CXX_COMPILER": "/usr/bin/g++"
+            }
+        },
+        {
+            "name": "linux-gcc-debug",
+            "displayName": "GCC x86_64-pc-linux-gnu (debug)",
+            "description": "VCMI Linux GCC (Debug)",
+            "inherits": "linux-release",
+            "cacheVariables": {
+                "CMAKE_BUILD_TYPE": "Debug",
                 "ENABLE_LUA" : "ON",
                 "ENABLE_PCH" : "OFF",
                 "CMAKE_C_COMPILER": "/usr/bin/gcc",
@@ -93,7 +105,6 @@
             "inherits": "linux-test",
             "cacheVariables": {
                 "ENABLE_LUA" : "OFF",
-                "ENABLE_PCH" : "OFF",
                 "CMAKE_C_COMPILER": "/usr/bin/gcc",
                 "CMAKE_CXX_COMPILER": "/usr/bin/g++"
             }
@@ -239,6 +250,11 @@
             "hidden": true,
             "configuration": "RelWithDebInfo"
         },
+        {
+            "name": "default-debug",
+            "hidden": true,
+            "configuration": "Debug"
+        },
         {
             "name": "linux-clang-release",
             "configurePreset": "linux-clang-release",
@@ -259,6 +275,11 @@
             "configurePreset": "linux-gcc-release",
             "inherits": "default-release"
         },
+        {
+            "name": "linux-gcc-debug",
+            "configurePreset": "linux-gcc-debug",
+            "inherits": "default-debug"
+        },
         {
             "name": "macos-xcode-release",
             "configurePreset": "macos-xcode-release",

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

@@ -74,6 +74,8 @@
 	"vcmi.systemOptions.longTouchMenu.entry"     : "%d milliseconds",
 	"vcmi.systemOptions.framerateButton.hover"  : "Show FPS",
 	"vcmi.systemOptions.framerateButton.help"   : "{Show FPS}\n\nToggle the visibility of the Frames Per Second counter in the corner of the game window",
+	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Haptic feedback",
+	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Haptic feedback}\n\nToggle the haptic feedback on touch inputs",
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "Show Messages in Info Panel",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Show Messages in Info Panel}\n\nWhenever possible, game messages from visiting map objects will be shown in the info panel, instead of popping up in a separate window.",

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

@@ -74,6 +74,8 @@
 	"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.",
+	"vcmi.systemOptions.hapticFeedbackButton.hover"  : "Haptisches Feedback",
+	"vcmi.systemOptions.hapticFeedbackButton.help"   : "{Haptisches Feedback}\n\nHaptisches Feedback bei Touch-Eingaben.",
 
 	"vcmi.adventureOptions.infoBarPick.hover" : "Meldungen im Infobereich anzeigen",
 	"vcmi.adventureOptions.infoBarPick.help" : "{Meldungen im Infobereich anzeigen}\n\nWann immer möglich, werden Spielnachrichten von besuchten Kartenobjekten in der Infoleiste angezeigt, anstatt als Popup-Fenster zu erscheinen",

+ 2 - 1
android/vcmi-app/src/main/AndroidManifest.xml

@@ -3,6 +3,7 @@
     package="eu.vcmi.vcmi">
 
     <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.VIBRATE" />
 
     <application
         android:extractNativeLibs="true"
@@ -50,4 +51,4 @@
             android:exported="false"/>
     </application>
 
-</manifest>
+</manifest>

+ 14 - 0
android/vcmi-app/src/main/java/eu/vcmi/vcmi/NativeMethods.java

@@ -3,11 +3,14 @@ package eu.vcmi.vcmi;
 import android.app.Activity;
 import android.content.Context;
 import android.content.Intent;
+import android.os.Build;
 import android.os.Environment;
 import android.os.Looper;
 import android.os.Message;
 import android.os.Messenger;
 import android.os.RemoteException;
+import android.os.VibrationEffect;
+import android.os.Vibrator;
 
 import org.libsdl.app.SDL;
 import org.libsdl.app.SDLActivity;
@@ -138,6 +141,17 @@ public class NativeMethods
     {
         internalProgressDisplay(false);
     }
+    
+    @SuppressWarnings(Const.JNI_METHOD_SUPPRESS)
+    public static void hapticFeedback()
+    {
+        final Context ctx = SDL.getContext();
+        if (Build.VERSION.SDK_INT >= 26) {
+            ((Vibrator) ctx.getSystemService(ctx.VIBRATOR_SERVICE)).vibrate(VibrationEffect.createPredefined(VibrationEffect.EFFECT_TICK));
+        } else {
+            ((Vibrator) ctx.getSystemService(ctx.VIBRATOR_SERVICE)).vibrate(30);
+        }
+    }
 
     private static void internalProgressDisplay(final boolean show)
     {

+ 0 - 13
client/CMT.cpp

@@ -449,19 +449,6 @@ void playIntro()
 
 static void mainLoop()
 {
-	SettingsListener resChanged = settings.listen["video"]["resolution"];
-	SettingsListener fsChanged = settings.listen["video"]["fullscreen"];
-
-	auto functor = [](const JsonNode &newState){
-		GH.dispatchMainThread([](){
-			boost::unique_lock<boost::recursive_mutex> lock(*CPlayerInterface::pim);
-			GH.onScreenResize();
-		});
-	};
-
-	resChanged(functor);
-	fsChanged(functor);
-
 	inGuiThread.reset(new bool(true));
 
 	while(1) //main SDL events loop

+ 0 - 3
client/CMT.h

@@ -10,13 +10,10 @@
 #pragma once
 
 struct SDL_Texture;
-struct SDL_Window;
 struct SDL_Renderer;
 struct SDL_Surface;
 
 extern SDL_Texture * screenTexture;
-
-extern SDL_Window * mainWindow;
 extern SDL_Renderer * mainRenderer;
 
 extern SDL_Surface *screen;      // main screen surface

+ 2 - 1
client/CPlayerInterface.cpp

@@ -1116,7 +1116,8 @@ void CPlayerInterface::showBlockingDialog( const std::string &text, const std::v
 		if (pom.size() > 1)
 			charperline = 50;
 		GH.windows().createAndPushWindow<CSelWindow>(text, playerID, charperline, intComps, pom, askID);
-		intComps[0]->clickLeft(true, false);
+		intComps[0]->clickPressed(GH.getCursorPosition());
+		intComps[0]->clickReleased(GH.getCursorPosition());
 	}
 }
 

+ 8 - 11
client/adventureMap/CInfoBar.cpp

@@ -271,20 +271,17 @@ void CInfoBar::tick(uint32_t msPassed)
 	}
 }
 
-void CInfoBar::clickLeft(tribool down, bool previousState)
+void CInfoBar::clickReleased(const Point & cursorPosition)
 {
-	if(down)
-	{
-		if(state == HERO || state == TOWN)
-			showGameStatus();
-		else if(state == GAME)
-			showDate();
-		else
-			popComponents(true);
-	}
+	if(state == HERO || state == TOWN)
+		showGameStatus();
+	else if(state == GAME)
+		showDate();
+	else
+		popComponents(true);
 }
 
-void CInfoBar::showPopupWindow()
+void CInfoBar::showPopupWindow(const Point & cursorPosition)
 {
 	CRClickPopup::createAndPush(CGI->generaltexth->allTexts[109]);
 }

+ 2 - 2
client/adventureMap/CInfoBar.h

@@ -154,8 +154,8 @@ private:
 
 	void tick(uint32_t msPassed) override;
 
-	void clickLeft(tribool down, bool previousState) override;
-	void showPopupWindow() override;
+	void clickReleased(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
 	void hover(bool on) override;
 
 	void playNewDaySound();

+ 10 - 13
client/adventureMap/CList.cpp

@@ -40,25 +40,22 @@ CList::CListItem::CListItem(CList * Parent)
 
 CList::CListItem::~CListItem() = default;
 
-void CList::CListItem::showPopupWindow()
+void CList::CListItem::showPopupWindow(const Point & cursorPosition)
 {
 	showTooltip();
 }
 
-void CList::CListItem::clickLeft(tribool down, bool previousState)
+void CList::CListItem::clickPressed(const Point & cursorPosition)
 {
-	if(down == true)
+	//second click on already selected item
+	if(parent->selected == this->shared_from_this())
 	{
-		//second click on already selected item
-		if(parent->selected == this->shared_from_this())
-		{
-			open();
-		}
-		else
-		{
-			//first click - switch selection
-			parent->select(this->shared_from_this());
-		}
+		open();
+	}
+	else
+	{
+		//first click - switch selection
+		parent->select(this->shared_from_this());
 	}
 }
 

+ 2 - 2
client/adventureMap/CList.h

@@ -35,8 +35,8 @@ protected:
 		CListItem(CList * parent);
 		~CListItem();
 
-		void showPopupWindow() override;
-		void clickLeft(tribool down, bool previousState) override;
+		void showPopupWindow(const Point & cursorPosition) override;
+		void clickPressed(const Point & cursorPosition) override;
 		void hover(bool on) override;
 		void onSelect(bool on);
 

+ 3 - 4
client/adventureMap/CMinimap.cpp

@@ -143,13 +143,12 @@ void CMinimap::gesturePanning(const Point & initialPosition, const Point & curre
 		moveAdvMapSelection(currentPosition);
 }
 
-void CMinimap::clickLeft(tribool down, bool previousState)
+void CMinimap::clickPressed(const Point & cursorPosition)
 {
-	if(down)
-		moveAdvMapSelection(GH.getCursorPosition());
+	moveAdvMapSelection(cursorPosition);
 }
 
-void CMinimap::showPopupWindow()
+void CMinimap::showPopupWindow(const Point & cursorPosition)
 {
 	CRClickPopup::createAndPush(CGI->generaltexth->zelp[291].second);
 }

+ 2 - 2
client/adventureMap/CMinimap.h

@@ -44,8 +44,8 @@ class CMinimap : public CIntObject
 	int level;
 
 	void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override;
-	void clickLeft(tribool down, bool previousState) override;
-	void showPopupWindow() override;
+	void clickPressed(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
 	void hover(bool on) override;
 	void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override;
 

+ 6 - 9
client/battle/BattleFieldController.cpp

@@ -184,7 +184,7 @@ void BattleFieldController::createHeroes()
 void BattleFieldController::gesture(bool on, const Point & initialPosition, const Point & finalPosition)
 {
 	if (!on && pos.isInside(finalPosition))
-		clickLeft(false, false);
+		clickPressed(finalPosition);
 }
 
 void BattleFieldController::gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance)
@@ -213,18 +213,15 @@ void BattleFieldController::mouseMoved(const Point & cursorPosition, const Point
 		owner.actionsController->onHoverEnded();
 }
 
-void BattleFieldController::clickLeft(tribool down, bool previousState)
+void BattleFieldController::clickPressed(const Point & cursorPosition)
 {
-	if(!down)
-	{
-		BattleHex selectedHex = getHoveredHex();
+	BattleHex selectedHex = getHoveredHex();
 
-		if (selectedHex != BattleHex::INVALID)
-			owner.actionsController->onHexLeftClicked(selectedHex);
-	}
+	if (selectedHex != BattleHex::INVALID)
+		owner.actionsController->onHexLeftClicked(selectedHex);
 }
 
-void BattleFieldController::showPopupWindow()
+void BattleFieldController::showPopupWindow(const Point & cursorPosition)
 {
 	BattleHex selectedHex = getHoveredHex();
 

+ 2 - 2
client/battle/BattleFieldController.h

@@ -100,8 +100,8 @@ class BattleFieldController : public CIntObject
 	void gesture(bool on, const Point & initialPosition, const Point & finalPosition) override;
 	void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override;
 	void mouseMoved(const Point & cursorPosition, const Point & lastUpdateDistance) override;
-	void clickLeft(tribool down, bool previousState) override;
-	void showPopupWindow() override;
+	void clickPressed(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
 	void activate() override;
 
 	void showAll(Canvas & to) override;

+ 2 - 0
client/eventsSDL/InputHandler.cpp

@@ -121,6 +121,8 @@ void InputHandler::preprocessEvent(const SDL_Event & ev)
 	{
 		Settings full = settings.write["video"]["fullscreen"];
 		full->Bool() = !full->Bool();
+
+		GH.onScreenResize();
 		return;
 	}
 	else if(ev.type == SDL_USEREVENT)

+ 6 - 24
client/eventsSDL/InputSourceText.cpp

@@ -14,20 +14,17 @@
 #include "../CMT.h"
 #include "../gui/CGuiHandler.h"
 #include "../gui/EventDispatcher.h"
+#include "../render/IScreenHandler.h"
+#include "../renderSDL/SDL_Extensions.h"
 
 #include "../../lib/Rect.h"
 
 #include <SDL_events.h>
-#include <SDL_render.h>
 
 #ifdef VCMI_APPLE
 #	include <dispatch/dispatch.h>
 #endif
 
-#ifdef VCMI_IOS
-#	include "ios/utils.h"
-#endif
-
 void InputSourceText::handleEventTextInput(const SDL_TextInputEvent & text)
 {
 	GH.events().dispatchTextInput(text.text);
@@ -40,30 +37,15 @@ void InputSourceText::handleEventTextEditing(const SDL_TextEditingEvent & text)
 
 void InputSourceText::startTextInput(const Rect & whereInput)
 {
+
 #ifdef VCMI_APPLE
 	dispatch_async(dispatch_get_main_queue(), ^{
 #endif
 
-	// TODO ios: looks like SDL bug actually, try fixing there
-	auto renderer = SDL_GetRenderer(mainWindow);
-	float scaleX, scaleY;
-	SDL_Rect viewport;
-	SDL_RenderGetScale(renderer, &scaleX, &scaleY);
-	SDL_RenderGetViewport(renderer, &viewport);
-
-#ifdef VCMI_IOS
-	const auto nativeScale = iOS_utils::screenScale();
-	scaleX /= nativeScale;
-	scaleY /= nativeScale;
-#endif
-
-	SDL_Rect rectInScreenCoordinates;
-	rectInScreenCoordinates.x = (viewport.x + whereInput.x) * scaleX;
-	rectInScreenCoordinates.y = (viewport.y + whereInput.y) * scaleY;
-	rectInScreenCoordinates.w = whereInput.w * scaleX;
-	rectInScreenCoordinates.h = whereInput.h * scaleY;
+	Rect rectInScreenCoordinates = GH.screenHandler().convertLogicalPointsToWindow(whereInput);
+	SDL_Rect textInputRect = CSDL_Ext::toSDL(rectInScreenCoordinates);
 
-	SDL_SetTextInputRect(&rectInScreenCoordinates);
+	SDL_SetTextInputRect(&textInputRect);
 
 	if (SDL_IsTextInputActive() == SDL_FALSE)
 	{

+ 20 - 0
client/eventsSDL/InputSourceTouch.cpp

@@ -22,6 +22,12 @@
 #include "../gui/MouseButton.h"
 #include "../gui/WindowHandler.h"
 
+#if defined(VCMI_ANDROID)
+#include "../../lib/CAndroidVMHelper.h"
+#elif defined(VCMI_IOS)
+#include "../ios/utils.h"
+#endif
+
 #include <SDL_events.h>
 #include <SDL_hints.h>
 #include <SDL_timer.h>
@@ -32,6 +38,7 @@ InputSourceTouch::InputSourceTouch()
 	params.useRelativeMode = settings["general"]["userRelativePointer"].Bool();
 	params.relativeModeSpeedFactor = settings["general"]["relativePointerSpeedMultiplier"].Float();
 	params.longTouchTimeMilliseconds = settings["general"]["longTouchTimeMilliseconds"].Float();
+	params.hapticFeedbackEnabled = settings["general"]["hapticFeedback"].Bool();
 
 	if (params.useRelativeMode)
 		state = TouchState::RELATIVE_MODE;
@@ -100,6 +107,7 @@ void InputSourceTouch::handleEventFingerDown(const SDL_TouchFingerEvent & tfinge
 {
 	// FIXME: better place to update potentially changed settings?
 	params.longTouchTimeMilliseconds = settings["general"]["longTouchTimeMilliseconds"].Float();
+	params.hapticFeedbackEnabled = settings["general"]["hapticFeedback"].Bool();
 
 	lastTapTimeTicks = tfinger.timestamp;
 
@@ -215,6 +223,7 @@ void InputSourceTouch::handleUpdate()
 		if (currentTime > lastTapTimeTicks + params.longTouchTimeMilliseconds)
 		{
 			GH.events().dispatchShowPopup(GH.getCursorPosition());
+			hapticFeedback();
 
 			if (GH.windows().isTopWindowPopup())
 				state = TouchState::TAP_DOWN_LONG;
@@ -287,3 +296,14 @@ void InputSourceTouch::emitPinchEvent(const SDL_TouchFingerEvent & tfinger)
 	if (distanceOld > params.pinchSensitivityThreshold)
 		GH.events().dispatchGesturePinch(lastTapPosition, distanceNew / distanceOld);
 }
+
+void InputSourceTouch::hapticFeedback() {
+	if(params.hapticFeedbackEnabled) {
+#if defined(VCMI_ANDROID)
+        CAndroidVMHelper vmHelper;
+        vmHelper.callStaticVoidMethod(CAndroidVMHelper::NATIVE_METHODS_DEFAULT_CLASS, "hapticFeedback");
+#elif defined(VCMI_IOS)
+    	iOS_utils::hapticFeedback();
+#endif
+	}
+}

+ 4 - 0
client/eventsSDL/InputSourceTouch.h

@@ -79,6 +79,8 @@ struct TouchInputParameters
 	uint32_t pinchSensitivityThreshold = 10;
 
 	bool useRelativeMode = false;
+
+	bool hapticFeedbackEnabled = false;
 };
 
 /// Class that handles touchscreen input from SDL events
@@ -94,6 +96,8 @@ class InputSourceTouch
 
 	void emitPanningEvent(const SDL_TouchFingerEvent & tfinger);
 	void emitPinchEvent(const SDL_TouchFingerEvent & tfinger);
+	
+	void hapticFeedback();
 
 public:
 	InputSourceTouch();

+ 2 - 2
client/gui/CIntObject.cpp

@@ -313,7 +313,7 @@ void CKeyShortcut::keyPressed(EShortcut key)
 	if( assignedKey == key && assignedKey != EShortcut::NONE && !shortcutPressed)
 	{
 		shortcutPressed = true;
-		clickLeft(true, false);
+		clickPressed(GH.getCursorPosition());
 	}
 }
 
@@ -322,7 +322,7 @@ void CKeyShortcut::keyReleased(EShortcut key)
 	if( assignedKey == key && assignedKey != EShortcut::NONE && shortcutPressed)
 	{
 		shortcutPressed = false;
-		clickLeft(false, true);
+		clickReleased(GH.getCursorPosition());
 	}
 }
 

+ 0 - 1
client/gui/CIntObject.h

@@ -62,7 +62,6 @@ public:
 	CIntObject(int used=0, Point offset=Point());
 	virtual ~CIntObject();
 
-	/// allows capturing key input so it will be delivered only to this element
 	bool captureThisKey(EShortcut key) override;
 
 	void addUsedEvents(ui16 newActions);

+ 20 - 17
client/gui/EventDispatcher.cpp

@@ -128,23 +128,23 @@ void EventDispatcher::dispatchMouseDoubleClick(const Point & position)
 
 		if(i->receiveEvent(position, AEventsReceiver::DOUBLECLICK))
 		{
-			i->clickDouble();
+			i->clickDouble(position);
 			doubleClicked = true;
 		}
 	}
 
 	if(!doubleClicked)
-		handleLeftButtonClick(true);
+		handleLeftButtonClick(position, true);
 }
 
 void EventDispatcher::dispatchMouseLeftButtonPressed(const Point & position)
 {
-	handleLeftButtonClick(true);
+	handleLeftButtonClick(position, true);
 }
 
 void EventDispatcher::dispatchMouseLeftButtonReleased(const Point & position)
 {
-	handleLeftButtonClick(false);
+	handleLeftButtonClick(position, false);
 }
 
 void EventDispatcher::dispatchShowPopup(const Point & position)
@@ -155,10 +155,10 @@ void EventDispatcher::dispatchShowPopup(const Point & position)
 		if(!vstd::contains(rclickable, i))
 			continue;
 
-		if( !i->receiveEvent(GH.getCursorPosition(), AEventsReceiver::LCLICK))
+		if( !i->receiveEvent(position, AEventsReceiver::LCLICK))
 			continue;
 
-		i->showPopupWindow();
+		i->showPopupWindow(position);
 	}
 }
 
@@ -170,7 +170,7 @@ void EventDispatcher::dispatchClosePopup(const Point & position)
 	assert(!GH.windows().isTopWindowPopup());
 }
 
-void EventDispatcher::handleLeftButtonClick(bool isPressed)
+void EventDispatcher::handleLeftButtonClick(const Point & position, bool isPressed)
 {
 	auto hlp = lclickable;
 	for(auto & i : hlp)
@@ -178,20 +178,23 @@ void EventDispatcher::handleLeftButtonClick(bool isPressed)
 		if(!vstd::contains(lclickable, i))
 			continue;
 
-		auto prev = i->isMouseLeftButtonPressed();
-
-		if(!isPressed)
-			i->mouseClickedState = isPressed;
-
-		if( i->receiveEvent(GH.getCursorPosition(), AEventsReceiver::LCLICK))
+		if( i->receiveEvent(position, AEventsReceiver::LCLICK))
 		{
 			if(isPressed)
-				i->mouseClickedState = isPressed;
-			i->clickLeft(isPressed, prev);
+				i->clickPressed(position);
+
+			if (i->mouseClickedState && !isPressed)
+				i->clickReleased(position);
+
+			i->mouseClickedState = isPressed;
 		}
-		else if(!isPressed)
+		else
 		{
-			i->clickLeft(boost::logic::indeterminate, prev);
+			if(i->mouseClickedState && !isPressed)
+			{
+				i->mouseClickedState = isPressed;
+				i->clickCancel(position);
+			}
 		}
 	}
 }

+ 1 - 1
client/gui/EventDispatcher.h

@@ -35,7 +35,7 @@ class EventDispatcher
 	EventReceiversList textInterested;
 	EventReceiversList panningInterested;
 
-	void handleLeftButtonClick(bool isPressed);
+	void handleLeftButtonClick(const Point & position, bool isPressed);
 
 
 	template<typename Functor>

+ 12 - 9
client/gui/EventsReceiver.h

@@ -15,7 +15,6 @@ VCMI_LIB_NAMESPACE_END
 
 class EventDispatcher;
 enum class EShortcut;
-using boost::logic::tribool;
 
 /// Class that is capable of subscribing and receiving input events
 /// Acts as base class for all UI elements
@@ -34,9 +33,18 @@ protected:
 	/// Deactivates particular events for this UI element. Uses unnamed enum from this class
 	void deactivateEvents(ui16 what);
 
-	virtual void clickLeft(tribool down, bool previousState) {}
-	virtual void showPopupWindow() {}
-	virtual void clickDouble() {}
+	/// allows capturing key input so it will be delivered only to this element
+	virtual bool captureThisKey(EShortcut key) = 0;
+
+	/// If true, event of selected type in selected position will be processed by this element
+	virtual bool receiveEvent(const Point & position, int eventType) const= 0;
+
+public:
+	virtual void clickPressed(const Point & cursorPosition) {}
+	virtual void clickReleased(const Point & cursorPosition) {}
+	virtual void clickCancel(const Point & cursorPosition) {}
+	virtual void showPopupWindow(const Point & cursorPosition) {}
+	virtual void clickDouble(const Point & cursorPosition) {}
 
 	/// Called when user pans screen by specified distance
 	virtual void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) {}
@@ -62,11 +70,6 @@ protected:
 
 	virtual void tick(uint32_t msPassed) {}
 
-	virtual bool captureThisKey(EShortcut key) = 0;
-
-	/// If true, event of selected type in selected position will be processed by this element
-	virtual bool receiveEvent(const Point & position, int eventType) const= 0;
-
 public:
 	AEventsReceiver();
 	virtual ~AEventsReceiver() = default;

+ 2 - 0
client/ios/utils.h

@@ -15,4 +15,6 @@ double screenScale();
 
 void showLoadingIndicator();
 void hideLoadingIndicator();
+
+void hapticFeedback();
 }

+ 6 - 0
client/ios/utils.mm

@@ -43,4 +43,10 @@ void hideLoadingIndicator()
 	[indicator removeFromSuperview];
 	indicator = nil;
 }
+
+void hapticFeedback()
+{
+    auto hapticGen = [[UIImpactFeedbackGenerator alloc] initWithStyle:UIImpactFeedbackStyleLight];
+    [hapticGen impactOccurred];
+}
 }

+ 4 - 8
client/lobby/CBonusSelection.cpp

@@ -499,23 +499,19 @@ void CBonusSelection::CRegion::updateState()
 	}
 }
 
-void CBonusSelection::CRegion::clickLeft(tribool down, bool previousState)
+void CBonusSelection::CRegion::clickReleased(const Point & cursorPosition)
 {
-	//select if selectable & clicked inside our graphic
-	if(indeterminate(down))
-		return;
-
-	if(!down && selectable && !graphicsNotSelected->getSurface()->isTransparent(GH.getCursorPosition() - pos.topLeft()))
+	if(selectable && !graphicsNotSelected->getSurface()->isTransparent(cursorPosition - pos.topLeft()))
 	{
 		CSH->setCampaignMap(idOfMapAndRegion);
 	}
 }
 
-void CBonusSelection::CRegion::showPopupWindow()
+void CBonusSelection::CRegion::showPopupWindow(const Point & cursorPosition)
 {
 	// FIXME: For some reason "down" is only ever contain indeterminate_value
 	auto text = CSH->si->campState->scenario(idOfMapAndRegion).regionText;
-	if(!graphicsNotSelected->getSurface()->isTransparent(GH.getCursorPosition() - pos.topLeft()) && text.size())
+	if(!graphicsNotSelected->getSurface()->isTransparent(cursorPosition - pos.topLeft()) && text.size())
 	{
 		CRClickPopup::createAndPush(text);
 	}

+ 2 - 2
client/lobby/CBonusSelection.h

@@ -47,8 +47,8 @@ public:
 	public:
 		CRegion(CampaignScenarioID id, bool accessible, bool selectable, const CampaignRegions & campDsc);
 		void updateState();
-		void clickLeft(tribool down, bool previousState) override;
-		void showPopupWindow() override;
+		void clickReleased(const Point & cursorPosition) override;
+		void showPopupWindow(const Point & cursorPosition) override;
 	};
 
 	void createBonusesIcons();

+ 4 - 3
client/lobby/CSavingScreen.cpp

@@ -36,10 +36,10 @@ CSavingScreen::CSavingScreen()
 	localMi->mapHeader = std::unique_ptr<CMapHeader>(new CMapHeader(*LOCPLINT->cb->getMapHeader()));
 
 	tabSel = std::make_shared<SelectionTab>(screenType);
-	curTab = tabSel;
-	tabSel->toggleMode();
-
 	tabSel->callOnSelect = std::bind(&CSavingScreen::changeSelection, this, _1);
+	tabSel->toggleMode();
+	curTab = tabSel;
+		
 	buttonStart = std::make_shared<CButton>(Point(411, 535), "SCNRSAV.DEF", CGI->generaltexth->zelp[103], std::bind(&CSavingScreen::saveGame, this), EShortcut::LOBBY_SAVE_GAME);
 }
 
@@ -62,6 +62,7 @@ void CSavingScreen::changeSelection(std::shared_ptr<CMapInfo> to)
 
 	localMi = to;
 	card->changeSelection();
+	card->redraw();
 }
 
 void CSavingScreen::saveGame()

+ 1 - 1
client/lobby/CSelectionBase.cpp

@@ -377,7 +377,7 @@ void CFlagBox::recreate()
 	}
 }
 
-void CFlagBox::showPopupWindow()
+void CFlagBox::showPopupWindow(const Point & cursorPosition)
 {
 	if(SEL->getMapInfo())
 		GH.windows().createAndPushWindow<CFlagBoxTooltipBox>(iconsTeamFlags);

+ 1 - 1
client/lobby/CSelectionBase.h

@@ -135,7 +135,7 @@ class CFlagBox : public CIntObject
 public:
 	CFlagBox(const Rect & rect);
 	void recreate();
-	void showPopupWindow() override;
+	void showPopupWindow(const Point & cursorPosition) override;
 	void showTeamsPopup();
 
 	class CFlagBoxTooltipBox : public CWindowObject

+ 1 - 1
client/lobby/OptionsTab.cpp

@@ -432,7 +432,7 @@ void OptionsTab::SelectedBox::update()
 	subtitle->setText(getName());
 }
 
-void OptionsTab::SelectedBox::showPopupWindow()
+void OptionsTab::SelectedBox::showPopupWindow(const Point & cursorPosition)
 {
 	// cases when we do not need to display a message
 	if(settings.castle == -2 && CPlayerSettingsHelper::type == TOWN)

+ 1 - 1
client/lobby/OptionsTab.h

@@ -101,7 +101,7 @@ public:
 		std::shared_ptr<CLabel> subtitle;
 
 		SelectedBox(Point position, PlayerSettings & settings, SelType type);
-		void showPopupWindow() override;
+		void showPopupWindow(const Point & cursorPosition) override;
 		void scrollBy(int distance) override;
 
 		void update();

+ 19 - 13
client/lobby/RandomMapTab.cpp

@@ -119,12 +119,11 @@ RandomMapTab::RandomMapTab():
 		std::string cbRoadType = "selectRoad_" + road->getJsonKey();
 		addCallback(cbRoadType, [&, road](bool on)
 		{
-			mapGenOptions->setRoadEnabled(road->getJsonKey(), on);
+			mapGenOptions->setRoadEnabled(road->getId(), on);
 			updateMapInfoByHost();
 		});
 	}
 	
-	
 	build(config);
 	
 	updateMapInfoByHost();
@@ -313,7 +312,7 @@ void RandomMapTab::setMapGenOptions(std::shared_ptr<CMapGenOptions> opts)
 	{
 		if(auto w = widget<CToggleButton>(r->getJsonKey()))
 		{
-			w->setSelected(opts->isRoadEnabled(r->getJsonKey()));
+			w->setSelected(opts->isRoadEnabled(r->getId()));
 		}
 	}
 }
@@ -407,18 +406,17 @@ void TemplatesDropBox::ListItem::hover(bool on)
 	redraw();
 }
 
-void TemplatesDropBox::ListItem::clickLeft(tribool down, bool previousState)
+void TemplatesDropBox::ListItem::clickPressed(const Point & cursorPosition)
 {
-	if(down && isHovered())
-	{
+	if(isHovered())
 		dropBox.setTemplate(item);
-	}
-	else 
-	{
-		dropBox.clickLeft(true, true);
-	}
 }
 
+void TemplatesDropBox::ListItem::clickReleased(const Point & cursorPosition)
+{
+	dropBox.clickPressed(cursorPosition);
+	dropBox.clickReleased(cursorPosition);
+}
 
 TemplatesDropBox::TemplatesDropBox(RandomMapTab & randomMapTab, int3 size):
 	InterfaceObjectConfigurable(LCLICK | HOVER),
@@ -472,9 +470,17 @@ void TemplatesDropBox::sliderMove(int slidPos)
 	redraw();
 }
 
-void TemplatesDropBox::clickLeft(tribool down, bool previousState)
+bool TemplatesDropBox::receiveEvent(const Point & position, int eventType) const
+{
+	if (eventType == LCLICK)
+		return true; // we want drop box to close when clicking outside drop box borders
+
+	return CIntObject::receiveEvent(position, eventType);
+}
+
+void TemplatesDropBox::clickPressed(const Point & cursorPosition)
 {
-	if (!pos.isInside(GH.getCursorPosition()))
+	if (!pos.isInside(cursorPosition))
 	{
 		assert(GH.windows().isTopWindow(this));
 		GH.windows().popWindows(1);

+ 4 - 2
client/lobby/RandomMapTab.h

@@ -62,7 +62,8 @@ class TemplatesDropBox : public InterfaceObjectConfigurable
 		void updateItem(int index, const CRmgTemplate * item = nullptr);
 		
 		void hover(bool on) override;
-		void clickLeft(tribool down, bool previousState) override;
+		void clickPressed(const Point & cursorPosition) override;
+		void clickReleased(const Point & cursorPosition) override;
 	};
 	
 	friend struct ListItem;
@@ -70,7 +71,8 @@ class TemplatesDropBox : public InterfaceObjectConfigurable
 public:
 	TemplatesDropBox(RandomMapTab & randomMapTab, int3 size);
 	
-	void clickLeft(tribool down, bool previousState) override;
+	bool receiveEvent(const Point & position, int eventType) const override;
+	void clickPressed(const Point & cursorPosition) override;
 	void setTemplate(const CRmgTemplate *);
 	
 private:

+ 15 - 15
client/lobby/SelectionTab.cpp

@@ -270,22 +270,20 @@ void SelectionTab::toggleMode()
 	redraw();
 }
 
-void SelectionTab::clickLeft(tribool down, bool previousState)
+void SelectionTab::clickReleased(const Point & cursorPosition)
 {
-	if(down)
-	{
-		int line = getLine();
+	int line = getLine();
 
-		if(line != -1)
-		{
-			select(line);
-		}
+	if(line != -1)
+	{
+		select(line);
+	}
 #ifdef VCMI_IOS
-		// focus input field if clicked inside it
-		else if(inputName && inputName->isActive() && inputNameRect.isInside(GH.getCursorPosition()))
-			inputName->giveFocus();
+	// focus input field if clicked inside it
+	else if(inputName && inputName->isActive() && inputNameRect.isInside(cursorPosition))
+		inputName->giveFocus();
 #endif
-	}
+
 }
 
 void SelectionTab::keyPressed(EShortcut key)
@@ -317,12 +315,12 @@ void SelectionTab::keyPressed(EShortcut key)
 	select((int)selectionPos - slider->getValue() + moveBy);
 }
 
-void SelectionTab::clickDouble()
+void SelectionTab::clickDouble(const Point & cursorPosition)
 {
 	if(getLine() != -1) //double clicked scenarios list
 	{
-		(static_cast<CLobbyScreen *>(parent))->buttonStart->clickLeft(true, false);
-		(static_cast<CLobbyScreen *>(parent))->buttonStart->clickLeft(false, true);
+		(static_cast<CLobbyScreen *>(parent))->buttonStart->clickPressed(cursorPosition);
+		(static_cast<CLobbyScreen *>(parent))->buttonStart->clickReleased(cursorPosition);
 	}
 }
 
@@ -419,7 +417,9 @@ void SelectionTab::select(int position)
 		auto filename = *CResourceHandler::get("local")->getResourceName(ResourceID(curItems[py]->fileURI, EResType::CLIENT_SAVEGAME));
 		inputName->setText(filename.stem().string());
 	}
+
 	updateListItems();
+	redraw();
 	if(callOnSelect)
 		callOnSelect(curItems[py]);
 }

+ 2 - 2
client/lobby/SelectionTab.h

@@ -65,9 +65,9 @@ public:
 	SelectionTab(ESelectionScreen Type);
 	void toggleMode();
 
-	void clickLeft(tribool down, bool previousState) override;
+	void clickReleased(const Point & cursorPosition) override;
 	void keyPressed(EShortcut key) override;
-	void clickDouble() override;
+	void clickDouble(const Point & cursorPosition) override;
 	bool receiveEvent(const Point & position, int eventType) const override;
 
 	void filter(int size, bool selectFirst = false); //0 - all

+ 3 - 6
client/mainmenu/CCampaignScreen.cpp

@@ -141,13 +141,10 @@ void CCampaignScreen::CCampaignButton::show(Canvas & to)
 	}
 }
 
-void CCampaignScreen::CCampaignButton::clickLeft(tribool down, bool previousState)
+void CCampaignScreen::CCampaignButton::clickReleased(const Point & cursorPosition)
 {
-	if(down)
-	{
-		CCS->videoh->close();
-		CMainMenu::openCampaignLobby(campFile);
-	}
+	CCS->videoh->close();
+	CMainMenu::openCampaignLobby(campFile);
 }
 
 void CCampaignScreen::CCampaignButton::hover(bool on)

+ 1 - 1
client/mainmenu/CCampaignScreen.h

@@ -40,7 +40,7 @@ private:
 		std::string video; // the resource name of the video
 		std::string hoverText;
 
-		void clickLeft(tribool down, bool previousState) override;
+		void clickReleased(const Point & cursorPosition) override;
 		void hover(bool on) override;
 
 	public:

+ 2 - 2
client/mainmenu/CPrologEpilogVideo.cpp

@@ -52,10 +52,10 @@ void CPrologEpilogVideo::show(Canvas & to)
 		text->showAll(to); // blit text over video, if needed
 
 	if(text->textSize.y + 100 < positionCounter / 5)
-		clickLeft(false, false);
+		clickPressed(GH.getCursorPosition());
 }
 
-void CPrologEpilogVideo::clickLeft(tribool down, bool previousState)
+void CPrologEpilogVideo::clickPressed(const Point & cursorPosition)
 {
 	close();
 	CCS->soundh->stopSound(voiceSoundHandle);

+ 1 - 1
client/mainmenu/CPrologEpilogVideo.h

@@ -26,6 +26,6 @@ class CPrologEpilogVideo : public CWindowObject
 public:
 	CPrologEpilogVideo(CampaignScenarioPrologEpilog _spe, std::function<void()> callback);
 
-	void clickLeft(tribool down, bool previousState) override;
+	void clickPressed(const Point & cursorPosition) override;
 	void show(Canvas & to) override;
 };

+ 2 - 2
client/mainmenu/CreditsScreen.cpp

@@ -43,10 +43,10 @@ void CreditsScreen::show(Canvas & to)
 
 	//end of credits, close this screen
 	if(credits->textSize.y + 600 < positionCounter / 2)
-		clickLeft(false, false);
+		clickPressed(GH.getCursorPosition());
 }
 
-void CreditsScreen::clickLeft(tribool down, bool previousState)
+void CreditsScreen::clickPressed(const Point & cursorPosition)
 {
 	CTabbedInt * menu = dynamic_cast<CTabbedInt *>(parent);
 	assert(menu);

+ 1 - 1
client/mainmenu/CreditsScreen.h

@@ -21,5 +21,5 @@ class CreditsScreen : public CIntObject
 public:
 	CreditsScreen(Rect rect);
 	void show(Canvas & to) override;
-	void clickLeft(tribool down, bool previousState) override;
+	void clickPressed(const Point & cursorPosition) override;
 };

+ 4 - 10
client/mapView/MapViewActions.cpp

@@ -41,23 +41,17 @@ void MapViewActions::setContext(const std::shared_ptr<IMapRendererContext> & con
 	this->context = context;
 }
 
-void MapViewActions::clickLeft(tribool down, bool previousState)
+void MapViewActions::clickPressed(const Point & cursorPosition)
 {
-	if(indeterminate(down))
-		return;
-
-	if(down == false)
-		return;
-
-	int3 tile = model->getTileAtPoint(GH.getCursorPosition() - pos.topLeft());
+	int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft());
 
 	if(context->isInMap(tile))
 		adventureInt->onTileLeftClicked(tile);
 }
 
-void MapViewActions::showPopupWindow()
+void MapViewActions::showPopupWindow(const Point & cursorPosition)
 {
-	int3 tile = model->getTileAtPoint(GH.getCursorPosition() - pos.topLeft());
+	int3 tile = model->getTileAtPoint(cursorPosition - pos.topLeft());
 
 	if(context->isInMap(tile))
 		adventureInt->onTileRightClicked(tile);

+ 2 - 2
client/mapView/MapViewActions.h

@@ -31,8 +31,8 @@ public:
 
 	void setContext(const std::shared_ptr<IMapRendererContext> & context);
 
-	void clickLeft(tribool down, bool previousState) override;
-	void showPopupWindow() override;
+	void clickPressed(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
 	void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override;
 	void gesturePinch(const Point & centerPosition, double lastUpdateFactor) override;
 	void hover(bool on) override;

+ 4 - 0
client/render/IScreenHandler.h

@@ -12,6 +12,7 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 class Point;
+class Rect;
 VCMI_LIB_NAMESPACE_END
 
 class IScreenHandler
@@ -33,4 +34,7 @@ public:
 
 	/// Returns <min, max> range of possible values for screen scaling percentage
 	virtual std::tuple<int, int> getSupportedScalingRange() const = 0;
+
+	/// Converts provided rect from logical coordinates into coordinates within window, accounting for scaling and viewport
+	virtual Rect convertLogicalPointsToWindow(const Rect & input) const = 0;
 };

+ 62 - 13
client/renderSDL/ScreenHandler.cpp

@@ -22,10 +22,14 @@
 #include "../lib/CAndroidVMHelper.h"
 #endif
 
+#ifdef VCMI_IOS
+#	include "ios/utils.h"
+#endif
+
 #include <SDL.h>
 
 // TODO: should be made into a private members of ScreenHandler
-SDL_Window * mainWindow = nullptr;
+static SDL_Window * mainWindow = nullptr;
 SDL_Renderer * mainRenderer = nullptr;
 SDL_Texture * screenTexture = nullptr;
 SDL_Surface * screen = nullptr; //main screen surface
@@ -42,28 +46,70 @@ std::tuple<int, int> ScreenHandler::getSupportedScalingRange() const
 	// arbitrary limit on *downscaling*. Allow some downscaling, if requested by user. Should be generally limited to 100+ for all but few devices
 	static const double minimalScaling = 50;
 
-	Point renderResolution = getPreferredRenderingResolution();
-	double maximalScalingWidth = 100.0 * renderResolution.x / minResolution.x;
-	double maximalScalingHeight = 100.0 * renderResolution.y / minResolution.y;
+	Point renderResolution = getActualRenderResolution();
+	double reservedAreaWidth = settings["video"]["reservedWidth"].Float();
+	Point availableResolution = Point(renderResolution.x * (1 - reservedAreaWidth), renderResolution.y);
+
+	double maximalScalingWidth = 100.0 * availableResolution.x / minResolution.x;
+	double maximalScalingHeight = 100.0 * availableResolution.y / minResolution.y;
 	double maximalScaling = std::min(maximalScalingWidth, maximalScalingHeight);
 
 	return { minimalScaling, maximalScaling };
 }
 
+Rect ScreenHandler::convertLogicalPointsToWindow(const Rect & input) const
+{
+	Rect result;
+
+	// FIXME: use SDL_RenderLogicalToWindow instead? Needs to be tested on ios
+
+	float scaleX, scaleY;
+	SDL_Rect viewport;
+	SDL_RenderGetScale(mainRenderer, &scaleX, &scaleY);
+	SDL_RenderGetViewport(mainRenderer, &viewport);
+
+#ifdef VCMI_IOS
+	// TODO ios: looks like SDL bug actually, try fixing there
+	const auto nativeScale = iOS_utils::screenScale();
+	scaleX /= nativeScale;
+	scaleY /= nativeScale;
+#endif
+
+	result.x = (viewport.x + input.x) * scaleX;
+	result.y = (viewport.y + input.y) * scaleY;
+	result.w = input.w * scaleX;
+	result.h = input.h * scaleY;
+
+	return result;
+}
+
 Point ScreenHandler::getPreferredLogicalResolution() const
 {
-	Point renderResolution = getPreferredRenderingResolution();
+	Point renderResolution = getActualRenderResolution();
+	double reservedAreaWidth = settings["video"]["reservedWidth"].Float();
+	Point availableResolution = Point(renderResolution.x * (1 - reservedAreaWidth), renderResolution.y);
+
 	auto [minimalScaling, maximalScaling] = getSupportedScalingRange();
 
 	int userScaling = settings["video"]["resolution"]["scaling"].Integer();
 	int scaling = std::clamp(userScaling, minimalScaling, maximalScaling);
 
-	Point logicalResolution = renderResolution * 100.0 / scaling;
+	Point logicalResolution = availableResolution * 100.0 / scaling;
 
 	return logicalResolution;
 }
 
-Point ScreenHandler::getPreferredRenderingResolution() const
+Point ScreenHandler::getActualRenderResolution() const
+{
+	assert(mainRenderer != nullptr);
+
+	Point result;
+	SDL_GetRendererOutputSize(mainRenderer, &result.x, &result.y);
+
+	return result;
+}
+
+Point ScreenHandler::getPreferredWindowResolution() const
 {
 	if (getPreferredWindowMode() == EWindowMode::FULLSCREEN_BORDERLESS_WINDOWED)
 	{
@@ -178,11 +224,14 @@ void ScreenHandler::updateWindowState()
 	{
 		case EWindowMode::FULLSCREEN_EXCLUSIVE:
 		{
+			// for some reason, VCMI fails to switch from FULLSCREEN_BORDERLESS_WINDOWED to FULLSCREEN_EXCLUSIVE directly
+			// Switch to windowed mode first to avoid this bug
+			SDL_SetWindowFullscreen(mainWindow, 0);
 			SDL_SetWindowFullscreen(mainWindow, SDL_WINDOW_FULLSCREEN);
 
 			SDL_DisplayMode mode;
 			SDL_GetDesktopDisplayMode(displayIndex, &mode);
-			Point resolution = getPreferredRenderingResolution();
+			Point resolution = getPreferredWindowResolution();
 
 			mode.w = resolution.x;
 			mode.h = resolution.y;
@@ -200,7 +249,7 @@ void ScreenHandler::updateWindowState()
 		}
 		case EWindowMode::WINDOWED:
 		{
-			Point resolution = getPreferredRenderingResolution();
+			Point resolution = getPreferredWindowResolution();
 			SDL_SetWindowFullscreen(mainWindow, 0);
 			SDL_SetWindowSize(mainWindow, resolution.x, resolution.y);
 			SDL_SetWindowPosition(mainWindow, SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex), SDL_WINDOWPOS_CENTERED_DISPLAY(displayIndex));
@@ -290,7 +339,7 @@ SDL_Window * ScreenHandler::createWindowImpl(Point dimensions, int flags, bool c
 SDL_Window * ScreenHandler::createWindow()
 {
 #ifndef VCMI_MOBILE
-	Point dimensions = getPreferredRenderingResolution();
+	Point dimensions = getPreferredWindowResolution();
 
 	switch(getPreferredWindowMode())
 	{
@@ -350,7 +399,7 @@ void ScreenHandler::validateSettings()
 	{
 		//we only check that our desired window size fits on screen
 		int displayIndex = getPreferredDisplayIndex();
-		Point resolution = getPreferredRenderingResolution();
+		Point resolution = getPreferredWindowResolution();
 
 		SDL_DisplayMode mode;
 
@@ -368,7 +417,7 @@ void ScreenHandler::validateSettings()
 	if (getPreferredWindowMode() == EWindowMode::FULLSCREEN_EXCLUSIVE)
 	{
 		auto legalOptions = getSupportedResolutions();
-		Point selectedResolution = getPreferredRenderingResolution();
+		Point selectedResolution = getPreferredWindowResolution();
 
 		if(!vstd::contains(legalOptions, selectedResolution))
 		{
@@ -476,7 +525,7 @@ void ScreenHandler::clearScreen()
 
 std::vector<Point> ScreenHandler::getSupportedResolutions() const
 {
-	int displayID = SDL_GetWindowDisplayIndex(mainWindow);
+	int displayID = getPreferredDisplayIndex();
 	return getSupportedResolutions(displayID);
 }
 

+ 6 - 2
client/renderSDL/ScreenHandler.h

@@ -30,14 +30,17 @@ enum class EWindowMode
 };
 
 /// This class is responsible for management of game window and its main rendering surface
-class ScreenHandler : public IScreenHandler
+class ScreenHandler final : public IScreenHandler
 {
 	/// Dimensions of target surfaces/textures, this value is what game logic views as screen size
 	Point getPreferredLogicalResolution() const;
 
 	/// Dimensions of output window, if different from logicalResolution SDL will perform scaling
 	/// This value is what player views as window size
-	Point getPreferredRenderingResolution() const;
+	Point getPreferredWindowResolution() const;
+
+	/// Dimensions of render output, usually same as window size except for high-DPI screens on macOS / iOS
+	Point getActualRenderResolution() const;
 
 	EWindowMode getPreferredWindowMode() const;
 
@@ -86,4 +89,5 @@ public:
 	std::vector<Point> getSupportedResolutions() const final;
 	std::vector<Point> getSupportedResolutions(int displayIndex) const;
 	std::tuple<int, int> getSupportedScalingRange() const final;
+	Rect convertLogicalPointsToWindow(const Rect & input) const final;
 };

+ 68 - 35
client/widgets/Buttons.cpp

@@ -167,39 +167,48 @@ void CButton::onButtonClicked()
 	callback();
 }
 
-void CButton::clickLeft(tribool down, bool previousState)
+void CButton::clickPressed(const Point & cursorPosition)
 {
 	if(isBlocked())
 		return;
 
-	if (down)
+	if (getState() != PRESSED)
 	{
-		if (getState() != PRESSED)
-		{
-			if (!soundDisabled)
-				CCS->soundh->playSound(soundBase::button);
-			setState(PRESSED);
-
-			if (actOnDown)
-				onButtonClicked();
-		}
+		if (!soundDisabled)
+			CCS->soundh->playSound(soundBase::button);
+		setState(PRESSED);
+
+		if (actOnDown)
+			onButtonClicked();
 	}
-	else
+}
+
+void CButton::clickReleased(const Point & cursorPosition)
+{
+	if (getState() == PRESSED)
 	{
-		if (getState() == PRESSED)
-		{
-			if(hoverable && isHovered())
-				setState(HIGHLIGHTED);
-			else
-				setState(NORMAL);
-
-			if (!actOnDown && previousState && (down == false))
-				onButtonClicked();
-		}
+		if(hoverable && isHovered())
+			setState(HIGHLIGHTED);
+		else
+			setState(NORMAL);
+
+		if (!actOnDown)
+			onButtonClicked();
 	}
 }
 
-void CButton::showPopupWindow()
+void CButton::clickCancel(const Point & cursorPosition)
+{
+	if (getState() == PRESSED)
+	{
+		if(hoverable && isHovered())
+			setState(HIGHLIGHTED);
+		else
+			setState(NORMAL);
+	}
+}
+
+void CButton::showPopupWindow(const Point & cursorPosition)
 {
 	if(helpBox.size()) //there is no point to show window with nothing inside...
 		CRClickPopup::createAndPush(helpBox);
@@ -331,11 +340,16 @@ void CToggleBase::setEnabled(bool enabled)
 	// for overrides
 }
 
-void CToggleBase::setSelected(bool on)
+void CToggleBase::setSelectedSilent(bool on)
 {
-	bool changed = (on != selected);
 	selected = on;
 	doSelect(on);
+}
+
+void CToggleBase::setSelected(bool on)
+{
+	bool changed = (on != selected);
+	setSelectedSilent(on);
 	if (changed)
 		callback(on);
 }
@@ -377,7 +391,7 @@ void CToggleButton::setEnabled(bool enabled)
 	setState(enabled ? NORMAL : BLOCKED);
 }
 
-void CToggleButton::clickLeft(tribool down, bool previousState)
+void CToggleButton::clickPressed(const Point & cursorPosition)
 {
 	// force refresh
 	hover(false);
@@ -386,22 +400,41 @@ void CToggleButton::clickLeft(tribool down, bool previousState)
 	if(isBlocked())
 		return;
 
-	if (down && canActivate())
+	if (canActivate())
 	{
 		CCS->soundh->playSound(soundBase::button);
 		setState(PRESSED);
 	}
+}
+
+void CToggleButton::clickReleased(const Point & cursorPosition)
+{
+	// force refresh
+	hover(false);
+	hover(true);
+
+	if(isBlocked())
+		return;
 
-	if(previousState)//mouse up
+	if (getState() == PRESSED && canActivate())
 	{
-		if(down == false && getState() == PRESSED && canActivate())
-		{
-			onButtonClicked();
-			setSelected(!selected);
-		}
-		else
-			doSelect(selected); // restore
+		onButtonClicked();
+		setSelected(!selected);
 	}
+	else
+		doSelect(selected); // restore
+}
+
+void CToggleButton::clickCancel(const Point & cursorPosition)
+{
+	// force refresh
+	hover(false);
+	hover(true);
+
+	if(isBlocked())
+		return;
+
+	doSelect(selected);
 }
 
 void CToggleGroup::addCallback(std::function<void(int)> callback)

+ 11 - 3
client/widgets/Buttons.h

@@ -102,8 +102,10 @@ public:
 	void setPlayerColor(PlayerColor player);
 
 	/// CIntObject overrides
-	void showPopupWindow() override;
-	void clickLeft(tribool down, bool previousState) override;
+	void showPopupWindow(const Point & cursorPosition) override;
+	void clickPressed(const Point & cursorPosition) override;
+	void clickReleased(const Point & cursorPosition) override;
+	void clickCancel(const Point & cursorPosition) override;
 	void hover (bool on) override;
 	void showAll(Canvas & to) override;
 
@@ -136,6 +138,9 @@ public:
 	/// Changes selection to "on", and calls callback
 	void setSelected(bool on);
 
+	/// Changes selection to "on" without calling callback
+	void setSelectedSilent(bool on);
+
 	void addCallback(std::function<void(bool)> callback);
 
 	/// Set whether the toggle is currently enabled for user to use, this is only inplemented in ToggleButton, not for other toggles yet.
@@ -151,7 +156,10 @@ class CToggleButton : public CButton, public CToggleBase
 public:
 	CToggleButton(Point position, const std::string &defName, const std::pair<std::string, std::string> &help,
 				  CFunctionList<void(bool)> Callback = 0, EShortcut key = {}, bool playerColoredButton = false );
-	void clickLeft(tribool down, bool previousState) override;
+
+	void clickPressed(const Point & cursorPosition) override;
+	void clickReleased(const Point & cursorPosition) override;
+	void clickCancel(const Point & cursorPosition) override;
 
 	// bring overrides into scope
 	//using CButton::addCallback;

+ 10 - 18
client/widgets/CArtifactHolder.cpp

@@ -67,11 +67,6 @@ CArtPlace::CArtPlace(Point position, const CArtifactInstance * Art)
 	pos.w = pos.h = 44;
 }
 
-void CArtPlace::clickLeft(tribool down, bool previousState)
-{
-	LRClickableAreaWTextComp::clickLeft(down, previousState);
-}
-
 const CArtifactInstance * CArtPlace::getArt()
 {
 	return ourArt;
@@ -121,16 +116,16 @@ void CCommanderArtPlace::returnArtToHeroCallback()
 	}
 }
 
-void CCommanderArtPlace::clickLeft(tribool down, bool previousState)
+void CCommanderArtPlace::clickPressed(const Point & cursorPosition)
 {
-	if(ourArt && text.size() && down)
+	if(ourArt && text.size())
 		LOCPLINT->showYesNoDialog(CGI->generaltexth->translate("vcmi.commanderWindow.artifactMessage"), [this]() { returnArtToHeroCallback(); }, []() {});
 }
 
-void CCommanderArtPlace::showPopupWindow()
+void CCommanderArtPlace::showPopupWindow(const Point & cursorPosition)
 {
 	if(ourArt && text.size())
-		CArtPlace::showPopupWindow();
+		CArtPlace::showPopupWindow(cursorPosition);
 }
 
 void CCommanderArtPlace::setArtifact(const CArtifactInstance * art)
@@ -183,16 +178,13 @@ bool CHeroArtPlace::isMarked() const
 	return marked;
 }
 
-void CHeroArtPlace::clickLeft(tribool down, bool previousState)
+void CHeroArtPlace::clickPressed(const Point & cursorPosition)
 {
-	if(down || !previousState)
-		return;
-
 	if(leftClickCallback)
 		leftClickCallback(*this);
 }
 
-void CHeroArtPlace::showPopupWindow()
+void CHeroArtPlace::showPopupWindow(const Point & cursorPosition)
 {
 	if(rightClickCallback)
 		rightClickCallback(*this);
@@ -236,11 +228,11 @@ void CHeroArtPlace::addCombinedArtInfo(std::map<const CArtifact*, int> & arts)
 		text += "{" + combinedArt.first->getNameTranslated() + "}";
 		if(arts.size() == 1)
 		{
-			for(const auto part : *combinedArt.first->constituents)
+			for(const auto part : combinedArt.first->getConstituents())
 				artList += "\n" + part->getNameTranslated();
 		}
 		text += " (" + boost::str(boost::format("%d") % combinedArt.second) + " / " +
-			boost::str(boost::format("%d") % combinedArt.first->constituents->size()) + ")" + artList;
+			boost::str(boost::format("%d") % combinedArt.first->getConstituents().size()) + ")" + artList;
 	}
 }
 
@@ -290,9 +282,9 @@ bool ArtifactUtilsClient::askToDisassemble(const CGHeroInstance * hero, const Ar
 	const auto art = hero->getArt(slot);
 	assert(art);
 
-	if(art->canBeDisassembled())
+	if(art->isCombined())
 	{
-		if(ArtifactUtils::isSlotBackpack(slot) && !ArtifactUtils::isBackpackFreeSlots(hero, art->artType->constituents->size() - 1))
+		if(ArtifactUtils::isSlotBackpack(slot) && !ArtifactUtils::isBackpackFreeSlots(hero, art->artType->getConstituents().size() - 1))
 			return false;
 
 		LOCPLINT->showArtifactAssemblyDialog(

+ 4 - 5
client/widgets/CArtifactHolder.h

@@ -41,7 +41,6 @@ protected:
 
 public:
 	CArtPlace(Point position, const CArtifactInstance * Art = nullptr);
-	void clickLeft(tribool down, bool previousState) override;
 	const CArtifactInstance * getArt();
 
 	virtual void setArtifact(const CArtifactInstance * art)=0;
@@ -58,8 +57,8 @@ protected:
 
 public:
 	CCommanderArtPlace(Point position, const CGHeroInstance * commanderOwner, ArtifactPosition artSlot, const CArtifactInstance * Art = nullptr);
-	void clickLeft(tribool down, bool previousState) override;
-	void showPopupWindow() override;
+	void clickPressed(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
 	void setArtifact(const CArtifactInstance * art) override;
 };
 
@@ -77,8 +76,8 @@ public:
 	bool isLocked();
 	void selectSlot(bool on);
 	bool isMarked() const;
-	void clickLeft(tribool down, bool previousState) override;
-	void showPopupWindow() override;
+	void clickPressed(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
 	void showAll(Canvas & to) override;
 	void setArtifact(const CArtifactInstance * art) override;
 	void addCombinedArtInfo(std::map<const CArtifact*, int> & arts);

+ 3 - 6
client/widgets/CArtifactsOfHeroAltar.cpp

@@ -98,13 +98,10 @@ void CArtifactsOfHeroAltar::deleteFromVisible(const CArtifactInstance * artInst)
 	}
 	else
 	{
-		if(artInst->canBeDisassembled())
+		for(const auto & part : artInst->getPartsInfo())
 		{
-			for(const auto & part : dynamic_cast<const CCombinedArtifactInstance*>(artInst)->constituentsInfo)
-			{
-				if(part.slot != ArtifactPosition::PRE_FIRST)
-					getArtPlace(part.slot)->setArtifact(nullptr);
-			}
+			if(part.slot != ArtifactPosition::PRE_FIRST)
+				getArtPlace(part.slot)->setArtifact(nullptr);
 		}
 	}
 }

+ 3 - 3
client/widgets/CArtifactsOfHeroBase.cpp

@@ -257,14 +257,14 @@ void CArtifactsOfHeroBase::setSlotData(ArtPlacePtr artPlace, const ArtifactPosit
 	{
 		artPlace->lockSlot(slotInfo->locked);
 		artPlace->setArtifact(slotInfo->artifact);
-		if(!slotInfo->artifact->canBeDisassembled())
+		if(!slotInfo->artifact->isCombined())
 		{
 			// If the artifact is part of at least one combined artifact, add additional information
 			std::map<const CArtifact*, int> arts;
-			for(const auto combinedArt : slotInfo->artifact->artType->constituentOf)
+			for(const auto combinedArt : slotInfo->artifact->artType->getPartOf())
 			{
 				arts.insert(std::pair(combinedArt, 0));
-				for(const auto part : *combinedArt->constituents)
+				for(const auto part : combinedArt->getConstituents())
 					if(artSet.hasArt(part->getId(), true))
 						arts.at(combinedArt)++;
 			}

+ 9 - 15
client/widgets/CComponent.cpp

@@ -34,6 +34,7 @@
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/NetPacksBase.h"
 #include "../../lib/CArtHandler.h"
+#include "../../lib/CArtifactInstance.h"
 
 CComponent::CComponent(Etype Type, int Subtype, int Val, ESize imageSize, EFonts font):
 	perDay(false)
@@ -169,16 +170,12 @@ std::string CComponent::getDescription()
 	case artifact:
 	{
 		auto artID = ArtifactID(subtype);
-		std::unique_ptr<CArtifactInstance> art;
-		if (artID != ArtifactID::SPELL_SCROLL)
+		auto description = VLC->arth->objects[artID]->getDescriptionTranslated();
+		if(artID == ArtifactID::SPELL_SCROLL)
 		{
-			art.reset(ArtifactUtils::createNewArtifactInstance(artID));
+			ArtifactUtils::insertScrrollSpellName(description, SpellID(val));
 		}
-		else
-		{
-			art.reset(ArtifactUtils::createScroll(SpellID(val)));
-		}
-		return art->getDescription();
+		return description;
 	}
 	case experience: return CGI->generaltexth->allTexts[241];
 	case spell:      return (*CGI->spellh)[subtype]->getDescriptionTranslated(val);
@@ -258,19 +255,16 @@ void CComponent::setSurface(std::string defName, int imgPos)
 	image = std::make_shared<CAnimImage>(defName, imgPos);
 }
 
-void CComponent::showPopupWindow()
+void CComponent::showPopupWindow(const Point & cursorPosition)
 {
 	if(!getDescription().empty())
 		CRClickPopup::createAndPush(getDescription());
 }
 
-void CSelectableComponent::clickLeft(tribool down, bool previousState)
+void CSelectableComponent::clickPressed(const Point & cursorPosition)
 {
-	if (down)
-	{
-		if(onSelect)
-			onSelect();
-	}
+	if(onSelect)
+		onSelect();
 }
 
 void CSelectableComponent::init()

+ 2 - 2
client/widgets/CComponent.h

@@ -65,7 +65,7 @@ public:
 	CComponent(Etype Type, int Subtype, int Val = 0, ESize imageSize=large, EFonts font = FONT_SMALL);
 	CComponent(const Component &c, ESize imageSize=large, EFonts font = FONT_SMALL);
 
-	void showPopupWindow() override; //call-in
+	void showPopupWindow(const Point & cursorPosition) override; //call-in
 };
 
 /// component that can be selected or deselected
@@ -79,7 +79,7 @@ public:
 	void showAll(Canvas & to) override;
 	void select(bool on);
 
-	void clickLeft(tribool down, bool previousState) override; //call-in
+	void clickPressed(const Point & cursorPosition) override; //call-in
 	CSelectableComponent(Etype Type, int Sub, int Val, ESize imageSize=large, std::function<void()> OnSelect = nullptr);
 	CSelectableComponent(const Component & c, std::function<void()> OnSelect = nullptr);
 };

+ 2 - 5
client/widgets/CGarrisonInt.cpp

@@ -285,7 +285,7 @@ bool CGarrisonSlot::mustForceReselection() const
 	return false;
 }
 
-void CGarrisonSlot::showPopupWindow()
+void CGarrisonSlot::showPopupWindow(const Point & cursorPosition)
 {
 	if(creature)
 	{
@@ -293,10 +293,8 @@ void CGarrisonSlot::showPopupWindow()
 	}
 }
 
-void CGarrisonSlot::clickLeft(tribool down, bool previousState)
+void CGarrisonSlot::clickPressed(const Point & cursorPosition)
 {
-	if(down)
-	{
 		bool refr = false;
 		const CGarrisonSlot * selection = owner->getSelection();
 
@@ -349,7 +347,6 @@ void CGarrisonSlot::clickLeft(tribool down, bool previousState)
 			hover(false);
 			hover(true);
 		}
-	}
 }
 
 void CGarrisonSlot::update()

+ 2 - 2
client/widgets/CGarrisonInt.h

@@ -58,8 +58,8 @@ public:
 	bool our() const;
 	SlotID getSlot() const { return ID; }
 	bool ally() const;
-	void showPopupWindow() override;
-	void clickLeft(tribool down, bool previousState) override;
+	void showPopupWindow(const Point & cursorPosition) override;
+	void clickPressed(const Point & cursorPosition) override;
 	void update();
 	CGarrisonSlot(CGarrisonInt *Owner, int x, int y, SlotID IID, EGarrisonType Upg=EGarrisonType::UP, const CStackInstance * creature_ = nullptr);
 

+ 2 - 2
client/widgets/CWindowWithArtifacts.cpp

@@ -200,7 +200,7 @@ void CWindowWithArtifacts::rightClickArtPlaceHero(CArtifactsOfHeroBase & artsIns
 						return;
 					}
 					if(artPlace.text.size())
-						artPlace.LRClickableAreaWTextComp::showPopupWindow();
+						artPlace.LRClickableAreaWTextComp::showPopupWindow(GH.getCursorPosition());
 				}
 			}
 			// Altar window, Market window right click handler
@@ -209,7 +209,7 @@ void CWindowWithArtifacts::rightClickArtPlaceHero(CArtifactsOfHeroBase & artsIns
 				std::is_same_v<decltype(artSetWeak), std::weak_ptr<CArtifactsOfHeroMarket>>)
 			{
 				if(artPlace.getArt() && artPlace.text.size())
-					artPlace.LRClickableAreaWTextComp::showPopupWindow();
+					artPlace.LRClickableAreaWTextComp::showPopupWindow(GH.getCursorPosition());
 			}
 		}, artSetWeak.value());
 }

+ 12 - 17
client/widgets/MiscWidgets.cpp

@@ -50,14 +50,12 @@ CHoverableArea::~CHoverableArea()
 {
 }
 
-void LRClickableAreaWText::clickLeft(tribool down, bool previousState)
+void LRClickableAreaWText::clickPressed(const Point & cursorPosition)
 {
-	if(!down && previousState && !text.empty())
-	{
+	if(!text.empty())
 		LOCPLINT->showInfoDialog(text);
-	}
 }
-void LRClickableAreaWText::showPopupWindow()
+void LRClickableAreaWText::showPopupWindow(const Point & cursorPosition)
 {
 	if (!text.empty())
 		CRClickPopup::createAndPush(text);
@@ -85,13 +83,10 @@ void LRClickableAreaWText::init()
 	addUsedEvents(LCLICK | SHOW_POPUP | HOVER);
 }
 
-void LRClickableAreaWTextComp::clickLeft(tribool down, bool previousState)
+void LRClickableAreaWTextComp::clickPressed(const Point & cursorPosition)
 {
-	if((!down) && previousState)
-	{
-		std::vector<std::shared_ptr<CComponent>> comp(1, createComponent());
-		LOCPLINT->showInfoDialog(text, comp);
-	}
+	std::vector<std::shared_ptr<CComponent>> comp(1, createComponent());
+	LOCPLINT->showInfoDialog(text, comp);
 }
 
 LRClickableAreaWTextComp::LRClickableAreaWTextComp(const Rect &Pos, int BaseType)
@@ -108,7 +103,7 @@ std::shared_ptr<CComponent> LRClickableAreaWTextComp::createComponent() const
 		return std::shared_ptr<CComponent>();
 }
 
-void LRClickableAreaWTextComp::showPopupWindow()
+void LRClickableAreaWTextComp::showPopupWindow(const Point & cursorPosition)
 {
 	if(auto comp = createComponent())
 	{
@@ -116,7 +111,7 @@ void LRClickableAreaWTextComp::showPopupWindow()
 		return;
 	}
 
-	LRClickableAreaWText::showPopupWindow(); //only if with-component variant not occurred
+	LRClickableAreaWText::showPopupWindow(cursorPosition); //only if with-component variant not occurred
 }
 
 CHeroArea::CHeroArea(int x, int y, const CGHeroInstance * _hero)
@@ -134,9 +129,9 @@ CHeroArea::CHeroArea(int x, int y, const CGHeroInstance * _hero)
 		portrait = std::make_shared<CAnimImage>("PortraitsLarge", hero->portrait);
 }
 
-void CHeroArea::clickLeft(tribool down, bool previousState)
+void CHeroArea::clickPressed(const Point & cursorPosition)
 {
-	if(hero && (!down) && previousState)
+	if(hero)
 		LOCPLINT->openHeroWindow(hero);
 }
 
@@ -148,9 +143,9 @@ void CHeroArea::hover(bool on)
 		GH.statusbar()->clear();
 }
 
-void LRClickableAreaOpenTown::clickLeft(tribool down, bool previousState)
+void LRClickableAreaOpenTown::clickPressed(const Point & cursorPosition)
 {
-	if(town && (!down) && previousState)
+	if(town)
 	{
 		LOCPLINT->openTownWindow(town);
 		if ( type == 2 )

+ 6 - 6
client/widgets/MiscWidgets.h

@@ -48,8 +48,8 @@ public:
 	virtual ~LRClickableAreaWText();
 	void init();
 
-	virtual void clickLeft(tribool down, bool previousState) override;
-	virtual void showPopupWindow() override;
+	void clickPressed(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
 };
 
 /// base class for hero/town/garrison tooltips
@@ -135,7 +135,7 @@ class CHeroArea: public CIntObject
 public:
 	CHeroArea(int x, int y, const CGHeroInstance * _hero);
 
-	void clickLeft(tribool down, bool previousState) override;
+	void clickPressed(const Point & cursorPosition) override;
 	void hover(bool on) override;
 };
 
@@ -146,8 +146,8 @@ public:
 	int type;
 	int baseType;
 	int bonusValue;
-	virtual void clickLeft(tribool down, bool previousState) override;
-	virtual void showPopupWindow() override;
+	void clickPressed(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
 
 	LRClickableAreaWTextComp(const Rect &Pos = Rect(0,0,0,0), int BaseType = -1);
 	std::shared_ptr<CComponent> createComponent() const;
@@ -158,7 +158,7 @@ class LRClickableAreaOpenTown: public LRClickableAreaWTextComp
 {
 public:
 	const CGTownInstance * town;
-	void clickLeft(tribool down, bool previousState) override;
+	void clickPressed(const Point & cursorPosition) override;
 	LRClickableAreaOpenTown(const Rect & Pos, const CGTownInstance * Town);
 };
 

+ 5 - 5
client/widgets/Slider.cpp

@@ -121,20 +121,20 @@ void CSlider::scrollTo(int to)
 	moved(to);
 }
 
-void CSlider::clickLeft(tribool down, bool previousState)
+void CSlider::clickPressed(const Point & cursorPosition)
 {
-	if(down && !slider->isBlocked())
+	if(!slider->isBlocked())
 	{
 		double pw = 0;
 		double rw = 0;
 		if(getOrientation() == Orientation::HORIZONTAL)
 		{
-			pw = GH.getCursorPosition().x-pos.x-25;
+			pw = cursorPosition.x-pos.x-25;
 			rw = pw / static_cast<double>(pos.w - 48);
 		}
 		else
 		{
-			pw = GH.getCursorPosition().y-pos.y-24;
+			pw = cursorPosition.y-pos.y-24;
 			rw = pw / (pos.h-48);
 		}
 
@@ -142,7 +142,7 @@ void CSlider::clickLeft(tribool down, bool previousState)
 		if (!vstd::iswithin(rw, 0, 1))
 			return;
 
-		slider->clickLeft(true, slider->isMouseLeftButtonPressed());
+		slider->clickPressed(cursorPosition);
 		scrollTo((int)(rw * positions  +  0.5));
 		return;
 	}

+ 1 - 1
client/widgets/Slider.h

@@ -69,7 +69,7 @@ public:
 
 	bool receiveEvent(const Point & position, int eventType) const override;
 	void keyPressed(EShortcut key) override;
-	void clickLeft(tribool down, bool previousState) override;
+	void clickPressed(const Point & cursorPosition) override;
 	void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override;
 	void gesturePanning(const Point & initialPosition, const Point & currentPosition, const Point & lastUpdateDistance) override;
 	void showAll(Canvas & to) override;

+ 5 - 8
client/widgets/TextControls.cpp

@@ -446,13 +446,10 @@ void CGStatusBar::show(Canvas & to)
 	showAll(to);
 }
 
-void CGStatusBar::clickLeft(tribool down, bool previousState)
+void CGStatusBar::clickPressed(const Point & cursorPosition)
 {
-	if(!down)
-	{
-		if(LOCPLINT && LOCPLINT->cingconsole->isActive())
-			LOCPLINT->cingconsole->startEnteringText();
-	}
+	if(LOCPLINT && LOCPLINT->cingconsole->isActive())
+		LOCPLINT->cingconsole->startEnteringText();
 }
 
 void CGStatusBar::activate()
@@ -561,9 +558,9 @@ std::string CTextInput::visibleText()
 	return focus ? text + newText + "_" : text;
 }
 
-void CTextInput::clickLeft(tribool down, bool previousState)
+void CTextInput::clickPressed(const Point & cursorPosition)
 {
-	if(down && !focus)
+	if(!focus)
 		giveFocus();
 }
 

+ 2 - 2
client/widgets/TextControls.h

@@ -138,7 +138,7 @@ class CGStatusBar : public CLabel, public std::enable_shared_from_this<CGStatusB
 protected:
 	Point getBorderSize() override;
 
-	void clickLeft(tribool down, bool previousState) override;
+	void clickPressed(const Point & cursorPosition) override;
 
 public:
 	~CGStatusBar();
@@ -224,7 +224,7 @@ public:
 	CTextInput(const Rect & Pos, const Point & bgOffset, const std::string & bgName, const CFunctionList<void(const std::string &)> & CB);
 	CTextInput(const Rect & Pos, std::shared_ptr<IImage> srf);
 
-	void clickLeft(tribool down, bool previousState) override;
+	void clickPressed(const Point & cursorPosition) override;
 	void keyPressed(EShortcut key) override;
 
 	//bool captureThisKey(EShortcut key) override;

+ 42 - 50
client/windows/CCastleInterface.cpp

@@ -134,16 +134,16 @@ void CBuildingRect::hover(bool on)
 	}
 }
 
-void CBuildingRect::clickLeft(tribool down, bool previousState)
+void CBuildingRect::clickPressed(const Point & cursorPosition)
 {
-	if(previousState && getBuilding() && area && !down && (parent->selectedBuilding==this))
+	if(getBuilding() && area && (parent->selectedBuilding==this))
 	{
 		auto building = getBuilding();
 		parent->buildingClicked(building->bid, building->subId, building->upgrade);
 	}
 }
 
-void CBuildingRect::showPopupWindow()
+void CBuildingRect::showPopupWindow(const Point & cursorPosition)
 {
 	if((!area) || (this!=parent->selectedBuilding) || getBuilding() == nullptr)
 		return;
@@ -377,37 +377,35 @@ void CHeroGSlot::hover(bool on)
 		GH.statusbar()->write(temp);
 }
 
-void CHeroGSlot::clickLeft(tribool down, bool previousState)
+void CHeroGSlot::clickPressed(const Point & cursorPosition)
 {
 	std::shared_ptr<CHeroGSlot> other = upg ? owner->garrisonedHero : owner->visitingHero;
-	if(!down)
-	{
-		owner->garr->setSplittingMode(false);
-		owner->garr->selectSlot(nullptr);
 
-		if(hero && isSelected())
-		{
-			setHighlight(false);
-			LOCPLINT->openHeroWindow(hero);
-		}
-		else if(other->hero && other->isSelected())
-		{
-			owner->swapArmies();
-		}
-		else if(hero)
-		{
-			setHighlight(true);
-			owner->garr->selectSlot(nullptr);
-			redraw();
-		}
+	owner->garr->setSplittingMode(false);
+	owner->garr->selectSlot(nullptr);
 
-		//refresh statusbar
-		hover(false);
-		hover(true);
+	if(hero && isSelected())
+	{
+		setHighlight(false);
+		LOCPLINT->openHeroWindow(hero);
+	}
+	else if(other->hero && other->isSelected())
+	{
+		owner->swapArmies();
 	}
+	else if(hero)
+	{
+		setHighlight(true);
+		owner->garr->selectSlot(nullptr);
+		redraw();
+	}
+
+	//refresh statusbar
+	hover(false);
+	hover(true);
 }
 
-void CHeroGSlot::showPopupWindow()
+void CHeroGSlot::showPopupWindow(const Point & cursorPosition)
 {
 	if(hero)
 	{
@@ -800,7 +798,7 @@ void CCastleBuildings::enterBlacksmith(ArtifactID artifactID)
 	bool possible = LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= price;
 	if(possible)
 	{
-		for(auto slot : art->possibleSlots.at(ArtBearer::HERO))
+		for(auto slot : art->getPossibleSlots().at(ArtBearer::HERO))
 		{
 			if(hero->getArt(slot) == nullptr)
 			{
@@ -1057,17 +1055,14 @@ void CCreaInfo::hover(bool on)
 	}
 }
 
-void CCreaInfo::clickLeft(tribool down, bool previousState)
+void CCreaInfo::clickPressed(const Point & cursorPosition)
 {
-	if(previousState && (!down))
+	int offset = LOCPLINT->castleInt? (-87) : 0;
+	auto recruitCb = [=](CreatureID id, int count)
 	{
-		int offset = LOCPLINT->castleInt? (-87) : 0;
-		auto recruitCb = [=](CreatureID id, int count)
-		{
-			LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level);
-		};
-		GH.windows().createAndPushWindow<CRecruitmentWindow>(town, level, town, recruitCb, offset);
-	}
+		LOCPLINT->cb->recruitCreatures(town, town->getUpperArmy(), id, count, level);
+	};
+	GH.windows().createAndPushWindow<CRecruitmentWindow>(town, level, town, recruitCb, offset);
 }
 
 std::string CCreaInfo::genGrowthText()
@@ -1081,7 +1076,7 @@ std::string CCreaInfo::genGrowthText()
 	return descr;
 }
 
-void CCreaInfo::showPopupWindow()
+void CCreaInfo::showPopupWindow(const Point & cursorPosition)
 {
 	if (showAvailable)
 		GH.windows().createAndPushWindow<CDwellingInfoBox>(GH.screenDimensions().x / 2, GH.screenDimensions().y / 2, town, level);
@@ -1133,7 +1128,7 @@ void CTownInfo::hover(bool on)
 	}
 }
 
-void CTownInfo::showPopupWindow()
+void CTownInfo::showPopupWindow(const Point & cursorPosition)
 {
 	if(building)
 	{
@@ -1379,13 +1374,12 @@ void CHallInterface::CBuildingBox::hover(bool on)
 	}
 }
 
-void CHallInterface::CBuildingBox::clickLeft(tribool down, bool previousState)
+void CHallInterface::CBuildingBox::clickPressed(const Point & cursorPosition)
 {
-	if(previousState && (!down))
-		GH.windows().createAndPushWindow<CBuildWindow>(town,building,state,0);
+	GH.windows().createAndPushWindow<CBuildWindow>(town,building,state,0);
 }
 
-void CHallInterface::CBuildingBox::showPopupWindow()
+void CHallInterface::CBuildingBox::showPopupWindow(const Point & cursorPosition)
 {
 	GH.windows().createAndPushWindow<CBuildWindow>(town,building,state,1);
 }
@@ -1740,10 +1734,9 @@ void CFortScreen::RecruitArea::creaturesChangedEventHandler()
 	}
 }
 
-void CFortScreen::RecruitArea::clickLeft(tribool down, bool previousState)
+void CFortScreen::RecruitArea::clickPressed(const Point & cursorPosition)
 {
-	if(!down && previousState)
-		LOCPLINT->castleInt->builds->enterDwelling(level);
+	LOCPLINT->castleInt->builds->enterDwelling(level);
 }
 
 CMageGuildScreen::CMageGuildScreen(CCastleInterface * owner,std::string imagem)
@@ -1796,13 +1789,12 @@ CMageGuildScreen::Scroll::Scroll(Point position, const CSpell *Spell)
 	pos = image->pos;
 }
 
-void CMageGuildScreen::Scroll::clickLeft(tribool down, bool previousState)
+void CMageGuildScreen::Scroll::clickPressed(const Point & cursorPosition)
 {
-	if(down)
-		LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared<CComponent>(CComponent::spell, spell->id));
+	LOCPLINT->showInfoDialog(spell->getDescriptionTranslated(0), std::make_shared<CComponent>(CComponent::spell, spell->id));
 }
 
-void CMageGuildScreen::Scroll::showPopupWindow()
+void CMageGuildScreen::Scroll::showPopupWindow(const Point & cursorPosition)
 {
 	CRClickPopup::createAndPush(spell->getDescriptionTranslated(0), std::make_shared<CComponent>(CComponent::spell, spell->id));
 }

+ 12 - 12
client/windows/CCastleInterface.h

@@ -66,8 +66,8 @@ public:
 	CBuildingRect(CCastleBuildings * Par, const CGTownInstance *Town, const CStructure *Str);
 	bool operator<(const CBuildingRect & p2) const;
 	void hover(bool on) override;
-	void clickLeft(tribool down, bool previousState) override;
-	void showPopupWindow() override;
+	void clickPressed(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
 	void mouseMoved (const Point & cursorPosition, const Point & lastUpdateDistance) override;
 	bool receiveEvent(const Point & position, int eventType) const override;
 	void tick(uint32_t msPassed) override;
@@ -112,8 +112,8 @@ public:
 	void set(const CGHeroInstance * newHero);
 
 	void hover (bool on) override;
-	void clickLeft(tribool down, bool previousState) override;
-	void showPopupWindow() override;
+	void clickPressed(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
 	void deactivate() override;
 };
 
@@ -192,8 +192,8 @@ public:
 
 	void update();
 	void hover(bool on) override;
-	void clickLeft(tribool down, bool previousState) override;
-	void showPopupWindow() override;
+	void clickPressed(const Point & cursorPosition) override;
+	void showPopupWindow(const Point & cursorPosition) override;
 	bool getShowAvailable();
 };
 
@@ -208,7 +208,7 @@ public:
 	CTownInfo(int posX, int posY, const CGTownInstance * town, bool townHall);
 
 	void hover(bool on) override;
-	void showPopupWindow() override;
+	void showPopupWindow(const Point & cursorPosition) override;
 };
 
 /// Class which manages the castle window
@@ -274,8 +274,8 @@ class CHallInterface : public CStatusbarWindow
 	public:
 		CBuildingBox(int x, int y, const CGTownInstance * Town, const CBuilding * Building);
 		void hover(bool on) override;
-		void clickLeft(tribool down, bool previousState) override;
-		void showPopupWindow() override;
+		void clickPressed(const Point & cursorPosition) override;
+		void showPopupWindow(const Point & cursorPosition) override;
 	};
 	const CGTownInstance * town;
 
@@ -346,7 +346,7 @@ class CFortScreen : public CStatusbarWindow
 
 		void creaturesChangedEventHandler();
 		void hover(bool on) override;
-		void clickLeft(tribool down, bool previousState) override;
+		void clickPressed(const Point & cursorPosition) override;
 	};
 	std::shared_ptr<CLabel> title;
 	std::vector<std::shared_ptr<RecruitArea>> recAreas;
@@ -371,8 +371,8 @@ class CMageGuildScreen : public CStatusbarWindow
 
 	public:
 		Scroll(Point position, const CSpell *Spell);
-		void clickLeft(tribool down, bool previousState) override;
-		void showPopupWindow() override;
+		void clickPressed(const Point & cursorPosition) override;
+		void showPopupWindow(const Point & cursorPosition) override;
 		void hover(bool on) override;
 	};
 	std::shared_ptr<CPicture> window;

+ 3 - 4
client/windows/CCreatureWindow.cpp

@@ -114,10 +114,9 @@ void CCommanderSkillIcon::setObject(std::shared_ptr<CIntObject> newObject)
 	redraw();
 }
 
-void CCommanderSkillIcon::clickLeft(tribool down, bool previousState)
+void CCommanderSkillIcon::clickPressed(const Point & cursorPosition)
 {
-	if(down)
-		callback();
+	callback();
 }
 
 static std::string skillToFile(int skill, int level, bool selected)
@@ -588,7 +587,7 @@ CStackWindow::MainSection::MainSection(CStackWindow * owner, int yOffset, bool s
 		auto art = parent->info->stackNode->getArt(ArtifactPosition::CREATURE_SLOT);
 		if(art)
 		{
-			parent->stackArtifactIcon = std::make_shared<CAnimImage>("ARTIFACT", art->artType->iconIndex, 0, pos.x, pos.y);
+			parent->stackArtifactIcon = std::make_shared<CAnimImage>("ARTIFACT", art->artType->getIconIndex(), 0, pos.x, pos.y);
 			parent->stackArtifactHelp = std::make_shared<LRClickableAreaWTextComp>(Rect(pos, Point(44, 44)), CComponent::artifact);
 			parent->stackArtifactHelp->type = art->artType->getId();
 

+ 1 - 1
client/windows/CCreatureWindow.h

@@ -37,7 +37,7 @@ public:
 
 	std::function<void()> callback;
 
-	void clickLeft(tribool down, bool previousState) override;
+	void clickPressed(const Point & cursorPosition) override;
 
 	void setObject(std::shared_ptr<CIntObject> object);
 };

+ 10 - 13
client/windows/CHeroWindow.cpp

@@ -85,21 +85,18 @@ CHeroWithMaybePickedArtifact::CHeroWithMaybePickedArtifact(CWindowWithArtifacts
 {
 }
 
-void CHeroSwitcher::clickLeft(tribool down, bool previousState)
+void CHeroSwitcher::clickPressed(const Point & cursorPosition)
 {
-	if(!down)
+	//TODO: do not recreate window
+	if (false)
 	{
-		//TODO: do not recreate window
-		if (false)
-		{
-			owner->update(hero, true);
-		}
-		else
-		{
-			const CGHeroInstance * buf = hero;
-			GH.windows().popWindows(1);
-			GH.windows().createAndPushWindow<CHeroWindow>(buf);
-		}
+		owner->update(hero, true);
+	}
+	else
+	{
+		const CGHeroInstance * buf = hero;
+		GH.windows().popWindows(1);
+		GH.windows().createAndPushWindow<CHeroWindow>(buf);
 	}
 }
 

+ 2 - 2
client/windows/CHeroWindow.h

@@ -41,7 +41,7 @@ class CHeroSwitcher : public CIntObject
 	std::shared_ptr<CAnimImage> image;
 	CHeroWindow * owner;
 public:
-	void clickLeft(tribool down, bool previousState) override;
+	void clickPressed(const Point & cursorPosition) override;
 
 	CHeroSwitcher(CHeroWindow * owner_, Point pos_, const CGHeroInstance * hero_);
 };
@@ -127,6 +127,6 @@ public:
 	void updateGarrisons() override;
 
 	//friends
-	friend void CHeroArtPlace::clickLeft(tribool down, bool previousState);
+	friend void CHeroArtPlace::clickPressed(const Point & cursorPosition);
 	friend class CPlayerInterface;
 };

+ 7 - 18
client/windows/CKingdomInterface.cpp

@@ -90,7 +90,7 @@ InfoBox::InfoBox(Point position, InfoPos Pos, InfoSize Size, std::shared_ptr<IIn
 
 InfoBox::~InfoBox() = default;
 
-void InfoBox::showPopupWindow()
+void InfoBox::showPopupWindow(const Point & cursorPosition)
 {
 	std::shared_ptr<CComponent> comp;
 	std::string text;
@@ -101,26 +101,15 @@ void InfoBox::showPopupWindow()
 		CRClickPopup::createAndPush(text);
 }
 
-void InfoBox::clickLeft(tribool down, bool previousState)
-{
-	if((!down) && previousState)
-	{
-		std::shared_ptr<CComponent> comp;
-		std::string text;
-		data->prepareMessage(text, comp);
-
-		if(comp)
-			LOCPLINT->showInfoDialog(text, CInfoWindow::TCompsInfo(1, comp));
-	}
-}
-
-//TODO?
-/*
-void InfoBox::update()
+void InfoBox::clickPressed(const Point & cursorPosition)
 {
+	std::shared_ptr<CComponent> comp;
+	std::string text;
+	data->prepareMessage(text, comp);
 
+	if(comp)
+		LOCPLINT->showInfoDialog(text, CInfoWindow::TCompsInfo(1, comp));
 }
-*/
 
 IInfoBoxData::IInfoBoxData(InfoType Type)
 	: type(Type)

+ 2 - 5
client/windows/CKingdomInterface.h

@@ -73,11 +73,8 @@ public:
 	InfoBox(Point position, InfoPos Pos, InfoSize Size, std::shared_ptr<IInfoBoxData> Data);
 	~InfoBox();
 
-	void showPopupWindow() override;
-	void clickLeft(tribool down, bool previousState) override;
-
-	//Update object if data may have changed
-	//void update();
+	void showPopupWindow(const Point & cursorPosition) override;
+	void clickPressed(const Point & cursorPosition) override;
 };
 
 class IInfoBoxData

+ 4 - 6
client/windows/CQuestLog.cpp

@@ -39,10 +39,9 @@ VCMI_LIB_NAMESPACE_END
 
 class CAdvmapInterface;
 
-void CQuestLabel::clickLeft(tribool down, bool previousState)
+void CQuestLabel::clickPressed(const Point & cursorPosition)
 {
-	if (down)
-		callback();
+	callback();
 }
 
 void CQuestLabel::showAll(Canvas & to)
@@ -56,10 +55,9 @@ CQuestIcon::CQuestIcon (const std::string &defname, int index, int x, int y) :
 	addUsedEvents(LCLICK);
 }
 
-void CQuestIcon::clickLeft(tribool down, bool previousState)
+void CQuestIcon::clickPressed(const Point & cursorPosition)
 {
-	if (down)
-		callback();
+	callback();
 }
 
 void CQuestIcon::showAll(Canvas & to)

+ 3 - 3
client/windows/CQuestLog.h

@@ -45,7 +45,7 @@ public:
 
 	CQuestLabel(Rect position, EFonts Font = FONT_SMALL, ETextAlignment Align = ETextAlignment::TOPLEFT, const SDL_Color &Color = Colors::WHITE, const std::string &Text =  "")
 		: CMultiLineLabel (position, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, Text){};
-	void clickLeft(tribool down, bool previousState) override;
+	void clickPressed(const Point & cursorPosition) override;
 	void showAll(Canvas & to) override;
 };
 
@@ -56,7 +56,7 @@ public:
 
 	CQuestIcon(const std::string &defname, int index, int x=0, int y=0);
 
-	void clickLeft(tribool down, bool previousState) override;
+	void clickPressed(const Point & cursorPosition) override;
 	void showAll(Canvas & to) override;
 };
 
@@ -64,7 +64,7 @@ class CQuestMinimap : public CMinimap
 {
 	std::vector<std::shared_ptr<CQuestIcon>> icons;
 
-	void clickLeft(tribool down, bool previousState) override{}; //minimap ignores clicking on its surface
+	void clickPressed(const Point & cursorPosition) override{}; //minimap ignores clicking on its surface
 	void iconClicked();
 	void mouseDragged(const Point & cursorPosition, const Point & lastUpdateDistance) override{};
 

+ 6 - 7
client/windows/CSpellWindow.cpp

@@ -51,13 +51,12 @@ CSpellWindow::InteractiveArea::InteractiveArea(const Rect & myRect, std::functio
 	owner = _owner;
 }
 
-void CSpellWindow::InteractiveArea::clickLeft(tribool down, bool previousState)
+void CSpellWindow::InteractiveArea::clickPressed(const Point & cursorPosition)
 {
-	if(!down)
-		onLeft();
+	onLeft();
 }
 
-void CSpellWindow::InteractiveArea::showPopupWindow()
+void CSpellWindow::InteractiveArea::showPopupWindow(const Point & cursorPosition)
 {
 	CRClickPopup::createAndPush(helpText);
 }
@@ -472,9 +471,9 @@ CSpellWindow::SpellArea::SpellArea(Rect pos, CSpellWindow * owner)
 
 CSpellWindow::SpellArea::~SpellArea() = default;
 
-void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
+void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition)
 {
-	if(mySpell && !down)
+	if(mySpell)
 	{
 		auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero);
 		if(spellCost > owner->myHero->mana) //insufficient mana
@@ -540,7 +539,7 @@ void CSpellWindow::SpellArea::clickLeft(tribool down, bool previousState)
 	}
 }
 
-void CSpellWindow::SpellArea::showPopupWindow()
+void CSpellWindow::SpellArea::showPopupWindow(const Point & cursorPosition)
 {
 	if(mySpell)
 	{

+ 4 - 4
client/windows/CSpellWindow.h

@@ -44,8 +44,8 @@ class CSpellWindow : public CWindowObject
 		~SpellArea();
 		void setSpell(const CSpell * spell);
 
-		void clickLeft(tribool down, bool previousState) override;
-		void showPopupWindow() override;
+		void clickPressed(const Point & cursorPosition) override;
+		void showPopupWindow(const Point & cursorPosition) override;
 		void hover(bool on) override;
 	};
 
@@ -57,8 +57,8 @@ class CSpellWindow : public CWindowObject
 		std::string hoverText;
 		std::string helpText;
 	public:
-		void clickLeft(tribool down, bool previousState) override;
-		void showPopupWindow() override;
+		void clickPressed(const Point & cursorPosition) override;
+		void showPopupWindow(const Point & cursorPosition) override;
 		void hover(bool on) override;
 
 		InteractiveArea(const Rect &myRect, std::function<void()> funcL, int helpTextId, CSpellWindow * _owner);

+ 3 - 7
client/windows/CTradeWindow.cpp

@@ -171,13 +171,10 @@ void CTradeWindow::CTradeableItem::showAll(Canvas & to)
 	to.drawText(pos.topLeft() + posToSubCenter, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, subtitle);
 }
 
-void CTradeWindow::CTradeableItem::clickLeft(tribool down, bool previousState)
+void CTradeWindow::CTradeableItem::clickPressed(const Point & cursorPosition)
 {
 	CTradeWindow *mw = dynamic_cast<CTradeWindow *>(parent);
 	assert(mw);
-	if(down)
-	{
-
 		if(type == ARTIFACT_PLACEHOLDER)
 		{
 			CAltarWindow *aw = static_cast<CAltarWindow *>(mw);
@@ -221,7 +218,6 @@ void CTradeWindow::CTradeableItem::clickLeft(tribool down, bool previousState)
 				return;
 		}
 		mw->selectionChanged(left);
-	}
 }
 
 void CTradeWindow::CTradeableItem::showAllAt(const Point &dstPos, const std::string &customSub, Canvas & to)
@@ -262,7 +258,7 @@ void CTradeWindow::CTradeableItem::hover(bool on)
 	}
 }
 
-void CTradeWindow::CTradeableItem::showPopupWindow()
+void CTradeWindow::CTradeableItem::showPopupWindow(const Point & cursorPosition)
 {
 	switch(type)
 	{
@@ -803,7 +799,7 @@ void CMarketplaceWindow::makeDeal()
 			leftIdToSend = hLeft->serial;
 			break;
 		case EMarketMode::ARTIFACT_RESOURCE:
-			leftIdToSend = hLeft->getArtInstance()->id.getNum();
+			leftIdToSend = hLeft->getArtInstance()->getId().getNum();
 			break;
 		case EMarketMode::RESOURCE_ARTIFACT:
 			if(!ArtifactID(hRight->id).toArtifact()->canBePutAt(hero))

+ 2 - 2
client/windows/CTradeWindow.h

@@ -56,10 +56,10 @@ public:
 
 		void showAllAt(const Point & dstPos, const std::string & customSub, Canvas & to);
 
-		void showPopupWindow() override;
+		void showPopupWindow(const Point & cursorPosition) override;
 		void hover(bool on) override;
 		void showAll(Canvas & to) override;
-		void clickLeft(tribool down, bool previousState) override;
+		void clickPressed(const Point & cursorPosition) override;
 		std::string getName(int number = -1) const;
 		CTradeableItem(Point pos, EType Type, int ID, bool Left, int Serial);
 	};

+ 1 - 1
client/windows/CreaturePurchaseCard.cpp

@@ -123,7 +123,7 @@ CreaturePurchaseCard::CCreatureClickArea::CCreatureClickArea(const Point & posit
 	pos.h = CREATURE_HEIGHT;
 }
 
-void CreaturePurchaseCard::CCreatureClickArea::showPopupWindow()
+void CreaturePurchaseCard::CCreatureClickArea::showPopupWindow(const Point & cursorPosition)
 {
 	GH.windows().createAndPushWindow<CStackWindow>(creatureOnTheCard, true);
 }

+ 1 - 1
client/windows/CreaturePurchaseCard.h

@@ -49,7 +49,7 @@ private:
 	{
 	public:
 		CCreatureClickArea(const Point & pos, const std::shared_ptr<CCreaturePic> creaturePic, const CCreature * creatureOnTheCard);
-		void showPopupWindow() override;
+		void showPopupWindow(const Point & cursorPosition) override;
 		const CCreature * creatureOnTheCard;
 
 		// These are obtained by guessing and checking. I'm not sure how the other numbers

+ 16 - 24
client/windows/GUIClasses.cpp

@@ -96,13 +96,12 @@ void CRecruitmentWindow::CCreatureCard::select(bool on)
 	redraw();
 }
 
-void CRecruitmentWindow::CCreatureCard::clickLeft(tribool down, bool previousState)
+void CRecruitmentWindow::CCreatureCard::clickPressed(const Point & cursorPosition)
 {
-	if(down)
-		parent->select(this->shared_from_this());
+	parent->select(this->shared_from_this());
 }
 
-void CRecruitmentWindow::CCreatureCard::showPopupWindow()
+void CRecruitmentWindow::CCreatureCard::showPopupWindow(const Point & cursorPosition)
 {
 	GH.windows().createAndPushWindow<CStackWindow>(creature, true);
 }
@@ -552,13 +551,13 @@ void CTavernWindow::show(Canvas & to)
 	CCS->videoh->update(pos.x+70, pos.y+56, to.getInternalSurface(), true, false);
 }
 
-void CTavernWindow::HeroPortrait::clickLeft(tribool down, bool previousState)
+void CTavernWindow::HeroPortrait::clickPressed(const Point & cursorPosition)
 {
-	if(h && previousState && !down)
+	if(h)
 		*_sel = _id;
 }
 
-void CTavernWindow::HeroPortrait::showPopupWindow()
+void CTavernWindow::HeroPortrait::showPopupWindow(const Point & cursorPosition)
 {
 	if(h)
 		GH.windows().createAndPushWindow<CRClickPopupInt>(std::make_shared<CHeroWindow>(h));
@@ -1150,13 +1149,10 @@ void CTransformerWindow::CItem::move()
 	left = !left;
 }
 
-void CTransformerWindow::CItem::clickLeft(tribool down, bool previousState)
+void CTransformerWindow::CItem::clickPressed(const Point & cursorPosition)
 {
-	if(previousState && (!down))
-	{
-		move();
-		parent->redraw();
-	}
+	move();
+	parent->redraw();
 }
 
 void CTransformerWindow::CItem::update()
@@ -1258,16 +1254,13 @@ CUniversityWindow::CItem::CItem(CUniversityWindow * _parent, int _ID, int X, int
 	pos.w = icon->pos.w;
 }
 
-void CUniversityWindow::CItem::clickLeft(tribool down, bool previousState)
+void CUniversityWindow::CItem::clickPressed(const Point & cursorPosition)
 {
-	if(previousState && (!down))
-	{
-		if(state() == 2)
-			GH.windows().createAndPushWindow<CUnivConfirmWindow>(parent, ID, LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= 2000);
-	}
+	if(state() == 2)
+		GH.windows().createAndPushWindow<CUnivConfirmWindow>(parent, ID, LOCPLINT->cb->getResourceAmount(EGameResID::GOLD) >= 2000);
 }
 
-void CUniversityWindow::CItem::showPopupWindow()
+void CUniversityWindow::CItem::showPopupWindow(const Point & cursorPosition)
 {
 	CRClickPopup::createAndPush(CGI->skillh->getByIndex(ID)->getDescriptionTranslated(1), std::make_shared<CComponent>(CComponent::secskill, ID, 1));
 }
@@ -1792,13 +1785,12 @@ void CObjectListWindow::CItem::select(bool on)
 	redraw();//???
 }
 
-void CObjectListWindow::CItem::clickLeft(tribool down, bool previousState)
+void CObjectListWindow::CItem::clickPressed(const Point & cursorPosition)
 {
-	if( previousState && !down)
-		parent->changeSelection(index);
+	parent->changeSelection(index);
 }
 
-void CObjectListWindow::CItem::clickDouble()
+void CObjectListWindow::CItem::clickDouble(const Point & cursorPosition)
 {
 	parent->elementSelected();
 }

+ 9 - 9
client/windows/GUIClasses.h

@@ -59,8 +59,8 @@ class CRecruitmentWindow : public CStatusbarWindow
 
 		CCreatureCard(CRecruitmentWindow * window, const CCreature * crea, int totalAmount);
 
-		void clickLeft(tribool down, bool previousState) override;
-		void showPopupWindow() override;
+		void clickPressed(const Point & cursorPosition) override;
+		void showPopupWindow(const Point & cursorPosition) override;
 		void showAll(Canvas & to) override;
 	};
 
@@ -161,8 +161,8 @@ class CObjectListWindow : public CWindowObject
 		CItem(CObjectListWindow * parent, size_t id, std::string text);
 
 		void select(bool on);
-		void clickLeft(tribool down, bool previousState) override;
-		void clickDouble() override;
+		void clickPressed(const Point & cursorPosition) override;
+		void clickDouble(const Point & cursorPosition) override;
 	};
 
 	std::function<void(int)> onSelect;//called when OK button is pressed, returns id of selected item.
@@ -205,8 +205,8 @@ public:
 		std::string description; // "XXX is a level Y ZZZ with N artifacts"
 		const CGHeroInstance * h;
 
-		void clickLeft(tribool down, bool previousState) override;
-		void showPopupWindow() override;
+		void clickPressed(const Point & cursorPosition) override;
+		void showPopupWindow(const Point & cursorPosition) override;
 		void hover (bool on) override;
 		HeroPortrait(int & sel, int id, int x, int y, const CGHeroInstance * H);
 
@@ -376,7 +376,7 @@ class CTransformerWindow : public CStatusbarWindow, public CGarrisonHolder
 		std::shared_ptr<CLabel> count;
 
 		void move();
-		void clickLeft(tribool down, bool previousState) override;
+		void clickPressed(const Point & cursorPosition) override;
 		void update();
 		CItem(CTransformerWindow * parent, int size, int id);
 	};
@@ -417,8 +417,8 @@ class CUniversityWindow : public CStatusbarWindow
 		CUniversityWindow * parent;
 
 		void showAll(Canvas & to) override;
-		void clickLeft(tribool down, bool previousState) override;
-		void showPopupWindow() override;
+		void clickPressed(const Point & cursorPosition) override;
+		void showPopupWindow(const Point & cursorPosition) override;
 		void hover(bool on) override;
 		int state();//0=can't learn, 1=learned, 2=can learn
 		CItem(CUniversityWindow * _parent, int _ID, int X, int Y);

+ 23 - 2
client/windows/settings/GeneralOptionsTab.cpp

@@ -153,6 +153,10 @@ GeneralOptionsTab::GeneralOptionsTab()
 	{
 		setBoolSetting("video", "showfps", value);
 	});
+	addCallback("hapticFeedbackChanged", [](bool value)
+	{
+		setBoolSetting("general", "hapticFeedback", value);
+	});
 
 	//moved from "other" tab that is disabled for now to avoid excessible tabs with barely any content
 	addCallback("availableCreaturesAsDwellingChanged", [=](int value)
@@ -190,6 +194,10 @@ GeneralOptionsTab::GeneralOptionsTab()
 	std::shared_ptr<CToggleButton> framerateCheckbox = widget<CToggleButton>("framerateCheckbox");
 	framerateCheckbox->setSelected(settings["video"]["showfps"].Bool());
 
+	std::shared_ptr<CToggleButton> hapticFeedbackCheckbox = widget<CToggleButton>("hapticFeedbackCheckbox");
+	if (hapticFeedbackCheckbox)
+		hapticFeedbackCheckbox->setSelected(settings["general"]["hapticFeedback"].Bool());
+
 	std::shared_ptr<CSlider> musicSlider = widget<CSlider>("musicSlider");
 	musicSlider->scrollTo(CCS->musich->getVolume());
 
@@ -277,10 +285,18 @@ void GeneralOptionsTab::setGameResolution(int index)
 	gameRes["height"].Float() = resolution.y;
 
 	widget<CLabel>("resolutionLabel")->setText(resolutionToLabelString(resolution.x, resolution.y));
+
+	GH.dispatchMainThread([](){
+		boost::unique_lock<boost::recursive_mutex> lock(*CPlayerInterface::pim);
+		GH.onScreenResize();
+	});
 }
 
 void GeneralOptionsTab::setFullscreenMode(bool on, bool exclusive)
 {
+	if (on == settings["video"]["fullscreen"].Bool() && exclusive == settings["video"]["realFullscreen"].Bool())
+		return;
+
 	setBoolSetting("video", "realFullscreen", exclusive);
 	setBoolSetting("video", "fullscreen", on);
 
@@ -288,12 +304,17 @@ void GeneralOptionsTab::setFullscreenMode(bool on, bool exclusive)
 	std::shared_ptr<CToggleButton> fullscreenBorderlessCheckbox = widget<CToggleButton>("fullscreenBorderlessCheckbox");
 
 	if (fullscreenBorderlessCheckbox)
-		fullscreenBorderlessCheckbox->setSelected(on && !exclusive);
+		fullscreenBorderlessCheckbox->setSelectedSilent(on && !exclusive);
 
 	if (fullscreenExclusiveCheckbox)
-		fullscreenExclusiveCheckbox->setSelected(on && exclusive);
+		fullscreenExclusiveCheckbox->setSelectedSilent(on && exclusive);
 
 	updateResolutionSelector();
+
+	GH.dispatchMainThread([](){
+		boost::unique_lock<boost::recursive_mutex> lock(*CPlayerInterface::pim);
+		GH.onScreenResize();
+	});
 }
 
 void GeneralOptionsTab::selectGameScaling()

+ 2 - 0
cmake_modules/VCMI_lib.cmake

@@ -220,6 +220,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/BattleFieldHandler.cpp
 		${MAIN_LIB_DIR}/CAndroidVMHelper.cpp
 		${MAIN_LIB_DIR}/CArtHandler.cpp
+		${MAIN_LIB_DIR}/CArtifactInstance.cpp
 		${MAIN_LIB_DIR}/CBonusTypeHandler.cpp
 		${MAIN_LIB_DIR}/CBuildingHandler.cpp
 		${MAIN_LIB_DIR}/CConfigHandler.cpp
@@ -550,6 +551,7 @@ macro(add_main_lib TARGET_NAME LIBRARY_TYPE)
 		${MAIN_LIB_DIR}/BattleFieldHandler.h
 		${MAIN_LIB_DIR}/CAndroidVMHelper.h
 		${MAIN_LIB_DIR}/CArtHandler.h
+		${MAIN_LIB_DIR}/CArtifactInstance.h
 		${MAIN_LIB_DIR}/CBonusTypeHandler.h
 		${MAIN_LIB_DIR}/CBuildingHandler.h
 		${MAIN_LIB_DIR}/CConfigHandler.h

+ 18 - 7
config/schemas/settings.json

@@ -33,6 +33,7 @@
 				"extraDump",
 				"userRelativePointer",
 				"relativePointerSpeedMultiplier",
+				"hapticFeedback",
 				"longTouchTimeMilliseconds",
 				"autosaveCountLimit",
 				"useSavePrefix",
@@ -105,6 +106,10 @@
 					"type" : "number",
 					"default" : 1000
 				},
+				"hapticFeedback" : {
+					"type" : "boolean",
+					"default" : false
+				},
 				"autosaveCountLimit" : {
 					"type" : "number",
 					"default": 5
@@ -124,13 +129,14 @@
 			"additionalProperties" : false,
 			"default" : {},
 			"required" : [ 
-				"resolution", 
-				"fullscreen", 
-				"realFullscreen", 
-				"cursor", 
-				"showIntro", 
-				"spellbookAnimation", 
-				"driver", 
+				"resolution",
+				"reservedWidth",
+				"fullscreen",
+				"realFullscreen",
+				"cursor",
+				"showIntro",
+				"spellbookAnimation",
+				"driver",
 				"displayIndex",
 				"showfps",
 				"targetfps"
@@ -149,6 +155,11 @@
 					"defaultAndroid" : {"width" : 800, "height" : 600, "scaling" : 200 },
 					"default" : {"width" : 800, "height" : 600, "scaling" : 100 }
 				},
+				"reservedWidth" : {
+					"type" : "number",
+					"defaultIOS" : 0.1, // iOS camera cutout / notch is excluded from available area by SDL
+					"default" : 0
+				},
 				"fullscreen" : {
 					"type" : "boolean",
 					"default" : false

+ 11 - 1
config/widgets/settings/generalOptionsTab.json

@@ -57,6 +57,10 @@
 					"name": "longTouchLabel",
 					"text": "vcmi.systemOptions.longTouchButton.hover",
 					"created" : "touchscreen"
+				},
+				{
+					"text": "vcmi.systemOptions.hapticFeedbackButton.hover",
+					"created" : "mobile"
 				}
 			]
 		},
@@ -76,7 +80,7 @@
 					"name": "scalingButton",
 					"type": "buttonGear",
 					"help": "vcmi.systemOptions.scalingButton",
-					"callback": "setGameScaling",
+					"callback": "setGameScaling"
 				},
 				{
 					"name": "fullscreenBorderlessCheckbox",
@@ -106,6 +110,12 @@
 					"help": "vcmi.systemOptions.longTouchButton",
 					"callback": "setLongTouchDuration",
 					"created" : "touchscreen"
+				},
+				{
+					"name": "hapticFeedbackCheckbox",
+					"help": "vcmi.systemOptions.hapticFeedbackButton",
+					"callback": "hapticFeedbackChanged",
+					"created" : "mobile"
 				}
 			]
 		},

+ 60 - 54
launcher/translation/polish.ts

@@ -6,84 +6,84 @@
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="22"/>
         <source>VCMI on Discord</source>
-        <translation type="unfinished">VCMI na Discordzie</translation>
+        <translation>VCMI na Discordzie</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="29"/>
         <source>Have a question? Found a bug? Want to help? Join us!</source>
-        <translation type="unfinished">Masz pytanie? Znalazłeś błąd? Chcesz pomóc? Dołącz do nas!</translation>
+        <translation>Masz pytanie? Znalazłeś błąd? Chcesz pomóc? Dołącz do nas!</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="36"/>
         <source>VCMI on Github</source>
-        <translation type="unfinished">VCMI na Github</translation>
+        <translation>VCMI na Github</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="55"/>
         <source>Our Community</source>
-        <translation type="unfinished"></translation>
+        <translation>Nasza społeczność</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="62"/>
         <source>VCMI on Slack</source>
-        <translation type="unfinished">VCMI na Slacku</translation>
+        <translation>VCMI na Slacku</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="98"/>
         <source>Build Information</source>
-        <translation type="unfinished"></translation>
+        <translation>Informacje o wersji</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="115"/>
         <source>User data directory</source>
-        <translation type="unfinished">Katalog danych użytkownika</translation>
+        <translation>Katalog danych użytkownika</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="122"/>
         <location filename="../aboutProject/aboutproject_moc.ui" line="129"/>
         <location filename="../aboutProject/aboutproject_moc.ui" line="193"/>
         <source>Open</source>
-        <translation type="unfinished">Otwórz</translation>
+        <translation>Otwórz</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="136"/>
         <source>Check for updates</source>
-        <translation type="unfinished"></translation>
+        <translation>Sprawdź aktualizacje</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="156"/>
         <source>Game version</source>
-        <translation type="unfinished"></translation>
+        <translation>Wersja gry</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="163"/>
         <source>Log files directory</source>
-        <translation type="unfinished">Katalog logów</translation>
+        <translation>Katalog logów</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="176"/>
         <source>Data Directories</source>
-        <translation type="unfinished">Katalogi z danymi</translation>
+        <translation>Katalogi z danymi</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="213"/>
         <source>Game data directory</source>
-        <translation type="unfinished"></translation>
+        <translation>Katalog danych gry</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="220"/>
         <source>Operating System</source>
-        <translation type="unfinished"></translation>
+        <translation>System operacyjny</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="273"/>
         <source>Project homepage</source>
-        <translation type="unfinished"></translation>
+        <translation>Witryna projektu</translation>
     </message>
     <message>
         <location filename="../aboutProject/aboutproject_moc.ui" line="286"/>
         <source>Report a bug</source>
-        <translation type="unfinished"></translation>
+        <translation>Zgłoś błąd</translation>
     </message>
 </context>
 <context>
@@ -365,27 +365,27 @@
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="311"/>
         <source>This mod can not be installed or enabled because the following dependencies are not present</source>
-        <translation type="unfinished">Ten mod nie może zostać zainstalowany lub włączony ponieważ następujące zależności nie zostały spełnione</translation>
+        <translation>Ten mod nie może zostać zainstalowany lub włączony ponieważ następujące zależności nie zostały spełnione</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="312"/>
         <source>This mod can not be enabled because the following mods are incompatible with it</source>
-        <translation type="unfinished">Ten mod nie może zostać włączony ponieważ następujące mody są z nim niekompatybilne</translation>
+        <translation>Ten mod nie może zostać włączony ponieważ następujące mody są z nim niekompatybilne</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="313"/>
         <source>This mod cannot be disabled because it is required by the following mods</source>
-        <translation type="unfinished">Ten mod nie może zostać wyłączony ponieważ jest wymagany by do uruchomienia następujących modów</translation>
+        <translation>Ten mod nie może zostać wyłączony ponieważ jest wymagany do uruchomienia następujących modów</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="314"/>
         <source>This mod cannot be uninstalled or updated because it is required by the following mods</source>
-        <translation type="unfinished">Ten mod nie może zostać odinstalowany lub zaktualizowany ponieważ jest wymagany do uruchomienia następujących modów</translation>
+        <translation>Ten mod nie może zostać odinstalowany lub zaktualizowany ponieważ jest wymagany do uruchomienia następujących modów</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="315"/>
         <source>This is a submod and it cannot be installed or uninstalled separately from its parent mod</source>
-        <translation type="unfinished">To jest moduł składowy innego moda i nie może być zainstalowany lub odinstalowany oddzielnie od moda nadrzędnego</translation>
+        <translation>To jest moduł składowy innego moda i nie może być zainstalowany lub odinstalowany oddzielnie od moda nadrzędnego</translation>
     </message>
     <message>
         <location filename="../modManager/cmodlistview_moc.cpp" line="330"/>
@@ -435,67 +435,67 @@
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="448"/>
         <source>Interface Scaling</source>
-        <translation type="unfinished"></translation>
+        <translation>Skala interfejsu</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="398"/>
         <source>Neutral AI in battles</source>
-        <translation type="unfinished"></translation>
+        <translation>AI bitewne jednostek neutralnych</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="203"/>
         <source>Enemy AI in battles</source>
-        <translation type="unfinished"></translation>
+        <translation>AI bitewne wrogów</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="509"/>
         <source>Additional repository</source>
-        <translation type="unfinished"></translation>
+        <translation>Dodatkowe repozytorium</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="482"/>
         <source>Adventure Map Allies</source>
-        <translation type="unfinished"></translation>
+        <translation>AI sojuszników mapy przygody</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="441"/>
         <source>Adventure Map Enemies</source>
-        <translation type="unfinished"></translation>
+        <translation>AI wrogów mapy przygody</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="316"/>
         <source>Windowed</source>
-        <translation type="unfinished"></translation>
+        <translation>Okno</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="321"/>
         <source>Borderless fullscreen</source>
-        <translation type="unfinished"></translation>
+        <translation>Pełny ekran (tryb okna)</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="326"/>
         <source>Exclusive fullscreen</source>
-        <translation type="unfinished"></translation>
+        <translation>Pełny ekran klasyczny</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="530"/>
         <source>Friendly AI in battles</source>
-        <translation type="unfinished"></translation>
+        <translation>AI bitewne sojuszników</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="590"/>
         <source>Framerate Limit</source>
-        <translation type="unfinished"></translation>
+        <translation>Limit FPS</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="523"/>
         <source>Refresh now</source>
-        <translation type="unfinished"></translation>
+        <translation>Odśwież</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="234"/>
         <source>Default repository</source>
-        <translation type="unfinished"></translation>
+        <translation>Domyślne repozytorium</translation>
     </message>
     <message>
         <source>Update now</source>
@@ -527,7 +527,13 @@ Windowed - game will run inside a window that covers part of your screen
 Borderless Windowed Mode - game will run in a window that covers entirely of your screen, using same resolution as your screen.
 
 Fullscreen Exclusive Mode - game will cover entirety of your screen and will use selected resolution.</source>
-        <translation type="unfinished"></translation>
+        <translation>Wybierz tryb wyświetlania dla gry
+
+Okno - gra będzie funkcjonować w oknie przysłaniającym część ekranu
+
+Pełny ekran w trybie okna - gra uruchomi się w oknie przysłaniającym cały ekran, w obecnej rozdzielczości twojego ekranu.
+
+Pełny ekran klasyczny - gra przysłoni cały ekran uruchamiając się w wybranej przez ciebie rozdzielczości ekranu.</translation>
     </message>
     <message>
         <location filename="../settingsView/csettingsview_moc.ui" line="290"/>
@@ -654,12 +660,12 @@ Fullscreen Exclusive Mode - game will cover entirety of your screen and will use
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="127"/>
         <source>Select your language</source>
-        <translation type="unfinished">Wybierz język</translation>
+        <translation>Wybierz język</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="177"/>
         <source>Have a question? Found a bug? Want to help? Join us!</source>
-        <translation type="unfinished">Masz pytanie? Znalazłeś błąd? Chcesz pomóc? Dołącz do nas!</translation>
+        <translation>Masz pytanie? Znalazłeś błąd? Chcesz pomóc? Dołącz do nas!</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="186"/>
@@ -670,7 +676,7 @@ Before you can start playing, there are a few more steps that need to be complet
 Please keep in mind that in order to use VCMI you must own the original data files for Heroes® of Might and Magic® III: Complete or The Shadow of Death.
 
 Heroes® of Might and Magic® III HD is currently not supported!</source>
-        <translation type="unfinished">Dziękujemy za zainstalowanie VCMI.
+        <translation>Dziękujemy za zainstalowanie VCMI.
 
 Jest jeszcze kilka kroków, które trzeba wykonać żeby móc zagrać.
 
@@ -681,22 +687,22 @@ Heroes III: HD Edition nie jest obecnie wspierane!</translation>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="257"/>
         <source>Locate Heroes III data files</source>
-        <translation type="unfinished">Znajdź pliki Heroes III</translation>
+        <translation>Znajdź pliki Heroes III</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="346"/>
         <source>If you don&apos;t have a copy of Heroes III installed, you can use our automatic installation tool &apos;vcmibuilder&apos;, which only requires the GoG.com Heroes III installer. Please visit our wiki for detailed instructions.</source>
-        <translation type="unfinished">Jeśli nie masz zainstalowanej kopii Heroes III istnieje możliwość użycia naszego automatycznego narzędzia instalacyjnego &apos;vcmibuilder&apos; by wyodrębnić dane z instalatora GoG.com. Odwiedź nasze wiki po szczegółowe instrukcje.</translation>
+        <translation>Jeśli nie masz zainstalowanej kopii Heroes III istnieje możliwość użycia naszego automatycznego narzędzia instalacyjnego &apos;vcmibuilder&apos; by wyodrębnić dane z instalatora GoG.com. Odwiedź nasze wiki po szczegółowe instrukcje.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="362"/>
         <source>To run VCMI, Heroes III data files need to be present in one of the specified locations. Please copy the Heroes III data to one of these directories.</source>
-        <translation type="unfinished">VCMI wymaga plików Heroes III w jednej z wymienionych wyżej lokalizacji. Proszę, skopiuj pliki Heroes III do jednego z tych katalogów.</translation>
+        <translation>VCMI wymaga plików Heroes III w jednej z wymienionych wyżej lokalizacji. Proszę, skopiuj pliki Heroes III do jednego z tych katalogów.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="397"/>
         <source>Alternatively, you can provide the directory where Heroes III data is installed and VCMI will copy the existing data automatically.</source>
-        <translation type="unfinished">Możesz też wybrać folder z zainstalowanym Heroes III i VCMI automatycznie skopiuje istniejące dane.</translation>
+        <translation>Możesz też wybrać folder z zainstalowanym Heroes III i VCMI automatycznie skopiuje istniejące dane.</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="426"/>
@@ -706,32 +712,32 @@ Heroes III: HD Edition nie jest obecnie wspierane!</translation>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="466"/>
         <source>The automatic detection of the Heroes III language has failed. Please select the language of your Heroes III manually</source>
-        <translation type="unfinished">Automatyczna detekcja języka nie powiodła się. Proszę wybrać język twojego Heroes III</translation>
+        <translation>Automatyczna detekcja języka nie powiodła się. Proszę wybrać język twojego Heroes III</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="740"/>
         <source>Install a translation of Heroes III in your preferred language</source>
-        <translation type="unfinished">Zainstaluj tłumaczenie Heroes III dla twojego języka</translation>
+        <translation>Zainstaluj tłumaczenie Heroes III dla twojego preferowanego języka</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="756"/>
         <source>Optionally, you can install additional mods either now, or at any point later, using the VCMI Launcher</source>
-        <translation type="unfinished">Opcjonalnie możesz zainstalować dodatkowe modyfikacje teraz lub później</translation>
+        <translation>Opcjonalnie możesz zainstalować dodatkowe modyfikacje teraz lub później</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="772"/>
         <source>Install support for playing Heroes III in resolutions higher than 800x600</source>
-        <translation type="unfinished">Zapinstaluj wsparcie dla grania w Heroes III w rozdzielczości innej niż 800x600</translation>
+        <translation>Zainstaluj wsparcie dla grania w Heroes III w rozdzielczości innej niż 800x600</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="788"/>
         <source>Install compatible version of &quot;Horn of the Abyss&quot;, a fan-made Heroes III expansion ported by the VCMI team</source>
-        <translation type="unfinished">Zainstaluj kompatybilną wersję fanowskiego dodatku Horn of the Abyss odtworzoną przez zespół VCMI</translation>
+        <translation>Zainstaluj kompatybilną wersję fanowskiego dodatku Horn of the Abyss odtworzoną przez zespół VCMI</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="804"/>
         <source>Install compatible version of &quot;In The Wake of Gods&quot;, a fan-made Heroes III expansion</source>
-        <translation type="unfinished">Zainstaluj kompatybilną wersję fanowskiego dodatku &quot;In The Wake Of Gods&quot;</translation>
+        <translation>Zainstaluj kompatybilną wersję fanowskiego dodatku &quot;In The Wake Of Gods&quot;</translation>
     </message>
     <message>
         <location filename="../firstLaunch/firstlaunch_moc.ui" line="851"/>
@@ -839,12 +845,12 @@ Heroes III: HD Edition nie jest obecnie wspierane!</translation>
     <message>
         <location filename="../languages.cpp" line="24"/>
         <source>Chinese</source>
-        <translation type="unfinished"></translation>
+        <translation></translation>
     </message>
     <message>
         <location filename="../languages.cpp" line="25"/>
         <source>English</source>
-        <translation type="unfinished">English (Angielski)</translation>
+        <translation type="unfinished"></translation>
     </message>
     <message>
         <location filename="../languages.cpp" line="26"/>
@@ -988,7 +994,7 @@ Heroes III: HD Edition nie jest obecnie wspierane!</translation>
     <message>
         <location filename="../lobby/lobby_moc.ui" line="76"/>
         <source>Players in lobby</source>
-        <translation type="unfinished">Ludzie w lobby</translation>
+        <translation>Gracze w lobby</translation>
     </message>
     <message>
         <location filename="../lobby/lobby_moc.ui" line="159"/>
@@ -1069,7 +1075,7 @@ Heroes III: HD Edition nie jest obecnie wspierane!</translation>
     <message>
         <location filename="../mainwindow_moc.ui" line="207"/>
         <source>Help</source>
-        <translation type="unfinished"></translation>
+        <translation>Pomoc</translation>
     </message>
     <message>
         <location filename="../mainwindow_moc.ui" line="276"/>
@@ -1097,7 +1103,7 @@ Heroes III: HD Edition nie jest obecnie wspierane!</translation>
     <message>
         <location filename="../updatedialog_moc.ui" line="71"/>
         <source>You have the latest version</source>
-        <translation type="unfinished">Posiadasz obecnie aktualną wersję</translation>
+        <translation>Masz obecnie najnowszą wersję</translation>
     </message>
     <message>
         <location filename="../updatedialog_moc.ui" line="94"/>
@@ -1107,7 +1113,7 @@ Heroes III: HD Edition nie jest obecnie wspierane!</translation>
     <message>
         <location filename="../updatedialog_moc.ui" line="101"/>
         <source>Check for updates on startup</source>
-        <translation type="unfinished">Sprawdź aktualizacje przy uruchomieniu</translation>
+        <translation>Sprawdź aktualizacje przy uruchomieniu</translation>
     </message>
 </context>
 </TS>

+ 36 - 23
lib/ArtifactUtils.cpp

@@ -12,6 +12,7 @@
 
 #include "CArtHandler.h"
 #include "GameSettings.h"
+#include "spells/CSpellHandler.h"
 
 #include "mapping/CMap.h"
 #include "mapObjects/CGHeroInstance.h"
@@ -21,7 +22,7 @@ VCMI_LIB_NAMESPACE_BEGIN
 DLL_LINKAGE ArtifactPosition ArtifactUtils::getArtAnyPosition(const CArtifactSet * target, const ArtifactID & aid)
 {
 	const auto * art = aid.toArtifact();
-	for(const auto & slot : art->possibleSlots.at(target->bearerType()))
+	for(const auto & slot : art->getPossibleSlots().at(target->bearerType()))
 	{
 		if(art->canBePutAt(target, slot))
 			return slot;
@@ -119,15 +120,15 @@ DLL_LINKAGE std::vector<const CArtifact*> ArtifactUtils::assemblyPossibilities(
 {
 	std::vector<const CArtifact*> arts;
 	const auto * art = aid.toArtifact();
-	if(art->canBeDisassembled())
+	if(art->isCombined())
 		return arts;
 
-	for(const auto artifact : art->constituentOf)
+	for(const auto artifact : art->getPartOf())
 	{
-		assert(artifact->constituents);
+		assert(artifact->isCombined());
 		bool possible = true;
 
-		for(const auto constituent : *artifact->constituents) //check if all constituents are available
+		for(const auto constituent : artifact->getConstituents()) //check if all constituents are available
 		{
 			if(equipped)
 			{
@@ -165,24 +166,23 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createScroll(const SpellID & sid)
 
 DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(CArtifact * art)
 {
-	if(art->canBeDisassembled())
+	assert(art);
+
+	auto * artInst = new CArtifactInstance(art);
+	if(art->isCombined())
 	{
-		auto * ret = new CCombinedArtifactInstance(art);
-		ret->createConstituents();
-		return ret;
+		assert(art->isCombined());
+		for(const auto & part : art->getConstituents())
+			artInst->addPart(ArtifactUtils::createNewArtifactInstance(part), ArtifactPosition::PRE_FIRST);
 	}
-	else
+	if(art->isGrowing())
 	{
-		auto * ret = new CArtifactInstance(art);
-		if(dynamic_cast<CGrowingArtifact*>(art))
-		{
-			auto bonus = std::make_shared<Bonus>();
-			bonus->type = BonusType::LEVEL_COUNTER;
-			bonus->val = 0;
-			ret->addNewBonus(bonus);
-		}
-		return ret;
+		auto bonus = std::make_shared<Bonus>();
+		bonus->type = BonusType::LEVEL_COUNTER;
+		bonus->val = 0;
+		artInst->addNewBonus(bonus);
 	}
+	return artInst;
 }
 
 DLL_LINKAGE CArtifactInstance * ArtifactUtils::createNewArtifactInstance(const ArtifactID & aid)
@@ -209,15 +209,28 @@ DLL_LINKAGE CArtifactInstance * ArtifactUtils::createArtifact(CMap * map, const
 		art = new CArtifactInstance(); // random, empty
 	}
 	map->addNewArtifactInstance(art);
-	if(art->artType && art->canBeDisassembled())
+	if(art->artType && art->isCombined())
 	{
-		auto * combined = dynamic_cast<CCombinedArtifactInstance*>(art);
-		for(CCombinedArtifactInstance::ConstituentInfo & ci : combined->constituentsInfo)
+		for(auto & part : art->getPartsInfo())
 		{
-			map->addNewArtifactInstance(ci.art);
+			map->addNewArtifactInstance(part.art);
 		}
 	}
 	return art;
 }
 
+DLL_LINKAGE void ArtifactUtils::insertScrrollSpellName(std::string & description, const SpellID & sid)
+{
+	// We expect scroll description to be like this: This scroll contains the [spell name] spell which is added
+	// into spell book for as long as hero carries the scroll. So we want to replace text in [...] with a spell name.
+	// However other language versions don't have name placeholder at all, so we have to be careful
+	auto nameStart = description.find_first_of('[');
+	auto nameEnd = description.find_first_of(']', nameStart);
+	if(sid.getNum() >= 0)
+	{
+		if(nameStart != std::string::npos && nameEnd != std::string::npos)
+			description = description.replace(nameStart, nameEnd - nameStart + 1, sid.toSpell(VLC->spells())->getNameTranslated());
+	}
+}
+
 VCMI_LIB_NAMESPACE_END

+ 1 - 0
lib/ArtifactUtils.h

@@ -41,6 +41,7 @@ namespace ArtifactUtils
 	DLL_LINKAGE CArtifactInstance * createNewArtifactInstance(CArtifact * art);
 	DLL_LINKAGE CArtifactInstance * createNewArtifactInstance(const ArtifactID & aid);
 	DLL_LINKAGE CArtifactInstance * createArtifact(CMap * map, const ArtifactID & aid, int spellID = -1);
+	DLL_LINKAGE void insertScrrollSpellName(std::string & description, const SpellID & sid);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 88 - 249
lib/CArtHandler.cpp

@@ -15,9 +15,7 @@
 #include "CGeneralTextHandler.h"
 #include "CModHandler.h"
 #include "GameSettings.h"
-#include "spells/CSpellHandler.h"
 #include "mapObjects/MapObjects.h"
-#include "NetPacksBase.h"
 #include "StringConstants.h"
 
 #include "mapObjectConstructors/AObjectTypeHandler.h"
@@ -48,6 +46,51 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 
+bool CCombinedArtifact::isCombined() const
+{
+	return !(constituents.empty());
+}
+
+const std::vector<CArtifact*> & CCombinedArtifact::getConstituents() const
+{
+	return constituents;
+}
+
+const std::vector<CArtifact*> & CCombinedArtifact::getPartOf() const
+{
+	return partOf;
+}
+
+bool CScrollArtifact::isScroll() const
+{
+	return static_cast<const CArtifact*>(this)->getId() == ArtifactID::SPELL_SCROLL;
+}
+
+bool CGrowingArtifact::isGrowing() const
+{
+	return !bonusesPerLevel.empty() || !thresholdBonuses.empty();
+}
+
+std::vector <std::pair<ui16, Bonus>> & CGrowingArtifact::getBonusesPerLevel()
+{
+	return bonusesPerLevel;
+}
+
+const std::vector <std::pair<ui16, Bonus>> & CGrowingArtifact::getBonusesPerLevel() const
+{
+	return bonusesPerLevel;
+}
+
+std::vector <std::pair<ui16, Bonus>> & CGrowingArtifact::getThresholdBonuses()
+{
+	return thresholdBonuses;
+}
+
+const std::vector <std::pair<ui16, Bonus>> & CGrowingArtifact::getThresholdBonuses() const
+{
+	return thresholdBonuses;
+}
+
 int32_t CArtifact::getIndex() const
 {
 	return id.toEnum();
@@ -136,11 +179,6 @@ bool CArtifact::isTradable() const
 	}
 }
 
-bool CArtifact::canBeDisassembled() const
-{
-	return !(constituents == nullptr);
-}
-
 bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) const
 {
 	auto simpleArtCanBePutAt = [this](const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) -> bool
@@ -160,7 +198,7 @@ bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, b
 
 	auto artCanBePutAt = [this, simpleArtCanBePutAt](const CArtifactSet * artSet, ArtifactPosition slot, bool assumeDestRemoved) -> bool
 	{
-		if(canBeDisassembled())
+		if(isCombined())
 		{
 			if(!simpleArtCanBePutAt(artSet, slot, assumeDestRemoved))
 				return false;
@@ -171,8 +209,8 @@ bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, b
 			fittingSet.artifactsWorn = artSet->artifactsWorn;
 			if(assumeDestRemoved)
 				fittingSet.removeArtifact(slot);
-			assert(constituents);
-			for(const auto art : *constituents)
+
+			for(const auto art : constituents)
 			{
 				auto possibleSlot = ArtifactUtils::getArtAnyPosition(&fittingSet, art->getId());
 				if(ArtifactUtils::isSlotEquipment(possibleSlot))
@@ -215,6 +253,8 @@ bool CArtifact::canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot, b
 }
 
 CArtifact::CArtifact()
+	: iconIndex(ArtifactID::NONE),
+	price(0)
 {
 	setNodeType(ARTIFACT);
 	possibleSlots[ArtBearer::HERO]; //we want to generate map entry even if it will be empty
@@ -259,38 +299,21 @@ void CArtifact::addNewBonus(const std::shared_ptr<Bonus>& b)
 	CBonusSystemNode::addNewBonus(b);
 }
 
-void CArtifact::updateFrom(const JsonNode& data)
+const std::map<ArtBearer::ArtBearer, std::vector<ArtifactPosition>> & CArtifact::getPossibleSlots() const
 {
-	//TODO:CArtifact::updateFrom
+	return possibleSlots;
 }
 
-void CArtifact::serializeJson(JsonSerializeFormat & handler)
+void CArtifact::updateFrom(const JsonNode& data)
 {
-
+	//TODO:CArtifact::updateFrom
 }
 
-void CGrowingArtifact::levelUpArtifact (CArtifactInstance * art)
+void CArtifact::setImage(int32_t iconIndex, std::string image, std::string large)
 {
-	auto b = std::make_shared<Bonus>();
-	b->type = BonusType::LEVEL_COUNTER;
-	b->val = 1;
-	b->duration = BonusDuration::COMMANDER_KILLED;
-	art->accumulateBonus(b);
-
-	for(const auto & bonus : bonusesPerLevel)
-	{
-		if (art->valOfBonuses(BonusType::LEVEL_COUNTER) % bonus.first == 0) //every n levels
-		{
-			art->accumulateBonus(std::make_shared<Bonus>(bonus.second));
-		}
-	}
-	for(const auto & bonus : thresholdBonuses)
-	{
-		if (art->valOfBonuses(BonusType::LEVEL_COUNTER) == bonus.first) //every n levels
-		{
-			art->addNewBonus(std::make_shared<Bonus>(bonus.second));
-		}
-	}
+	this->iconIndex = iconIndex;
+	this->image = image;
+	this->large = large;
 }
 
 CArtHandler::~CArtHandler() = default;
@@ -376,17 +399,19 @@ CArtifact * CArtHandler::loadFromJson(const std::string & scope, const JsonNode
 	assert(identifier.find(':') == std::string::npos);
 	assert(!scope.empty());
 
-	CArtifact * art = nullptr;
-
-	if(!VLC->settings()->getBoolean(EGameSettings::MODULE_COMMANDERS) || node["growing"].isNull())
+	CArtifact * art = new CArtifact();
+	if(!node["growing"].isNull())
 	{
-		art = new CArtifact();
-	}
-	else
-	{
-		auto * growing = new CGrowingArtifact();
-		loadGrowingArt(growing, node);
-		art = growing;
+		for(auto bonus : node["growing"]["bonusesPerLevel"].Vector())
+		{
+			art->bonusesPerLevel.emplace_back(static_cast<ui16>(bonus["level"].Float()), Bonus());
+			JsonUtils::parseBonus(bonus["bonus"], &art->bonusesPerLevel.back().second);
+		}
+		for(auto bonus : node["growing"]["thresholdBonuses"].Vector())
+		{
+			art->thresholdBonuses.emplace_back(static_cast<ui16>(bonus["level"].Float()), Bonus());
+			JsonUtils::parseBonus(bonus["bonus"], &art->thresholdBonuses.back().second);
+		}
 	}
 	art->id = ArtifactID(index);
 	art->identifier = identifier;
@@ -571,34 +596,19 @@ void CArtHandler::loadComponents(CArtifact * art, const JsonNode & node)
 {
 	if (!node["components"].isNull())
 	{
-		art->constituents = std::make_unique<std::vector<CArtifact *>>();
 		for(const auto & component : node["components"].Vector())
 		{
 			VLC->modh->identifiers.requestIdentifier("artifact", component, [=](si32 id)
 			{
 				// when this code is called both combinational art as well as component are loaded
 				// so it is safe to access any of them
-				art->constituents->push_back(objects[id]);
-				objects[id]->constituentOf.push_back(art);
+				art->constituents.push_back(objects[id]);
+				objects[id]->partOf.push_back(art);
 			});
 		}
 	}
 }
 
-void CArtHandler::loadGrowingArt(CGrowingArtifact * art, const JsonNode & node) const
-{
-	for (auto b : node["growing"]["bonusesPerLevel"].Vector())
-	{
-		art->bonusesPerLevel.emplace_back(static_cast<ui16>(b["level"].Float()), Bonus());
-		JsonUtils::parseBonus(b["bonus"], &art->bonusesPerLevel.back().second);
-	}
-	for (auto b : node["growing"]["thresholdBonuses"].Vector())
-	{
-		art->thresholdBonuses.emplace_back(static_cast<ui16>(b["level"].Float()), Bonus());
-		JsonUtils::parseBonus(b["bonus"], &art->thresholdBonuses.back().second);
-	}
-}
-
 ArtifactID CArtHandler::pickRandomArtifact(CRandomGenerator & rand, int flags, std::function<bool(ArtifactID)> accepts)
 {
 	auto getAllowedArts = [&](std::vector<ConstTransitivePtr<CArtifact> > &out, std::vector<CArtifact*> *arts, CArtifact::EartClass flag)
@@ -683,7 +693,7 @@ bool CArtHandler::legalArtifact(const ArtifactID & id)
 	auto art = objects[id];
 	//assert ( (!art->constituents) || art->constituents->size() ); //artifacts is not combined or has some components
 
-	if(art->constituents)
+	if(art->isCombined())
 		return false; //no combo artifacts spawning
 
 	if(art->aClass < CArtifact::ART_TREASURE || art->aClass > CArtifact::ART_RELIC)
@@ -787,179 +797,11 @@ void CArtHandler::afterLoadFinalization()
 	CBonusSystemNode::treeHasChanged();
 }
 
-CArtifactInstance::CArtifactInstance()
-{
-	init();
-}
-
-CArtifactInstance::CArtifactInstance( CArtifact *Art)
-{
-	init();
-	setType(Art);
-}
-
-void CArtifactInstance::setType( CArtifact *Art )
-{
-	artType = Art;
-	attachTo(*Art);
-}
-
-std::string CArtifactInstance::nodeName() const
-{
-	return "Artifact instance of " + (artType ? artType->getJsonKey() : std::string("uninitialized")) + " type";
-}
-
-void CArtifactInstance::init()
-{
-	id = ArtifactInstanceID();
-	id = static_cast<ArtifactInstanceID>(ArtifactID::NONE); //to be randomized
-	setNodeType(ARTIFACT_INSTANCE);
-}
-
-std::string CArtifactInstance::getDescription() const
-{
-	std::string text = artType->getDescriptionTranslated();
-	if(artType->getId() == ArtifactID::SPELL_SCROLL)
-	{
-		// we expect scroll description to be like this: This scroll contains the [spell name] spell which is added into your spell book for as long as you carry the scroll.
-		// so we want to replace text in [...] with a spell name
-		// however other language versions don't have name placeholder at all, so we have to be careful
-		SpellID spellID = getScrollSpellID();
-		size_t nameStart = text.find_first_of('[');
-		size_t nameEnd = text.find_first_of(']', nameStart);
-		if(spellID.getNum() >= 0)
-		{
-			if(nameStart != std::string::npos  &&  nameEnd != std::string::npos)
-				text = text.replace(nameStart, nameEnd - nameStart + 1, spellID.toSpell(VLC->spells())->getNameTranslated());
-		}
-	}
-	return text;
-}
-
-ArtifactID CArtifactInstance::getTypeId() const
-{
-	return artType->getId();
-}
-
-bool CArtifactInstance::canBePutAt(const ArtifactLocation & al, bool assumeDestRemoved) const
-{
-	return artType->canBePutAt(al.getHolderArtSet(), al.slot, assumeDestRemoved);
-}
-
-void CArtifactInstance::putAt(const ArtifactLocation & al)
-{
-	al.getHolderArtSet()->putArtifact(al.slot, this);
-}
-
-void CArtifactInstance::removeFrom(const ArtifactLocation & al)
-{
-	al.getHolderArtSet()->removeArtifact(al.slot);
-}
-
-bool CArtifactInstance::canBeDisassembled() const
-{
-	return artType->canBeDisassembled();
-}
-
-void CArtifactInstance::move(const ArtifactLocation & src, const ArtifactLocation & dst)
-{
-	removeFrom(src);
-	putAt(dst);
-}
-
-void CArtifactInstance::deserializationFix()
-{
-	setType(artType);
-}
-
-SpellID CArtifactInstance::getScrollSpellID() const
-{
-	const auto b = getBonusLocalFirst(Selector::type()(BonusType::SPELL));
-	if(!b)
-	{
-		logMod->warn("Warning: %s doesn't bear any spell!", nodeName());
-		return SpellID::NONE;
-	}
-	return SpellID(b->subtype);
-}
-
-bool CArtifactInstance::isPart(const CArtifactInstance *supposedPart) const
-{
-	return supposedPart == this;
-}
-
-CCombinedArtifactInstance::CCombinedArtifactInstance(CArtifact *Art)
-	: CArtifactInstance(Art) //TODO: seems unused, but need to be written
-{
-}
-
-void CCombinedArtifactInstance::createConstituents()
-{
-	assert(artType);
-	assert(artType->constituents);
-
-	for(const CArtifact * art : *artType->constituents)
-	{
-		addAsConstituent(ArtifactUtils::createNewArtifactInstance(art->getId()), ArtifactPosition::PRE_FIRST);
-	}
-}
-
-void CCombinedArtifactInstance::addAsConstituent(CArtifactInstance * art, const ArtifactPosition & slot)
-{
-	assert(vstd::contains_if(*artType->constituents, [=](const CArtifact * constituent){
-		return constituent->getId() == art->artType->getId();
-	}));
-	assert(art->getParentNodes().size() == 1  &&  art->getParentNodes().front() == art->artType);
-	constituentsInfo.emplace_back(art, slot);
-	attachTo(*art);
-}
-
-void CCombinedArtifactInstance::removeFrom(const ArtifactLocation & al)
-{
-	CArtifactInstance::removeFrom(al);
-	for(auto & part : constituentsInfo)
-	{
-		if(part.slot != ArtifactPosition::PRE_FIRST)
-			part.slot = ArtifactPosition::PRE_FIRST;
-	}
-}
-
-void CCombinedArtifactInstance::deserializationFix()
-{
-	for(ConstituentInfo &ci : constituentsInfo)
-		attachTo(*ci.art);
-}
-
-bool CCombinedArtifactInstance::isPart(const CArtifactInstance *supposedPart) const
-{
-	bool me = CArtifactInstance::isPart(supposedPart);
-	if(me)
-		return true;
-
-	//check for constituents
-	for(const ConstituentInfo &constituent : constituentsInfo)
-		if(constituent.art == supposedPart)
-			return true;
-
-	return false;
-}
-
-CCombinedArtifactInstance::ConstituentInfo::ConstituentInfo(CArtifactInstance * Art, const ArtifactPosition & Slot):
-	art(Art),
-	slot(Slot)
-{
-}
-
-bool CCombinedArtifactInstance::ConstituentInfo::operator==(const ConstituentInfo &rhs) const
-{
-	return art == rhs.art && slot == rhs.slot;
-}
-
 CArtifactSet::~CArtifactSet() = default;
 
 const CArtifactInstance * CArtifactSet::getArt(const ArtifactPosition & pos, bool excludeLocked) const
 {
-	if(const ArtSlotInfo *si = getSlot(pos))
+	if(const ArtSlotInfo * si = getSlot(pos))
 	{
 		if(si->artifact && (!excludeLocked || !si->locked))
 			return si->artifact;
@@ -1033,11 +875,11 @@ ArtifactPosition CArtifactSet::getArtPos(const CArtifactInstance *art) const
 const CArtifactInstance * CArtifactSet::getArtByInstanceId(const ArtifactInstanceID & artInstId) const
 {
 	for(auto i : artifactsWorn)
-		if(i.second.artifact->id == artInstId)
+		if(i.second.artifact->getId() == artInstId)
 			return i.second.artifact;
 
 	for(auto i : artifactsInBackpack)
-		if(i.artifact->id == artInstId)
+		if(i.artifact->getId() == artInstId)
 			return i.artifact;
 
 	return nullptr;
@@ -1047,7 +889,7 @@ const ArtifactPosition CArtifactSet::getSlotByInstance(const CArtifactInstance *
 {
 	if(artInst)
 	{
-		for(auto & slot : artInst->artType->possibleSlots.at(bearerType()))
+		for(const auto & slot : artInst->artType->getPossibleSlots().at(bearerType()))
 			if(getArt(slot) == artInst)
 				return slot;
 
@@ -1087,19 +929,18 @@ unsigned CArtifactSet::getArtPosCount(const ArtifactID & aid, bool onlyWorn, boo
 void CArtifactSet::putArtifact(ArtifactPosition slot, CArtifactInstance * art)
 {
 	setNewArtSlot(slot, art, false);
-	if(art->artType->canBeDisassembled() && ArtifactUtils::isSlotEquipment(slot))
+	if(art->artType->isCombined() && ArtifactUtils::isSlotEquipment(slot))
 	{
 		const CArtifactInstance * mainPart = nullptr;
-		auto & parts = dynamic_cast<CCombinedArtifactInstance*>(art)->constituentsInfo;
-		for(const auto & part : parts)
-			if(vstd::contains(part.art->artType->possibleSlots.at(bearerType()), slot)
+		for(const auto & part : art->getPartsInfo())
+			if(vstd::contains(part.art->artType->getPossibleSlots().at(bearerType()), slot)
 				&& (part.slot == ArtifactPosition::PRE_FIRST))
 			{
 				mainPart = part.art;
 				break;
 			}
 
-		for(auto & part : parts)
+		for(auto & part : art->getPartsInfo())
 		{
 			if(part.art != mainPart)
 			{
@@ -1118,10 +959,9 @@ void CArtifactSet::removeArtifact(ArtifactPosition slot)
 	auto art = getArt(slot, false);
 	if(art)
 	{
-		if(art->canBeDisassembled())
+		if(art->isCombined())
 		{
-			auto combinedArt = dynamic_cast<CCombinedArtifactInstance*>(art);
-			for(auto & part : combinedArt->constituentsInfo)
+			for(auto & part : art->getPartsInfo())
 			{
 				if(getArt(part.slot, false))
 					eraseArtSlot(part.slot);
@@ -1131,19 +971,18 @@ void CArtifactSet::removeArtifact(ArtifactPosition slot)
 	}
 }
 
-std::pair<const CCombinedArtifactInstance *, const CArtifactInstance *> CArtifactSet::searchForConstituent(const ArtifactID & aid) const
+std::pair<const CArtifactInstance *, const CArtifactInstance *> CArtifactSet::searchForConstituent(const ArtifactID & aid) const
 {
 	for(const auto & slot : artifactsInBackpack)
 	{
 		auto art = slot.artifact;
-		if(art->canBeDisassembled())
+		if(art->isCombined())
 		{
-			auto * ass = dynamic_cast<CCombinedArtifactInstance *>(art.get());
-			for(auto& ci : ass->constituentsInfo)
+			for(auto & ci : art->getPartsInfo())
 			{
 				if(ci.art->getTypeId() == aid)
 				{
-					return {ass, ci.art};
+					return {art, ci.art};
 				}
 			}
 		}
@@ -1156,7 +995,7 @@ const CArtifactInstance * CArtifactSet::getHiddenArt(const ArtifactID & aid) con
 	return searchForConstituent(aid).second;
 }
 
-const CCombinedArtifactInstance * CArtifactSet::getAssemblyByConstituent(const ArtifactID & aid) const
+const CArtifactInstance * CArtifactSet::getAssemblyByConstituent(const ArtifactID & aid) const
 {
 	return searchForConstituent(aid).first;
 }

+ 68 - 116
lib/CArtHandler.h

@@ -20,9 +20,7 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 class CArtHandler;
-class CArtifact;
 class CGHeroInstance;
-struct ArtifactLocation;
 class CArtifactSet;
 class CArtifactInstance;
 class CRandomGenerator;
@@ -44,26 +42,75 @@ namespace ArtBearer
 	};
 }
 
-class DLL_LINKAGE CArtifact : public Artifact, public CBonusSystemNode //container for artifacts
+class DLL_LINKAGE CCombinedArtifact
 {
-	ArtifactID id;
+protected:
+	CCombinedArtifact() = default;
+
+	std::vector<CArtifact*> constituents; // Artifacts IDs a combined artifact consists of, or nullptr.
+	std::vector<CArtifact*> partOf; // Reverse map of constituents - combined arts that include this art
+public:
+	bool isCombined() const;
+	const std::vector<CArtifact*> & getConstituents() const;
+	const std::vector<CArtifact*> & getPartOf() const;
+
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & constituents;
+		h & partOf;
+	}
+};
+
+class DLL_LINKAGE CScrollArtifact
+{
+protected:
+	CScrollArtifact() = default;
+public:
+	bool isScroll() const;
+};
+
+class DLL_LINKAGE CGrowingArtifact
+{
+protected:
+	CGrowingArtifact() = default;
+
+	std::vector <std::pair<ui16, Bonus>> bonusesPerLevel; // Bonus given each n levels
+	std::vector <std::pair<ui16, Bonus>> thresholdBonuses; // After certain level they will be added once
+public:
+	bool isGrowing() const;
+
+	std::vector <std::pair<ui16, Bonus>> & getBonusesPerLevel();
+	const std::vector <std::pair<ui16, Bonus>> & getBonusesPerLevel() const;
+	std::vector <std::pair<ui16, Bonus>> & getThresholdBonuses();
+	const std::vector <std::pair<ui16, Bonus>> & getThresholdBonuses() const;
 
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & bonusesPerLevel;
+		h & thresholdBonuses;
+	}
+};
+
+// Container for artifacts. Not for instances.
+class DLL_LINKAGE CArtifact
+	: public Artifact, public CBonusSystemNode, public CCombinedArtifact, public CScrollArtifact, public CGrowingArtifact
+{
+	ArtifactID id;
+	std::string image;
+	std::string large; // big image for custom artifacts, used in drag & drop
+	std::string advMapDef; // used for adventure map object
 	std::string modScope;
 	std::string identifier;
+	int32_t iconIndex;
+	uint32_t price;
+	CreatureID warMachine;
+	// Bearer Type => ids of slots where artifact can be placed
+	std::map<ArtBearer::ArtBearer, std::vector<ArtifactPosition>> possibleSlots;
 
 public:
 	enum EartClass {ART_SPECIAL=1, ART_TREASURE=2, ART_MINOR=4, ART_MAJOR=8, ART_RELIC=16}; //artifact classes
 
-	std::string image;
-	std::string large; // big image for custom artifacts, used in drag & drop
-	std::string advMapDef; //used for adventure map object
-	si32 iconIndex = ArtifactID::NONE;
-	ui32 price = 0;
-	std::map<ArtBearer::ArtBearer, std::vector<ArtifactPosition> > possibleSlots; //Bearer Type => ids of slots where artifact can be placed
-	std::unique_ptr<std::vector<CArtifact *> > constituents; // Artifacts IDs a combined artifact consists of, or nullptr.
-	std::vector<CArtifact *> constituentOf; // Reverse map of constituents - combined arts that include this art
 	EartClass aClass = ART_SPECIAL;
-	CreatureID warMachine;
 
 	int32_t getIndex() const override;
 	int32_t getIconIndex() const override;
@@ -88,26 +135,25 @@ public:
 	int getArtClassSerial() const; //0 - treasure, 1 - minor, 2 - major, 3 - relic, 4 - spell scroll, 5 - other
 	std::string nodeName() const override;
 	void addNewBonus(const std::shared_ptr<Bonus>& b) override;
+	const std::map<ArtBearer::ArtBearer, std::vector<ArtifactPosition>> & getPossibleSlots() const;
 
-	virtual void levelUpArtifact (CArtifactInstance * art){};
-
-	virtual bool canBeDisassembled() const;
 	virtual bool canBePutAt(const CArtifactSet * artSet, ArtifactPosition slot = ArtifactPosition::FIRST_AVAILABLE,
 		bool assumeDestRemoved = false) const;
 	void updateFrom(const JsonNode & data);
-	void serializeJson(JsonSerializeFormat & handler);
+	// Is used for testing purposes only
+	void setImage(int32_t iconIndex, std::string image, std::string large);
 
-	template <typename Handler> void serialize(Handler &h, const int version)
+	template <typename Handler> void serialize(Handler & h, const int version)
 	{
 		h & static_cast<CBonusSystemNode&>(*this);
+		h & static_cast<CCombinedArtifact&>(*this);
+		h & static_cast<CGrowingArtifact&>(*this);
 		h & image;
 		h & large;
 		h & advMapDef;
 		h & iconIndex;
 		h & price;
 		h & possibleSlots;
-		h & constituents;
-		h & constituentOf;
 		h & aClass;
 		h & id;
 		h & modScope;
@@ -121,99 +167,6 @@ public:
 	friend class CArtHandler;
 };
 
-class DLL_LINKAGE CGrowingArtifact : public CArtifact //for example commander artifacts getting bonuses after battle
-{
-public:
-	std::vector <std::pair <ui16, Bonus> > bonusesPerLevel; //bonus given each n levels
-	std::vector <std::pair <ui16, Bonus> > thresholdBonuses; //after certain level they will be added once
-
-	void levelUpArtifact(CArtifactInstance * art) override;
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CArtifact&>(*this);
-		h & bonusesPerLevel;
-		h & thresholdBonuses;
-	}
-};
-
-class DLL_LINKAGE CArtifactInstance : public CBonusSystemNode
-{
-protected:
-	void init();
-public:
-	CArtifactInstance(CArtifact * Art);
-	CArtifactInstance();
-
-	ConstTransitivePtr<CArtifact> artType;
-	ArtifactInstanceID id;
-
-	std::string nodeName() const override;
-	void deserializationFix();
-	void setType(CArtifact *Art);
-
-	std::string getDescription() const;
-	SpellID getScrollSpellID() const; //to be used with scrolls (and similar arts), -1 if none
-
-	ArtifactID getTypeId() const;
-	bool canBePutAt(const ArtifactLocation & al, bool assumeDestRemoved = false) const;  //forwards to the above one
-	virtual bool canBeDisassembled() const;
-	/// Checks if this a part of this artifact: artifact instance is a part
-	/// of itself, additionally truth is returned for constituents of combined arts
-	virtual bool isPart(const CArtifactInstance *supposedPart) const;
-
-	virtual void putAt(const ArtifactLocation & al);
-	virtual void removeFrom(const ArtifactLocation & al);
-	virtual void move(const ArtifactLocation & src, const ArtifactLocation & dst);
-
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CBonusSystemNode&>(*this);
-		h & artType;
-		h & id;
-		BONUS_TREE_DESERIALIZATION_FIX
-	}
-};
-
-class DLL_LINKAGE CCombinedArtifactInstance : public CArtifactInstance
-{
-public:
-	CCombinedArtifactInstance(CArtifact * Art);
-	struct ConstituentInfo
-	{
-		ConstTransitivePtr<CArtifactInstance> art;
-		ArtifactPosition slot;
-		template <typename Handler> void serialize(Handler &h, const int version)
-		{
-			h & art;
-			h & slot;
-		}
-
-		bool operator==(const ConstituentInfo &rhs) const;
-		ConstituentInfo(CArtifactInstance * art = nullptr, const ArtifactPosition & slot = ArtifactPosition::PRE_FIRST);
-	};
-
-	std::vector<ConstituentInfo> constituentsInfo;
-
-	bool isPart(const CArtifactInstance *supposedPart) const override;
-	void createConstituents();
-	void addAsConstituent(CArtifactInstance * art, const ArtifactPosition & slot);
-	void removeFrom(const ArtifactLocation & al) override;
-
-	CCombinedArtifactInstance() = default;
-
-	void deserializationFix();
-
-	friend class CArtifactInstance;
-	friend struct AssembledArtifact;
-	template <typename Handler> void serialize(Handler &h, const int version)
-	{
-		h & static_cast<CArtifactInstance&>(*this);
-		h & constituentsInfo;
-		BONUS_TREE_DESERIALIZATION_FIX
-	}
-};
-
 class DLL_LINKAGE CArtHandler : public CHandlerBase<ArtifactID, Artifact, CArtifact, ArtifactService>
 {
 public:
@@ -269,7 +222,6 @@ private:
 	void loadClass(CArtifact * art, const JsonNode & node) const;
 	void loadType(CArtifact * art, const JsonNode & node) const;
 	void loadComponents(CArtifact * art, const JsonNode & node);
-	void loadGrowingArt(CGrowingArtifact * art, const JsonNode & node) const;
 
 	void erasePickedArt(const ArtifactID & id);
 };
@@ -313,7 +265,7 @@ public:
 	const ArtifactPosition getSlotByInstance(const CArtifactInstance * artInst) const;
 	/// Search for constituents of assemblies in backpack which do not have an ArtifactPosition
 	const CArtifactInstance * getHiddenArt(const ArtifactID & aid) const;
-	const CCombinedArtifactInstance * getAssemblyByConstituent(const ArtifactID & aid) const;
+	const CArtifactInstance * getAssemblyByConstituent(const ArtifactID & aid) const;
 	/// Checks if hero possess artifact of given id (either in backack or worn)
 	bool hasArt(const ArtifactID & aid, bool onlyWorn = false, bool searchBackpackAssemblies = false, bool allowLocked = true) const;
 	bool hasArtBackpack(const ArtifactID & aid) const;
@@ -335,7 +287,7 @@ public:
 
 	void serializeJsonArtifacts(JsonSerializeFormat & handler, const std::string & fieldName, CMap * map);
 protected:
-	std::pair<const CCombinedArtifactInstance *, const CArtifactInstance *> searchForConstituent(const ArtifactID & aid) const;
+	std::pair<const CArtifactInstance *, const CArtifactInstance *> searchForConstituent(const ArtifactID & aid) const;
 
 private:
 	void serializeJsonHero(JsonSerializeFormat & handler, CMap * map);

+ 192 - 0
lib/CArtifactInstance.cpp

@@ -0,0 +1,192 @@
+/*
+ * CArtifactInstance.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+ 
+#include "StdInc.h"
+#include "CArtifactInstance.h"
+
+#include "ArtifactUtils.h"
+#include "CArtHandler.h"
+#include "NetPacksBase.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+void CCombinedArtifactInstance::addPart(CArtifactInstance * art, const ArtifactPosition & slot)
+{
+	auto artInst = static_cast<CArtifactInstance*>(this);
+	assert(vstd::contains_if(artInst->artType->getConstituents(),
+		[=](const CArtifact * partType)
+		{
+			return partType->getId() == art->getTypeId();
+		}));
+	assert(art->getParentNodes().size() == 1  &&  art->getParentNodes().front() == art->artType);
+	partsInfo.emplace_back(art, slot);
+	artInst->attachTo(*art);
+}
+
+bool CCombinedArtifactInstance::isPart(const CArtifactInstance * supposedPart) const
+{
+	if(supposedPart == this)
+		return true;
+
+	for(const PartInfo & constituent : partsInfo)
+	{
+		if(constituent.art == supposedPart)
+			return true;
+	}
+
+	return false;
+}
+
+std::vector<CCombinedArtifactInstance::PartInfo> & CCombinedArtifactInstance::getPartsInfo()
+{
+	// TODO romove this func. encapsulation violation
+	return partsInfo;
+}
+
+const std::vector<CCombinedArtifactInstance::PartInfo> & CCombinedArtifactInstance::getPartsInfo() const
+{
+	return partsInfo;
+}
+
+SpellID CScrollArtifactInstance::getScrollSpellID() const
+{
+	auto artInst = static_cast<const CArtifactInstance*>(this);
+	const auto bonus = artInst->getBonusLocalFirst(Selector::type()(BonusType::SPELL));
+	if(!bonus)
+	{
+		logMod->warn("Warning: %s doesn't bear any spell!", artInst->nodeName());
+		return SpellID::NONE;
+	}
+	return SpellID(bonus->subtype);
+}
+
+void CGrowingArtifactInstance::growingUp()
+{
+	auto artInst = static_cast<CArtifactInstance*>(this);
+	
+	if(artInst->artType->isGrowing())
+	{
+
+		auto bonus = std::make_shared<Bonus>();
+		bonus->type = BonusType::LEVEL_COUNTER;
+		bonus->val = 1;
+		bonus->duration = BonusDuration::COMMANDER_KILLED;
+		artInst->accumulateBonus(bonus);
+
+		for(const auto & bonus : artInst->artType->getBonusesPerLevel())
+		{
+			// Every n levels
+			if(artInst->valOfBonuses(BonusType::LEVEL_COUNTER) % bonus.first == 0)
+			{
+				artInst->accumulateBonus(std::make_shared<Bonus>(bonus.second));
+			}
+		}
+		for(const auto & bonus : artInst->artType->getThresholdBonuses())
+		{
+			// At n level
+			if(artInst->valOfBonuses(BonusType::LEVEL_COUNTER) == bonus.first)
+			{
+				artInst->addNewBonus(std::make_shared<Bonus>(bonus.second));
+			}
+		}
+	}
+}
+
+void CArtifactInstance::init()
+{
+	// Artifact to be randomized
+	id = static_cast<ArtifactInstanceID>(ArtifactID::NONE);
+	setNodeType(ARTIFACT_INSTANCE);
+}
+
+CArtifactInstance::CArtifactInstance(CArtifact * art)
+{
+	init();
+	setType(art);
+}
+
+CArtifactInstance::CArtifactInstance()
+{
+	init();
+}
+
+void CArtifactInstance::setType(CArtifact * art)
+{
+	artType = art;
+	attachTo(*art);
+}
+
+std::string CArtifactInstance::nodeName() const
+{
+	return "Artifact instance of " + (artType ? artType->getJsonKey() : std::string("uninitialized")) + " type";
+}
+
+std::string CArtifactInstance::getDescription() const
+{
+	std::string text = artType->getDescriptionTranslated();
+	if(artType->isScroll())
+		ArtifactUtils::insertScrrollSpellName(text, getScrollSpellID());
+	return text;
+}
+
+ArtifactID CArtifactInstance::getTypeId() const
+{
+	return artType->getId();
+}
+
+ArtifactInstanceID CArtifactInstance::getId() const
+{
+	return id;
+}
+
+void CArtifactInstance::setId(ArtifactInstanceID id)
+{
+	this->id = id;
+}
+
+bool CArtifactInstance::canBePutAt(const ArtifactLocation & al, bool assumeDestRemoved) const
+{
+	return artType->canBePutAt(al.getHolderArtSet(), al.slot, assumeDestRemoved);
+}
+
+bool CArtifactInstance::isCombined() const
+{
+	return artType->isCombined();
+}
+
+void CArtifactInstance::putAt(const ArtifactLocation & al)
+{
+	al.getHolderArtSet()->putArtifact(al.slot, this);
+}
+
+void CArtifactInstance::removeFrom(const ArtifactLocation & al)
+{
+	al.getHolderArtSet()->removeArtifact(al.slot);
+	for(auto & part : partsInfo)
+	{
+		if(part.slot != ArtifactPosition::PRE_FIRST)
+			part.slot = ArtifactPosition::PRE_FIRST;
+	}
+}
+
+void CArtifactInstance::move(const ArtifactLocation & src, const ArtifactLocation & dst)
+{
+	removeFrom(src);
+	putAt(dst);
+}
+
+void CArtifactInstance::deserializationFix()
+{
+	setType(artType);
+	for(PartInfo & part : partsInfo)
+		attachTo(*part.art);
+}
+
+VCMI_LIB_NAMESPACE_END

+ 102 - 0
lib/CArtifactInstance.h

@@ -0,0 +1,102 @@
+/*
+ * CArtifactInstance.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+
+#include "bonuses/CBonusSystemNode.h"
+#include "GameConstants.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+
+struct ArtifactLocation;
+
+class DLL_LINKAGE CCombinedArtifactInstance
+{
+protected:
+	CCombinedArtifactInstance() = default;
+public:
+	struct PartInfo
+	{
+		ConstTransitivePtr<CArtifactInstance> art;
+		ArtifactPosition slot;
+		template <typename Handler> void serialize(Handler & h, const int version)
+		{
+			h & art;
+			h & slot;
+		}
+		PartInfo(CArtifactInstance * art = nullptr, const ArtifactPosition & slot = ArtifactPosition::PRE_FIRST)
+			: art(art), slot(slot) {};
+	};
+	void addPart(CArtifactInstance * art, const ArtifactPosition & slot);
+	// Checks if supposed part inst is part of this combined art inst
+	bool isPart(const CArtifactInstance * supposedPart) const;
+	std::vector<PartInfo> & getPartsInfo();
+	const std::vector<PartInfo> & getPartsInfo() const;
+
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & partsInfo;
+	}
+protected:
+	std::vector<PartInfo> partsInfo;
+};
+
+class DLL_LINKAGE CScrollArtifactInstance
+{
+protected:
+	CScrollArtifactInstance() = default;
+public:
+	SpellID getScrollSpellID() const;
+};
+
+class DLL_LINKAGE CGrowingArtifactInstance
+{
+protected:
+	CGrowingArtifactInstance() = default;
+public:
+	void growingUp();
+};
+
+class DLL_LINKAGE CArtifactInstance
+	: public CBonusSystemNode, public CCombinedArtifactInstance, public CScrollArtifactInstance, public CGrowingArtifactInstance
+{
+protected:
+	void init();
+
+	ArtifactInstanceID id;
+public:
+	ConstTransitivePtr<CArtifact> artType;
+
+	CArtifactInstance(CArtifact * art);
+	CArtifactInstance();
+	void setType(CArtifact * art);
+	std::string nodeName() const override;
+	std::string getDescription() const;
+	ArtifactID getTypeId() const;
+	ArtifactInstanceID getId() const;
+	void setId(ArtifactInstanceID id);
+
+	bool canBePutAt(const ArtifactLocation & al, bool assumeDestRemoved = false) const;
+	bool isCombined() const;
+	void putAt(const ArtifactLocation & al);
+	void removeFrom(const ArtifactLocation & al);
+	void move(const ArtifactLocation & src, const ArtifactLocation & dst);
+	
+	void deserializationFix();
+	template <typename Handler> void serialize(Handler & h, const int version)
+	{
+		h & static_cast<CBonusSystemNode&>(*this);
+		h & static_cast<CCombinedArtifactInstance&>(*this);
+		h & artType;
+		h & id;
+		BONUS_TREE_DESERIALIZATION_FIX
+	}
+};
+
+VCMI_LIB_NAMESPACE_END

Неке датотеке нису приказане због велике количине промена