2
0
Эх сурвалжийг харах

Merge remote-tracking branch 'vcmi/beta' into develop

Ivan Savenko 2 жил өмнө
parent
commit
e8453916cf
54 өөрчлөгдсөн 320 нэмэгдсэн , 111 устгасан
  1. BIN
      Mods/vcmi/Data/radialMenu/stackFillOne.png
  2. BIN
      Mods/vcmi/Data/radialMenu/stackInfo.png
  3. BIN
      Mods/vcmi/Data/radialMenu/stackSplitEqual.png
  4. BIN
      Mods/vcmi/Data/radialMenu/stackSplitOne.png
  5. BIN
      Mods/vcmi/Sprites/buttons/backpackButtonIcon.png
  6. BIN
      Mods/vcmi/Sprites/buttons/backpackNormal.png
  7. BIN
      Mods/vcmi/Sprites/buttons/backpackPressed.png
  8. 1 1
      Mods/vcmi/config/vcmi/english.json
  9. 0 1
      Mods/vcmi/config/vcmi/german.json
  10. 0 1
      Mods/vcmi/config/vcmi/polish.json
  11. 1 1
      Mods/vcmi/config/vcmi/ukrainian.json
  12. 5 3
      client/battle/BattleActionsController.cpp
  13. 18 3
      client/battle/BattleFieldController.cpp
  14. 1 0
      client/battle/BattleFieldController.h
  15. 3 3
      client/eventsSDL/InputSourceMouse.cpp
  16. 7 6
      client/eventsSDL/InputSourceTouch.cpp
  17. 3 0
      client/eventsSDL/InputSourceTouch.h
  18. 5 0
      client/gui/CIntObject.cpp
  19. 2 0
      client/gui/CIntObject.h
  20. 3 3
      client/gui/CursorHandler.h
  21. 48 10
      client/gui/EventDispatcher.cpp
  22. 5 5
      client/gui/EventDispatcher.h
  23. 3 0
      client/gui/EventsReceiver.h
  24. 1 1
      client/gui/TextAlignment.h
  25. 2 0
      client/render/Canvas.cpp
  26. 7 0
      client/widgets/Buttons.cpp
  27. 18 20
      client/widgets/CArtifactsOfHeroBackpack.cpp
  28. 3 1
      client/widgets/CArtifactsOfHeroBackpack.h
  29. 3 2
      client/widgets/CGarrisonInt.cpp
  30. 24 0
      client/widgets/MiscWidgets.cpp
  31. 11 0
      client/widgets/MiscWidgets.h
  32. 8 0
      client/widgets/TextControls.cpp
  33. 38 6
      client/windows/CCreatureWindow.cpp
  34. 7 1
      client/windows/CCreatureWindow.h
  35. 11 7
      client/windows/CHeroBackpackWindow.cpp
  36. 2 1
      client/windows/CHeroBackpackWindow.h
  37. 1 0
      client/windows/CHeroWindow.cpp
  38. 16 3
      client/windows/InfoWindows.cpp
  39. 5 3
      client/windows/InfoWindows.h
  40. 5 1
      config/schemas/settings.json
  41. 0 7
      launcher/modManager/cmodlistmodel_moc.cpp
  42. 0 1
      launcher/modManager/cmodlistmodel_moc.h
  43. 11 7
      launcher/modManager/cmodlistview_moc.cpp
  44. 4 2
      launcher/modManager/cmodmanager.cpp
  45. 1 1
      launcher/modManager/cmodmanager.h
  46. 8 0
      lib/Rect.cpp
  47. 3 0
      lib/Rect.h
  48. 1 1
      lib/mapObjects/CGCreature.cpp
  49. 2 1
      lib/mapObjects/MiscObjects.h
  50. 3 5
      lib/rewardable/Interface.cpp
  51. 1 1
      lib/spells/effects/Teleport.cpp
  52. 13 2
      mapeditor/inspector/inspector.cpp
  53. 1 0
      mapeditor/mapcontroller.cpp
  54. 5 0
      server/battles/BattleFlowProcessor.cpp

BIN
Mods/vcmi/Data/radialMenu/stackFillOne.png


BIN
Mods/vcmi/Data/radialMenu/stackInfo.png


BIN
Mods/vcmi/Data/radialMenu/stackSplitEqual.png


BIN
Mods/vcmi/Data/radialMenu/stackSplitOne.png


BIN
Mods/vcmi/Sprites/buttons/backpackButtonIcon.png


BIN
Mods/vcmi/Sprites/buttons/backpackNormal.png


BIN
Mods/vcmi/Sprites/buttons/backpackPressed.png


+ 1 - 1
Mods/vcmi/config/vcmi/english.json

@@ -31,7 +31,7 @@
 	"vcmi.capitalColors.7" : "Pink",
 	
 	"vcmi.radialWheel.mergeSameUnit" : "Merge same creatures",
-	"vcmi.radialWheel.showUnitInformation" : "Show creature information",
+	"vcmi.radialWheel.fillSingleUnit" : "Fill with single creatures",
 	"vcmi.radialWheel.splitSingleUnit" : "Split off single creature",
 	"vcmi.radialWheel.splitUnitEqually" : "Split creatures equally",
 	"vcmi.radialWheel.moveUnit" : "Move creatures to another army",

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

@@ -31,7 +31,6 @@
 	"vcmi.capitalColors.7" : "Rosa",
 	
 	"vcmi.radialWheel.mergeSameUnit" : "Gleiche Kreaturen zusammenführen",
-	"vcmi.radialWheel.showUnitInformation" : "Informationen zur Kreatur anzeigen",
 	"vcmi.radialWheel.splitSingleUnit" : "Wegtrennen einzelner Kreaturen",
 	"vcmi.radialWheel.splitUnitEqually" : "Gleichmäßiges trennen der Kreaturen",
 	"vcmi.radialWheel.moveUnit" : "Verschieben der Kreatur in andere Armee",

+ 0 - 1
Mods/vcmi/config/vcmi/polish.json

@@ -31,7 +31,6 @@
 	"vcmi.capitalColors.7" : "Różowy",
 
 	"vcmi.radialWheel.mergeSameUnit" : "Złącz takie same stworzenia",
-	"vcmi.radialWheel.showUnitInformation" : "Pokaż informacje o stworzeniu",
 	"vcmi.radialWheel.splitSingleUnit" : "Wydziel pojedyncze stworzenie",
 	"vcmi.radialWheel.splitUnitEqually" : "Podziel stworzenia równo",
 	"vcmi.radialWheel.moveUnit" : "Przenieś stworzenia do innej armii",

+ 1 - 1
Mods/vcmi/config/vcmi/ukrainian.json

@@ -31,7 +31,7 @@
 	"vcmi.capitalColors.7" : "Рожевий",
 
 	"vcmi.radialWheel.mergeSameUnit" : "Об'єднати однакових істот",
-	"vcmi.radialWheel.showUnitInformation" : "Показати відомості про істоту",
+	"vcmi.radialWheel.fillSingleUnit" : "Заповнити одиничними істотами",
 	"vcmi.radialWheel.splitSingleUnit" : "Відділити одну істоту",
 	"vcmi.radialWheel.splitUnitEqually" : "Розділити істот порівну",
 	"vcmi.radialWheel.moveUnit" : "Перемістити істоту до іншої армії",

+ 5 - 3
client/battle/BattleActionsController.cpp

@@ -24,6 +24,7 @@
 #include "../gui/CIntObject.h"
 #include "../gui/WindowHandler.h"
 #include "../windows/CCreatureWindow.h"
+#include "../windows/InfoWindows.h"
 
 #include "../../CCallback.h"
 #include "../../lib/CConfigHandler.h"
@@ -607,10 +608,10 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 			return false;
 
 		case PossiblePlayerBattleAction::ANY_LOCATION:
-			return isCastingPossibleHere(action.spell().toSpell(), targetStack, targetHex);
+			return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex);
 
 		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-			return !selectedStack && targetStack && isCastingPossibleHere(action.spell().toSpell(), targetStack, targetHex);
+			return !selectedStack && targetStack && isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex);
 
 		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
 			if(targetStack && targetStackOwned && targetStack != owner.stacksController->getActiveStack() && targetStack->alive()) //only positive spells for other allied creatures
@@ -628,7 +629,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, B
 
 		case PossiblePlayerBattleAction::OBSTACLE:
 		case PossiblePlayerBattleAction::FREE_LOCATION:
-			return isCastingPossibleHere(action.spell().toSpell(), targetStack, targetHex);
+			return isCastingPossibleHere(action.spell().toSpell(), nullptr, targetHex);
 
 		case PossiblePlayerBattleAction::CATAPULT:
 			return owner.siegeController && owner.siegeController->isAttackableByCatapult(targetHex);
@@ -1003,6 +1004,7 @@ void BattleActionsController::onHexRightClicked(BattleHex clickedHex)
 	if (spellcastingModeActive() || isCurrentStackInSpellcastMode)
 	{
 		endCastingSpell();
+		CRClickPopup::createAndPush(CGI->generaltexth->translate("core.genrltxt.731")); // spell cancelled
 		return;
 	}
 

+ 18 - 3
client/battle/BattleFieldController.cpp

@@ -129,6 +129,9 @@ BattleFieldController::BattleFieldController(BattleInterface & owner):
 	attackCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRCOMBAT"));
 	attackCursors->preload();
 
+	spellCursors = GH.renderHandler().loadAnimation(AnimationPath::builtin("CRSPELL"));
+	spellCursors->preload();
+
 	initializeHexEdgeMaskToFrameIndex();
 
 	rangedFullDamageLimitImages = GH.renderHandler().loadAnimation(AnimationPath::builtin("battle/rangeHighlights/rangeHighlightsGreen.json"));
@@ -889,10 +892,22 @@ void BattleFieldController::show(Canvas & to)
 
 	if (isActive() && isGesturing() && getHoveredHex() != BattleHex::INVALID)
 	{
-		auto cursorIndex = CCS->curh->get<Cursor::Combat>();
-		auto imageIndex = static_cast<size_t>(cursorIndex);
+		auto combatCursorIndex = CCS->curh->get<Cursor::Combat>();
+		if (combatCursorIndex)
+		{
+			auto combatImageIndex = static_cast<size_t>(*combatCursorIndex);
+			to.draw(attackCursors->getImage(combatImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetCombat(combatImageIndex));
+			return;
+		}
+
+		auto spellCursorIndex = CCS->curh->get<Cursor::Spellcast>();
+		if (spellCursorIndex)
+		{
+			auto spellImageIndex = static_cast<size_t>(*spellCursorIndex);
+			to.draw(spellCursors->getImage(spellImageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetSpellcast());
+			return;
+		}
 
-		to.draw(attackCursors->getImage(imageIndex), hexPositionAbsolute(getHoveredHex()).center() - CCS->curh->getPivotOffsetCombat(imageIndex));
 	}
 }
 

+ 1 - 0
client/battle/BattleFieldController.h

@@ -38,6 +38,7 @@ class BattleFieldController : public CIntObject
 	std::shared_ptr<CAnimation> shootingRangeLimitImages;
 
 	std::shared_ptr<CAnimation> attackCursors;
+	std::shared_ptr<CAnimation> spellCursors;
 
 	/// Canvas that contains background, hex grid (if enabled), absolute obstacles and movement range of active stack
 	std::unique_ptr<Canvas> backgroundWithHexes;

+ 3 - 3
client/eventsSDL/InputSourceMouse.cpp

@@ -50,10 +50,10 @@ void InputSourceMouse::handleEventMouseButtonDown(const SDL_MouseButtonEvent & b
 			if(button.clicks > 1)
 				GH.events().dispatchMouseDoubleClick(position);
 			else
-				GH.events().dispatchMouseLeftButtonPressed(position);
+				GH.events().dispatchMouseLeftButtonPressed(position, 0);
 			break;
 		case SDL_BUTTON_RIGHT:
-			GH.events().dispatchShowPopup(position);
+			GH.events().dispatchShowPopup(position, 0);
 			break;
 		case SDL_BUTTON_MIDDLE:
 			middleClickPosition = position;
@@ -74,7 +74,7 @@ void InputSourceMouse::handleEventMouseButtonUp(const SDL_MouseButtonEvent & but
 	switch(button.button)
 	{
 		case SDL_BUTTON_LEFT:
-			GH.events().dispatchMouseLeftButtonReleased(position);
+			GH.events().dispatchMouseLeftButtonReleased(position, 0);
 			break;
 		case SDL_BUTTON_RIGHT:
 			GH.events().dispatchClosePopup(position);

+ 7 - 6
client/eventsSDL/InputSourceTouch.cpp

@@ -39,6 +39,7 @@ InputSourceTouch::InputSourceTouch()
 	params.relativeModeSpeedFactor = settings["general"]["relativePointerSpeedMultiplier"].Float();
 	params.longTouchTimeMilliseconds = settings["general"]["longTouchTimeMilliseconds"].Float();
 	params.hapticFeedbackEnabled = settings["general"]["hapticFeedback"].Bool();
+	params.touchToleranceDistance = settings["input"]["touchToleranceDistance"].Float();
 
 	if (params.useRelativeMode)
 		state = TouchState::RELATIVE_MODE;
@@ -121,9 +122,9 @@ void InputSourceTouch::handleEventFingerDown(const SDL_TouchFingerEvent & tfinge
 			if(tfinger.x > 0.5)
 			{
 				if (tfinger.y < 0.5)
-					GH.events().dispatchShowPopup(GH.getCursorPosition());
+					GH.events().dispatchShowPopup(GH.getCursorPosition(), params.touchToleranceDistance);
 				else
-					GH.events().dispatchMouseLeftButtonPressed(GH.getCursorPosition());
+					GH.events().dispatchMouseLeftButtonPressed(GH.getCursorPosition(), params.touchToleranceDistance);
 			}
 			break;
 		}
@@ -168,7 +169,7 @@ void InputSourceTouch::handleEventFingerUp(const SDL_TouchFingerEvent & tfinger)
 				if (tfinger.y < 0.5)
 					GH.events().dispatchClosePopup(GH.getCursorPosition());
 				else
-					GH.events().dispatchMouseLeftButtonReleased(GH.getCursorPosition());
+					GH.events().dispatchMouseLeftButtonReleased(GH.getCursorPosition(), params.touchToleranceDistance);
 			}
 			break;
 		}
@@ -180,8 +181,8 @@ void InputSourceTouch::handleEventFingerUp(const SDL_TouchFingerEvent & tfinger)
 		case TouchState::TAP_DOWN_SHORT:
 		{
 			GH.input().setCursorPosition(convertTouchToMouse(tfinger));
-			GH.events().dispatchMouseLeftButtonPressed(convertTouchToMouse(tfinger));
-			GH.events().dispatchMouseLeftButtonReleased(convertTouchToMouse(tfinger));
+			GH.events().dispatchMouseLeftButtonPressed(convertTouchToMouse(tfinger), params.touchToleranceDistance);
+			GH.events().dispatchMouseLeftButtonReleased(convertTouchToMouse(tfinger), params.touchToleranceDistance);
 			state = TouchState::IDLE;
 			break;
 		}
@@ -230,7 +231,7 @@ void InputSourceTouch::handleUpdate()
 		uint32_t currentTime = SDL_GetTicks();
 		if (currentTime > lastTapTimeTicks + params.longTouchTimeMilliseconds)
 		{
-			GH.events().dispatchShowPopup(GH.getCursorPosition());
+			GH.events().dispatchShowPopup(GH.getCursorPosition(), params.touchToleranceDistance);
 
 			if (GH.windows().isTopWindowPopup())
 			{

+ 3 - 0
client/eventsSDL/InputSourceTouch.h

@@ -78,6 +78,9 @@ struct TouchInputParameters
 	/// gesture will be qualified as pinch if distance between fingers is at least specified here
 	uint32_t pinchSensitivityThreshold = 10;
 
+	/// touch event will trigger clicking of elements up to X pixels away from actual touch position
+	uint32_t touchToleranceDistance = 20;
+
 	bool useRelativeMode = false;
 
 	bool hapticFeedbackEnabled = false;

+ 5 - 0
client/gui/CIntObject.cpp

@@ -263,6 +263,11 @@ bool CIntObject::receiveEvent(const Point & position, int eventType) const
 	return pos.isInside(position);
 }
 
+const Rect & CIntObject::getPosition() const
+{
+	return pos;
+}
+
 void CIntObject::onScreenResize()
 {
 	center(pos, true);

+ 2 - 0
client/gui/CIntObject.h

@@ -111,6 +111,8 @@ public:
 	/// by default, usedEvents inside UI elements are always handled
 	bool receiveEvent(const Point & position, int eventType) const override;
 
+	const Rect & getPosition() const override;
+
 	const Rect & center(const Rect &r, bool propagate = true); //sets pos so that r will be in the center of screen, assigns sizes of r to pos, returns new position
 	const Rect & center(const Point &p, bool propagate = true);  //moves object so that point p will be in its center
 	const Rect & center(bool propagate = true); //centers when pos.w and pos.h are set, returns new position

+ 3 - 3
client/gui/CursorHandler.h

@@ -126,7 +126,6 @@ class CursorHandler final
 
 	void changeGraphic(Cursor::Type type, size_t index);
 
-	Point getPivotOffsetSpellcast();
 	Point getPivotOffset();
 
 	void updateSpellcastCursor();
@@ -154,7 +153,7 @@ public:
 
 	/// Returns current index of cursor
 	template<typename Index>
-	Index get()
+	std::optional<Index> get()
 	{
 		bool typeValid = true;
 
@@ -165,9 +164,10 @@ public:
 
 		if (typeValid)
 			return static_cast<Index>(frame);
-		return Index::POINTER;
+		return std::nullopt;
 	}
 
+	Point getPivotOffsetSpellcast();
 	Point getPivotOffsetDefault(size_t index);
 	Point getPivotOffsetMap(size_t index);
 	Point getPivotOffsetCombat(size_t index);

+ 48 - 10
client/gui/EventDispatcher.cpp

@@ -16,7 +16,7 @@
 #include "MouseButton.h"
 #include "WindowHandler.h"
 
-#include "../../lib/Point.h"
+#include "../../lib/Rect.h"
 
 template<typename Functor>
 void EventDispatcher::processLists(ui16 activityFlag, const Functor & cb)
@@ -134,28 +134,64 @@ void EventDispatcher::dispatchMouseDoubleClick(const Point & position)
 	}
 
 	if(!doubleClicked)
-		handleLeftButtonClick(position, true);
+		handleLeftButtonClick(position, 0, true);
 }
 
-void EventDispatcher::dispatchMouseLeftButtonPressed(const Point & position)
+void EventDispatcher::dispatchMouseLeftButtonPressed(const Point & position, int tolerance)
 {
-	handleLeftButtonClick(position, true);
+	handleLeftButtonClick(position, tolerance, true);
 }
 
-void EventDispatcher::dispatchMouseLeftButtonReleased(const Point & position)
+void EventDispatcher::dispatchMouseLeftButtonReleased(const Point & position, int tolerance)
 {
-	handleLeftButtonClick(position, false);
+	handleLeftButtonClick(position, tolerance, false);
 }
 
-void EventDispatcher::dispatchShowPopup(const Point & position)
+AEventsReceiver * EventDispatcher::findElementInToleranceRange(const EventReceiversList & list, const Point & position, int eventToTest, int tolerance)
 {
+	AEventsReceiver * bestElement = nullptr;
+	int bestDistance = std::numeric_limits<int>::max();
+
+	for(auto & i : list)
+	{
+		// if there is element that can actually receive event then tolerance clicking is disabled
+		if( i->receiveEvent(position, eventToTest))
+			return nullptr;
+
+		if (i->getPosition().distanceTo(position) > bestDistance)
+			continue;
+
+		Point center = i->getPosition().center();
+		Point distance = center - position;
+
+		if (distance.lengthSquared() == 0)
+			continue;
+
+		Point moveDelta = distance * tolerance / distance.length();
+		Point testPosition = position + moveDelta;
+
+		if( !i->receiveEvent(testPosition, eventToTest))
+			continue;
+
+		bestElement = i;
+		bestDistance = i->getPosition().distanceTo(position);
+	}
+
+	return bestElement;
+}
+
+void EventDispatcher::dispatchShowPopup(const Point & position, int tolerance)
+{
+	AEventsReceiver * nearestElement = findElementInToleranceRange(rclickable, position, AEventsReceiver::LCLICK, tolerance);
+
 	auto hlp = rclickable;
+
 	for(auto & i : hlp)
 	{
 		if(!vstd::contains(rclickable, i))
 			continue;
 
-		if( !i->receiveEvent(position, AEventsReceiver::LCLICK))
+		if( !i->receiveEvent(position, AEventsReceiver::SHOW_POPUP) && i != nearestElement)
 			continue;
 
 		i->showPopupWindow(position);
@@ -170,7 +206,7 @@ void EventDispatcher::dispatchClosePopup(const Point & position)
 	assert(!GH.windows().isTopWindowPopup());
 }
 
-void EventDispatcher::handleLeftButtonClick(const Point & position, bool isPressed)
+void EventDispatcher::handleLeftButtonClick(const Point & position, int tolerance, bool isPressed)
 {
 	// WARNING: this approach is NOT SAFE
 	// 1) We allow (un)registering elements when list itself is being processed/iterated
@@ -181,13 +217,15 @@ void EventDispatcher::handleLeftButtonClick(const Point & position, bool isPress
 	// 3) new element is created *with exactly same address(!)
 	// 4) new element is registered and code will incorrectly assume that this element is still registered
 	// POSSIBLE SOLUTION: make EventReceivers inherit from create_shared_from this and store weak_ptr's in lists
+	AEventsReceiver * nearestElement = findElementInToleranceRange(lclickable, position, AEventsReceiver::LCLICK, tolerance);
 	auto hlp = lclickable;
+
 	for(auto & i : hlp)
 	{
 		if(!vstd::contains(lclickable, i))
 			continue;
 
-		if( i->receiveEvent(position, AEventsReceiver::LCLICK))
+		if( i->receiveEvent(position, AEventsReceiver::LCLICK) || i == nearestElement)
 		{
 			if(isPressed)
 				i->clickPressed(position);

+ 5 - 5
client/gui/EventDispatcher.h

@@ -35,8 +35,8 @@ class EventDispatcher
 	EventReceiversList textInterested;
 	EventReceiversList panningInterested;
 
-	void handleLeftButtonClick(const Point & position, bool isPressed);
-
+	void handleLeftButtonClick(const Point & position, int tolerance, bool isPressed);
+	AEventsReceiver * findElementInToleranceRange(const EventReceiversList & list, const Point & position, int eventToTest, int tolerance);
 
 	template<typename Functor>
 	void processLists(ui16 activityFlag, const Functor & cb);
@@ -56,15 +56,15 @@ public:
 	void dispatchShortcutReleased(const std::vector<EShortcut> & shortcuts);
 
 	/// Mouse events
-	void dispatchMouseLeftButtonPressed(const Point & position);
-	void dispatchMouseLeftButtonReleased(const Point & position);
+	void dispatchMouseLeftButtonPressed(const Point & position, int tolerance);
+	void dispatchMouseLeftButtonReleased(const Point & position, int tolerance);
 	void dispatchMouseScrolled(const Point & distance, const Point & position);
 	void dispatchMouseDoubleClick(const Point & position);
 	void dispatchMouseMoved(const Point & distance, const Point & position);
 
 	void dispatchMouseDragged(const Point & currentPosition, const Point & lastUpdateDistance);
 
-	void dispatchShowPopup(const Point & position);
+	void dispatchShowPopup(const Point & position, int tolerance);
 	void dispatchClosePopup(const Point & position);
 
 	void dispatchGesturePanningStarted(const Point & initialPosition);

+ 3 - 0
client/gui/EventsReceiver.h

@@ -11,6 +11,7 @@
 
 VCMI_LIB_NAMESPACE_BEGIN
 class Point;
+class Rect;
 VCMI_LIB_NAMESPACE_END
 
 class EventDispatcher;
@@ -39,6 +40,8 @@ protected:
 	/// 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;
 
+	virtual const Rect & getPosition() const= 0;
+
 public:
 	virtual void clickPressed(const Point & cursorPosition) {}
 	virtual void clickReleased(const Point & cursorPosition) {}

+ 1 - 1
client/gui/TextAlignment.h

@@ -9,4 +9,4 @@
  */
 #pragma once
 
-enum class ETextAlignment {TOPLEFT, CENTER, BOTTOMRIGHT};
+enum class ETextAlignment {TOPLEFT, TOPCENTER, CENTER, BOTTOMRIGHT};

+ 2 - 0
client/render/Canvas.cpp

@@ -151,6 +151,7 @@ void Canvas::drawText(const Point & position, const EFonts & font, const ColorRG
 	switch (alignment)
 	{
 	case ETextAlignment::TOPLEFT:      return graphics->fonts[font]->renderTextLeft  (surface, text, colorDest, renderArea.topLeft() + position);
+	case ETextAlignment::TOPCENTER:    return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, renderArea.topLeft() + position);
 	case ETextAlignment::CENTER:       return graphics->fonts[font]->renderTextCenter(surface, text, colorDest, renderArea.topLeft() + position);
 	case ETextAlignment::BOTTOMRIGHT:  return graphics->fonts[font]->renderTextRight (surface, text, colorDest, renderArea.topLeft() + position);
 	}
@@ -161,6 +162,7 @@ void Canvas::drawText(const Point & position, const EFonts & font, const ColorRG
 	switch (alignment)
 	{
 	case ETextAlignment::TOPLEFT:      return graphics->fonts[font]->renderTextLinesLeft  (surface, text, colorDest, renderArea.topLeft() + position);
+	case ETextAlignment::TOPCENTER:    return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, renderArea.topLeft() + position);
 	case ETextAlignment::CENTER:       return graphics->fonts[font]->renderTextLinesCenter(surface, text, colorDest, renderArea.topLeft() + position);
 	case ETextAlignment::BOTTOMRIGHT:  return graphics->fonts[font]->renderTextLinesRight (surface, text, colorDest, renderArea.topLeft() + position);
 	}

+ 7 - 0
client/widgets/Buttons.cpp

@@ -117,6 +117,13 @@ void CButton::setState(ButtonState newState)
 {
 	if (state == newState)
 		return;
+
+	if (newState == BLOCKED)
+		removeUsedEvents(LCLICK | SHOW_POPUP | HOVER | KEYBOARD);
+	else
+		addUsedEvents(LCLICK | SHOW_POPUP | HOVER | KEYBOARD);
+
+
 	state = newState;
 	update();
 }

+ 18 - 20
client/widgets/CArtifactsOfHeroBackpack.cpp

@@ -11,12 +11,9 @@
 #include "CArtifactsOfHeroBackpack.h"
 
 #include "../gui/CGuiHandler.h"
-#include "../gui/Shortcut.h"
 
-#include "Buttons.h"
 #include "Images.h"
 #include "GameSettings.h"
-#include "IHandlerBase.h"
 #include "ObjectLists.h"
 
 #include "../CPlayerInterface.h"
@@ -37,21 +34,12 @@ CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack(const Point & position)
 
 	backpack.resize(visibleCapacityMax);
 	size_t artPlaceIdx = 0;
-
-	const int slotSizeWithMargin = 46;
-
-	for(int i = 0; i < visibleCapacityMax; i++)
-	{
-		auto artifactSlotBackground = std::make_shared<CPicture>( ImagePath::builtin("heroWindow/artifactSlotEmpty"),
-			Point(slotSizeWithMargin * (i % HERO_BACKPACK_WINDOW_SLOT_COLUMNS), slotSizeWithMargin * (i / HERO_BACKPACK_WINDOW_SLOT_COLUMNS)));
-
-		backpackSlotsBackgrounds.emplace_back(artifactSlotBackground);
-	}
-
 	for(auto & artPlace : backpack)
 	{
-		artPlace = std::make_shared<CHeroArtPlace>(
-			Point(slotSizeWithMargin * (artPlaceIdx % HERO_BACKPACK_WINDOW_SLOT_COLUMNS), slotSizeWithMargin * (artPlaceIdx / HERO_BACKPACK_WINDOW_SLOT_COLUMNS)));
+		const auto pos = Point(slotSizeWithMargin * (artPlaceIdx % HERO_BACKPACK_WINDOW_SLOT_COLUMNS),
+			slotSizeWithMargin * (artPlaceIdx / HERO_BACKPACK_WINDOW_SLOT_COLUMNS));
+		backpackSlotsBackgrounds.emplace_back(std::make_shared<CPicture>(ImagePath::builtin("heroWindow/artifactSlotEmpty"), pos));
+		artPlace = std::make_shared<CHeroArtPlace>(pos);
 		artPlace->setArtifact(nullptr);
 		artPlace->leftClickCallback = std::bind(&CArtifactsOfHeroBase::leftClickArtPlace, this, _1);
 		artPlace->rightClickCallback = std::bind(&CArtifactsOfHeroBase::rightClickArtPlace, this, _1);
@@ -70,8 +58,18 @@ CArtifactsOfHeroBackpack::CArtifactsOfHeroBackpack(const Point & position)
 		};
 		backpackListBox = std::make_shared<CListBoxWithCallback>(
 				posMoved, onCreate, Point(0, 0), Point(0, 0), HERO_BACKPACK_WINDOW_SLOT_ROWS, 0, 0, 1,
-				Rect(HERO_BACKPACK_WINDOW_SLOT_COLUMNS * slotSizeWithMargin + 10, 0, HERO_BACKPACK_WINDOW_SLOT_ROWS * slotSizeWithMargin - 5, 0));
+				Rect(HERO_BACKPACK_WINDOW_SLOT_COLUMNS * slotSizeWithMargin + sliderPosOffsetX, 0, HERO_BACKPACK_WINDOW_SLOT_ROWS * slotSizeWithMargin - 2, 0));
 	}
+
+	pos.w = visibleCapacityMax > HERO_BACKPACK_WINDOW_SLOT_COLUMNS ? HERO_BACKPACK_WINDOW_SLOT_COLUMNS : visibleCapacityMax;
+	pos.w *= slotSizeWithMargin;
+	if(backpackListBox)
+		pos.w += sliderPosOffsetX + 16; // 16 is slider width. TODO: get it from CListBox directly;
+
+	pos.h = (visibleCapacityMax / HERO_BACKPACK_WINDOW_SLOT_COLUMNS);
+	if(visibleCapacityMax % HERO_BACKPACK_WINDOW_SLOT_COLUMNS != 0)
+		pos.h += 1;
+	pos.h *= slotSizeWithMargin;
 }
 
 void CArtifactsOfHeroBackpack::swapArtifacts(const ArtifactLocation & srcLoc, const ArtifactLocation & dstLoc)
@@ -88,7 +86,7 @@ void CArtifactsOfHeroBackpack::pickUpArtifact(CHeroArtPlace & artPlace)
 void CArtifactsOfHeroBackpack::scrollBackpack(int offset)
 {
 	if(backpackListBox)
-		backpackListBox->resize(getActiveSlotLinesNum());
+		backpackListBox->resize(getActiveSlotRowsNum());
 	backpackPos += offset;
 	auto slot = ArtifactPosition::BACKPACK_START + backpackPos;
 	for(auto artPlace : backpack)
@@ -102,11 +100,11 @@ void CArtifactsOfHeroBackpack::scrollBackpack(int offset)
 void CArtifactsOfHeroBackpack::updateBackpackSlots()
 {
 	if(backpackListBox)
-		backpackListBox->resize(getActiveSlotLinesNum());
+		backpackListBox->resize(getActiveSlotRowsNum());
 	CArtifactsOfHeroBase::updateBackpackSlots();
 }
 
-size_t CArtifactsOfHeroBackpack::getActiveSlotLinesNum()
+size_t CArtifactsOfHeroBackpack::getActiveSlotRowsNum()
 {
 	return (curHero->artifactsInBackpack.size() + HERO_BACKPACK_WINDOW_SLOT_COLUMNS - 1) / HERO_BACKPACK_WINDOW_SLOT_COLUMNS;
 }

+ 3 - 1
client/widgets/CArtifactsOfHeroBackpack.h

@@ -27,11 +27,13 @@ public:
 	void pickUpArtifact(CHeroArtPlace & artPlace);
 	void scrollBackpack(int offset) override;
 	void updateBackpackSlots() override;
-	size_t getActiveSlotLinesNum();
+	size_t getActiveSlotRowsNum();
 
 private:
 	std::shared_ptr<CListBoxWithCallback> backpackListBox;
 	std::vector<std::shared_ptr<CPicture>> backpackSlotsBackgrounds;
 	const size_t HERO_BACKPACK_WINDOW_SLOT_COLUMNS = 8;
 	const size_t HERO_BACKPACK_WINDOW_SLOT_ROWS = 8;
+	const int slotSizeWithMargin = 46;
+	const int sliderPosOffsetX = 10;
 };

+ 3 - 2
client/widgets/CGarrisonInt.cpp

@@ -369,14 +369,15 @@ void CGarrisonSlot::gesture(bool on, const Point & initialPosition, const Point
 
 	std::vector<RadialMenuConfig> menuElements = {
 		{ RadialMenuConfig::ITEM_NW, hasSameUnit, "stackMerge", "vcmi.radialWheel.mergeSameUnit", [this](){owner->bulkMergeStacks(this);} },
-		{ RadialMenuConfig::ITEM_NE, stackExists, "stackInfo", "vcmi.radialWheel.showUnitInformation", [this](){viewInfo();} },
+		{ RadialMenuConfig::ITEM_NE, hasOwnEmptySlots, "stackFillOne", "vcmi.radialWheel.fillSingleUnit", [this](){owner->bulkSplitStack(this);} },
 		{ RadialMenuConfig::ITEM_WW, hasOwnEmptySlots, "stackSplitOne", "vcmi.radialWheel.splitSingleUnit", [this](){splitIntoParts(this->getGarrison(), 1); } },
 		{ RadialMenuConfig::ITEM_EE, hasOwnEmptySlots, "stackSplitEqual", "vcmi.radialWheel.splitUnitEqually", [this](){owner->bulkSmartSplitStack(this);} },
 		{ RadialMenuConfig::ITEM_SW, hasOtherEmptySlots, "heroMove", "vcmi.radialWheel.moveUnit", [this](){owner->moveStackToAnotherArmy(this);} },
 		{ RadialMenuConfig::ITEM_SE, hasAnyEmptySlots, "heroSwap", "vcmi.radialWheel.splitUnit", [this](){ owner->selectSlot(this); owner->splitClick();} },
 	};
 
-	GH.windows().createAndPushWindow<RadialMenu>(pos.center(), menuElements);
+	if (hasAnyEmptySlots || hasSameUnit)
+		GH.windows().createAndPushWindow<RadialMenu>(pos.center(), menuElements);
 }
 
 void CGarrisonSlot::update()

+ 24 - 0
client/widgets/MiscWidgets.cpp

@@ -17,11 +17,13 @@
 
 #include "../CPlayerInterface.h"
 #include "../CGameInfo.h"
+#include "../PlayerLocalState.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/CGarrisonInt.h"
 #include "../windows/CCastleInterface.h"
 #include "../windows/InfoWindows.h"
 #include "../render/Canvas.h"
+#include "../render/Graphics.h"
 
 #include "../../CCallback.h"
 
@@ -30,6 +32,7 @@
 #include "../../lib/CGeneralTextHandler.h"
 #include "../../lib/GameSettings.h"
 #include "../../lib/TextOperations.h"
+#include "../../lib/mapObjects/CGCreature.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 
@@ -447,6 +450,27 @@ void CInteractableTownTooltip::init(const InfoAboutTown & town)
 	}
 }
 
+CreatureTooltip::CreatureTooltip(Point pos, const CGCreature * creature)
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	auto creatureData = (*CGI->creh)[creature->stacks.begin()->second->getCreatureID()].get();
+	creatureImage = std::make_shared<CAnimImage>(graphics->getAnimation(AnimationPath::builtin("TWCRPORT")), creatureData->getIconIndex());
+	creatureImage->center(Point(parent->pos.x + parent->pos.w / 2, parent->pos.y + creatureImage->pos.h / 2 + 11));
+
+	bool isHeroSelected = LOCPLINT->localState->getCurrentHero() != nullptr;
+	std::string textContent = isHeroSelected
+			? creature->getHoverText(LOCPLINT->localState->getCurrentHero())
+			: creature->getHoverText(LOCPLINT->playerID);
+
+	//TODO: window is bigger than OH3
+	//TODO: vertical alignment does not match H3. Commented below example that matches H3 for creatures count but supports only 1 line:
+	/*std::shared_ptr<CLabel> = std::make_shared<CLabel>(parent->pos.w / 2, 103,
+			FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, creature->getHoverText(LOCPLINT->playerID));*/
+
+	tooltipTextbox = std::make_shared<CTextBox>(textContent, Rect(15, 95, 230, 150), 0, FONT_SMALL, ETextAlignment::TOPCENTER, Colors::WHITE);
+}
+
 void MoraleLuckBox::set(const AFactionMember * node)
 {
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);

+ 11 - 0
client/widgets/MiscWidgets.h

@@ -14,6 +14,7 @@
 VCMI_LIB_NAMESPACE_BEGIN
 
 class CGGarrison;
+class CGCreature;
 struct InfoAboutArmy;
 struct InfoAboutHero;
 struct InfoAboutTown;
@@ -25,6 +26,7 @@ class AFactionMember;
 VCMI_LIB_NAMESPACE_END
 
 class CLabel;
+class CTextBox;
 class CGarrisonInt;
 class CCreatureAnim;
 class CComponent;
@@ -151,6 +153,15 @@ public:
 	void setAmount(int newAmount);
 };
 
+class CreatureTooltip : public CIntObject
+{
+	std::shared_ptr<CAnimImage> creatureImage;
+	std::shared_ptr<CTextBox> tooltipTextbox;
+
+public:
+	CreatureTooltip(Point pos, const CGCreature * creature);
+};
+
 /// Resource bar like that at the bottom of the adventure map screen
 class CMinorResDataBar : public CIntObject
 {

+ 8 - 0
client/widgets/TextControls.cpp

@@ -161,6 +161,12 @@ void CTextContainer::blitLine(Canvas & to, Rect destRect, std::string what)
 		where.y += getBorderSize().y;
 	}
 
+	if(alignment == ETextAlignment::TOPCENTER)
+	{
+		where.x += (int(destRect.w) - int(f->getStringWidth(what) - delimitersCount)) / 2;
+		where.y += getBorderSize().y;
+	}
+
 	if(alignment == ETextAlignment::CENTER)
 	{
 		where.x += (int(destRect.w) - int(f->getStringWidth(what) - delimitersCount)) / 2;
@@ -271,6 +277,7 @@ Rect CMultiLineLabel::getTextLocation()
 	switch(alignment)
 	{
 	case ETextAlignment::TOPLEFT:     return Rect(pos.topLeft(), textSize);
+	case ETextAlignment::TOPCENTER:   return Rect(pos.topLeft(), textSize);
 	case ETextAlignment::CENTER:      return Rect(pos.topLeft() + textOffset / 2, textSize);
 	case ETextAlignment::BOTTOMRIGHT: return Rect(pos.topLeft() + textOffset, textSize);
 	}
@@ -480,6 +487,7 @@ Point CGStatusBar::getBorderSize()
 	switch(alignment)
 	{
 	case ETextAlignment::TOPLEFT:     return Point(borderSize.x, borderSize.y);
+	case ETextAlignment::TOPCENTER:   return Point(pos.w / 2, borderSize.y);
 	case ETextAlignment::CENTER:      return Point(pos.w / 2, pos.h / 2);
 	case ETextAlignment::BOTTOMRIGHT: return Point(pos.w - borderSize.x, pos.h - borderSize.y);
 	}

+ 38 - 6
client/windows/CCreatureWindow.cpp

@@ -15,6 +15,7 @@
 
 #include "../CGameInfo.h"
 #include "../CPlayerInterface.h"
+#include "../render/Canvas.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/CArtifactHolder.h"
 #include "../widgets/CComponent.h"
@@ -83,7 +84,6 @@ public:
 	{
 	}
 
-
 	std::string getName() const
 	{
 		if(commander)
@@ -95,11 +95,14 @@ private:
 
 };
 
-CCommanderSkillIcon::CCommanderSkillIcon(std::shared_ptr<CIntObject> object_, std::function<void()> callback)
+CCommanderSkillIcon::CCommanderSkillIcon(std::shared_ptr<CIntObject> object_, bool isGrandmasterAbility_, std::function<void()> callback)
 	: object(),
+	isGrandmasterAbility(isGrandmasterAbility_),
+	isSelected(false),
 	callback(callback)
 {
 	pos = object_->pos;
+	this->isGrandmasterAbility = isGrandmasterAbility_;
 	setObject(object_);
 }
 
@@ -116,6 +119,25 @@ void CCommanderSkillIcon::setObject(std::shared_ptr<CIntObject> newObject)
 void CCommanderSkillIcon::clickPressed(const Point & cursorPosition)
 {
 	callback();
+	isSelected = true;
+}
+
+void CCommanderSkillIcon::deselect()
+{
+	isSelected = false;
+}
+
+bool CCommanderSkillIcon::getIsGrandmasterAbility()
+{
+	return isGrandmasterAbility;
+}
+
+void CCommanderSkillIcon::show(Canvas &to)
+{
+	CIntObject::show(to);
+
+	if(isGrandmasterAbility && isSelected)
+		to.drawBorder(pos, Colors::YELLOW, 2);
 }
 
 static ImagePath skillToFile(int skill, int level, bool selected)
@@ -375,7 +397,7 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i
 	{
 		Point skillPos = getSkillPos(index);
 
-		auto icon = std::make_shared<CCommanderSkillIcon>(std::make_shared<CPicture>(getSkillImage(index), skillPos.x, skillPos.y), [=]()
+		auto icon = std::make_shared<CCommanderSkillIcon>(std::make_shared<CPicture>(getSkillImage(index), skillPos.x, skillPos.y), false, [=]()
 		{
 			LOCPLINT->showInfoDialog(getSkillDescription(index));
 		});
@@ -429,7 +451,7 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i
 				{
 					const auto bonus = CGI->creh->skillRequirements[skillID-100].first;
 					const CStackInstance * stack = parent->info->commander;
-					auto icon = std::make_shared<CCommanderSkillIcon>(std::make_shared<CPicture>(stack->bonusToGraphics(bonus)), [](){});
+					auto icon = std::make_shared<CCommanderSkillIcon>(std::make_shared<CPicture>(stack->bonusToGraphics(bonus)), true,  [](){});
 					icon->callback = [=]()
 					{
 						parent->setSelection(skillID, icon);
@@ -446,6 +468,7 @@ CStackWindow::CommanderMainSection::CommanderMainSection(CStackWindow * owner, i
 		};
 
 		abilities = std::make_shared<CListBox>(onCreate, Point(38, 3+pos.h), Point(63, 0), 6, abilitiesCount);
+		abilities->setRedrawParent(true);
 
 		leftBtn = std::make_shared<CButton>(Point(10,  pos.h + 6), AnimationPath::builtin("hsbtns3.def"), CButton::tooltip(), [=](){ abilities->moveToPrev(); }, EShortcut::MOVE_LEFT);
 		rightBtn = std::make_shared<CButton>(Point(411, pos.h + 6), AnimationPath::builtin("hsbtns5.def"), CButton::tooltip(), [=](){ abilities->moveToNext(); }, EShortcut::MOVE_RIGHT);
@@ -896,13 +919,22 @@ void CStackWindow::setSelection(si32 newSkill, std::shared_ptr<CCommanderSkillIc
 		selectedIcon->setObject(std::make_shared<CPicture>(getSkillImage(oldSelection)));
 
 	if(selectedIcon)
-		selectedIcon->text = getSkillDescription(oldSelection, false); //update previously selected icon's message to existing skill level
+	{
+		if(!selectedIcon->getIsGrandmasterAbility()) //unlike WoG, in VCMI grandmaster skill descriptions are taken from bonus descriptions
+		{
+			selectedIcon->text = getSkillDescription(oldSelection, false); //update previously selected icon's message to existing skill level
+		}
+		selectedIcon->deselect();
+	}
 
 	selectedIcon = newIcon; // update new selection
 	if(newSkill < 100)
 	{
 		newIcon->setObject(std::make_shared<CPicture>(getSkillImage(newSkill)));
-		newIcon->text = getSkillDescription(newSkill, true); //update currently selected icon's message to show upgrade description
+		if(!newIcon->getIsGrandmasterAbility())
+		{
+			newIcon->text = getSkillDescription(newSkill, true); //update currently selected icon's message to show upgrade description
+		}
 	}
 }
 

+ 7 - 1
client/windows/CCreatureWindow.h

@@ -33,14 +33,20 @@ class CCommanderArtPlace;
 class CCommanderSkillIcon : public LRClickableAreaWText //TODO: maybe bring commander skill button initialization logic inside?
 {
 	std::shared_ptr<CIntObject> object; // passive object that will be used to determine clickable area
+	bool isGrandmasterAbility; // refers to WoG abilities obtainable via combining grandmaster skills (for example attack + speed unlocks shoot)
+	bool isSelected; // used only for programatically created border around selected "grandmaster abilities"
 public:
-	CCommanderSkillIcon(std::shared_ptr<CIntObject> object_, std::function<void()> callback);
+	CCommanderSkillIcon(std::shared_ptr<CIntObject> object_, bool isGrandmasterAbility, std::function<void()> callback);
 
 	std::function<void()> callback;
 
 	void clickPressed(const Point & cursorPosition) override;
 
 	void setObject(std::shared_ptr<CIntObject> object);
+	void deselect(); //TODO: consider using observer pattern instead?
+	bool getIsGrandmasterAbility();
+
+	void show(Canvas &to) override;
 };
 
 class CStackWindow : public CWindowObject

+ 11 - 7
client/windows/CHeroBackpackWindow.cpp

@@ -25,21 +25,25 @@ CHeroBackpackWindow::CHeroBackpackWindow(const CGHeroInstance * hero)
 	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
 
 	stretchedBackground = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, 410, 425));
-	pos.w = stretchedBackground->pos.w;
-	pos.h = stretchedBackground->pos.h;
-	center();
 
-
-	arts = std::make_shared<CArtifactsOfHeroBackpack>(/*Point(-100, -170)*/Point(10, 10));
+	arts = std::make_shared<CArtifactsOfHeroBackpack>(Point(windowMargin, windowMargin));
 	arts->setHero(hero);
 	addSet(arts);
 
 	addCloseCallback(std::bind(&CHeroBackpackWindow::close, this));
 
-	quitButton = std::make_shared<CButton>(Point(173, 385), AnimationPath::builtin("IOKAY32.def"), CButton::tooltip(""), [this]() { close(); }, EShortcut::GLOBAL_RETURN);
+	quitButton = std::make_shared<CButton>(Point(), AnimationPath::builtin("IOKAY32.def"), CButton::tooltip(""), [this]() { close(); }, EShortcut::GLOBAL_RETURN);
+
+	stretchedBackground->pos.w = arts->pos.w + 2 * windowMargin;
+	stretchedBackground->pos.h = arts->pos.h + quitButton->pos.h + 3 * windowMargin;
+	pos.w = stretchedBackground->pos.w;
+	pos.h = stretchedBackground->pos.h;
+	center();
+
+	quitButton->moveBy(Point(GH.screenDimensions().x / 2 - quitButton->pos.w / 2 - quitButton->pos.x, arts->pos.h + 2 * windowMargin));
 }
 
-void CHeroBackpackWindow::showAll(Canvas &to)
+void CHeroBackpackWindow::showAll(Canvas & to)
 {
 	CIntObject::showAll(to);
 	CMessage::drawBorder(PlayerColor(LOCPLINT->playerID), to.getInternalSurface(), pos.w+28, pos.h+29, pos.x-14, pos.y-15);

+ 2 - 1
client/windows/CHeroBackpackWindow.h

@@ -23,6 +23,7 @@ private:
 	std::shared_ptr<CArtifactsOfHeroBackpack> arts;
 	std::shared_ptr<CButton> quitButton;
 	std::shared_ptr<CFilledTexture> stretchedBackground;
+	const int windowMargin = 10;
 
-	void showAll(Canvas &to) override;
+	void showAll(Canvas & to) override;
 };

+ 1 - 0
client/windows/CHeroWindow.cpp

@@ -89,6 +89,7 @@ CHeroWindow::CHeroWindow(const CGHeroInstance * hero)
 	{
 		questlogButton = std::make_shared<CButton>(Point(314, 429), AnimationPath::builtin("hsbtns4.def"), CButton::tooltip(heroscrn[0]), [=](){ LOCPLINT->showQuestLog(); }, EShortcut::ADVENTURE_QUEST_LOG);
 		backpackButton = std::make_shared<CButton>(Point(424, 429), AnimationPath::builtin("buttons/backpack"), CButton::tooltipLocalized("vcmi.heroWindow.Backpack"), [=](){ createBackpackWindow(); }, EShortcut::HERO_BACKPACK);
+		backpackButton->addOverlay(std::make_shared<CPicture>(ImagePath::builtin("buttons/backpackButtonIcon")));
 		dismissButton = std::make_shared<CButton>(Point(534, 429), AnimationPath::builtin("hsbtns2.def"), CButton::tooltip(heroscrn[28]), [=](){ dismissCurrent(); }, EShortcut::HERO_DISMISS);
 	}
 	else

+ 16 - 3
client/windows/InfoWindows.cpp

@@ -35,6 +35,7 @@
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CondSh.h"
 #include "../../lib/CGeneralTextHandler.h" //for Unicode related stuff
+#include "../../lib/mapObjects/CGCreature.h"
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/mapObjects/MiscObjects.h"
@@ -243,6 +244,9 @@ CInfoPopup::CInfoPopup(SDL_Surface * Bitmap, const Point &p, ETextAlignment alig
 	case ETextAlignment::TOPLEFT:
 		init(p.x, p.y);
 		break;
+	case ETextAlignment::TOPCENTER:
+		init(p.x - Bitmap->w/2, p.y);
+		break;
 	default:
 		assert(0); //not implemented
 	}
@@ -333,7 +337,7 @@ void CRClickPopup::createAndPush(const std::string & txt, std::shared_ptr<CCompo
 
 void CRClickPopup::createAndPush(const CGObjectInstance * obj, const Point & p, ETextAlignment alignment)
 {
-	auto iWin = createInfoWin(p, obj); //try get custom infowindow for this obj
+	auto iWin = createCustomInfoWindow(p, obj); //try get custom infowindow for this obj
 	if(iWin)
 	{
 		GH.windows().pushWindow(iWin);
@@ -401,14 +405,21 @@ CInfoBoxPopup::CInfoBoxPopup(Point position, const CGGarrison * garr)
 	tooltip = std::make_shared<CArmyTooltip>(Point(9, 10), iah);
 }
 
-std::shared_ptr<WindowBase> CRClickPopup::createInfoWin(Point position, const CGObjectInstance * specific) //specific=0 => draws info about selected town/hero
+CInfoBoxPopup::CInfoBoxPopup(Point position, const CGCreature * creature)
+		: CWindowObject(RCLICK_POPUP | BORDERED, ImagePath::builtin("DIBOXBCK"), toScreen(position))
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+	tooltip = std::make_shared<CreatureTooltip>(Point(9, 10), creature);
+}
+
+std::shared_ptr<WindowBase> CRClickPopup::createCustomInfoWindow(Point position, const CGObjectInstance * specific) //specific=0 => draws info about selected town/hero
 {
 	if(nullptr == specific)
 		specific = LOCPLINT->localState->getCurrentArmy();
 
 	if(nullptr == specific)
 	{
-		logGlobal->error("createInfoWin: no object to describe");
+		logGlobal->error("createCustomInfoWindow: no object to describe");
 		return nullptr;
 	}
 
@@ -418,6 +429,8 @@ std::shared_ptr<WindowBase> CRClickPopup::createInfoWin(Point position, const CG
 		return std::make_shared<CInfoBoxPopup>(position, dynamic_cast<const CGHeroInstance *>(specific));
 	case Obj::TOWN:
 		return std::make_shared<CInfoBoxPopup>(position, dynamic_cast<const CGTownInstance *>(specific));
+	case Obj::MONSTER:
+		return std::make_shared<CInfoBoxPopup>(position, dynamic_cast<const CGCreature *>(specific));
 	case Obj::GARRISON:
 	case Obj::GARRISON2:
 		return std::make_shared<CInfoBoxPopup>(position, dynamic_cast<const CGGarrison *>(specific));

+ 5 - 3
client/windows/InfoWindows.h

@@ -19,6 +19,7 @@ class CGObjectInstance;
 class CGTownInstance;
 class CGHeroInstance;
 class CGGarrison;
+class CGCreature;
 class Rect;
 
 VCMI_LIB_NAMESPACE_END
@@ -81,7 +82,7 @@ public:
 	virtual void close();
 	bool isPopupWindow() const override;
 
-	static std::shared_ptr<WindowBase> createInfoWin(Point position, const CGObjectInstance * specific);
+	static std::shared_ptr<WindowBase> createCustomInfoWindow(Point position, const CGObjectInstance * specific);
 	static void createAndPush(const std::string & txt, const CInfoWindow::TCompsInfo &comps = CInfoWindow::TCompsInfo());
 	static void createAndPush(const std::string & txt, std::shared_ptr<CComponent> component);
 	static void createAndPush(const CGObjectInstance * obj, const Point & p, ETextAlignment alignment = ETextAlignment::BOTTOMRIGHT);
@@ -111,15 +112,16 @@ public:
 	~CInfoPopup();
 };
 
-/// popup on adventure map for town\hero objects
+/// popup on adventure map for town\hero and other objects with customized popup content
 class CInfoBoxPopup : public CWindowObject
 {
-	std::shared_ptr<CArmyTooltip> tooltip;
+	std::shared_ptr<CIntObject> tooltip;
 	Point toScreen(Point pos);
 public:
 	CInfoBoxPopup(Point position, const CGTownInstance * town);
 	CInfoBoxPopup(Point position, const CGHeroInstance * hero);
 	CInfoBoxPopup(Point position, const CGGarrison * garr);
+	CInfoBoxPopup(Point position, const CGCreature * creature);
 };
 
 /// component selection window

+ 5 - 1
config/schemas/settings.json

@@ -214,11 +214,15 @@
 			"type" : "object",
 			"additionalProperties" : false,
 			"default" : {},
-			"required" : [ "radialWheelGarrisonSwipe" ],
+			"required" : [ "radialWheelGarrisonSwipe", "touchToleranceDistance" ],
 			"properties" : {
 				"radialWheelGarrisonSwipe" : {
 					"type" : "boolean",
 					"default" : true
+				},
+				"touchToleranceDistance" : {
+					"type" : "number",
+					"default" : 20
 				}
 			}
 		},

+ 0 - 7
launcher/modManager/cmodlistmodel_moc.cpp

@@ -192,13 +192,6 @@ void CModListModel::resetRepositories()
 	endResetModel();
 }
 
-void CModListModel::addRepository(QVariantMap data)
-{
-	beginResetModel();
-	CModList::addRepository(data);
-	endResetModel();
-}
-
 void CModListModel::modChanged(QString modID)
 {
 	int index = modNameToID.indexOf(modID);

+ 0 - 1
launcher/modManager/cmodlistmodel_moc.h

@@ -61,7 +61,6 @@ public:
 	/// CModListContainer overrides
 	void resetRepositories() override;
 	void reloadRepositories() override;
-	void addRepository(QVariantMap data) override;
 	void modChanged(QString modID) override;
 
 	QVariant data(const QModelIndex & index, int role) const override;

+ 11 - 7
launcher/modManager/cmodlistview_moc.cpp

@@ -666,6 +666,7 @@ void CModListView::installFiles(QStringList files)
 {
 	QStringList mods;
 	QStringList images;
+	QVector<QVariantMap> repositories;
 
 	// TODO: some better way to separate zip's with mods and downloaded repository files
 	for(QString filename : files)
@@ -675,12 +676,12 @@ void CModListView::installFiles(QStringList files)
 		if(filename.endsWith(".json"))
 		{
 			//download and merge additional files
-			auto repodata = JsonUtils::JsonFromFile(filename).toMap();
-			if(repodata.value("name").isNull())
+			auto repoData = JsonUtils::JsonFromFile(filename).toMap();
+			if(repoData.value("name").isNull())
 			{
-				for(const auto & key : repodata.keys())
+				for(const auto & key : repoData.keys())
 				{
-					auto modjson = repodata[key].toMap().value("mod");
+					auto modjson = repoData[key].toMap().value("mod");
 					if(!modjson.isNull())
 					{
 						downloadFile(key + ".json", modjson.toString(), "repository index");
@@ -691,14 +692,17 @@ void CModListView::installFiles(QStringList files)
 			{
 				auto modn = QFileInfo(filename).baseName();
 				QVariantMap temp;
-				temp[modn] = repodata;
-				repodata = temp;
+				temp[modn] = repoData;
+				repoData = temp;
 			}
-			manager->loadRepository(repodata);
+			repositories.push_back(repoData);
 		}
 		if(filename.endsWith(".png"))
 			images.push_back(filename);
 	}
+
+	manager->loadRepositories(repositories);
+
 	if(!mods.empty())
 		installMods(mods);
 

+ 4 - 2
launcher/modManager/cmodmanager.cpp

@@ -70,9 +70,11 @@ void CModManager::resetRepositories()
 	modList->resetRepositories();
 }
 
-void CModManager::loadRepository(QVariantMap repomap)
+void CModManager::loadRepositories(QVector<QVariantMap> repomap)
 {
-	modList->addRepository(repomap);
+	for (auto const & entry : repomap)
+		modList->addRepository(entry);
+	modList->reloadRepositories();
 }
 
 void CModManager::loadMods()

+ 1 - 1
launcher/modManager/cmodmanager.h

@@ -35,7 +35,7 @@ public:
 	CModManager(CModList * modList);
 
 	void resetRepositories();
-	void loadRepository(QVariantMap repomap);
+	void loadRepositories(QVector<QVariantMap> repomap);
 	void loadModSettings();
 	void loadMods();
 

+ 8 - 0
lib/Rect.cpp

@@ -136,4 +136,12 @@ Rect Rect::intersect(const Rect & other) const
 	}
 }
 
+int Rect::distanceTo(const Point & target) const
+{
+	int distanceX = std::max({left() - target.x, 0, target.x - right()});
+	int distanceY = std::max({top() - target.y, 0, target.y - bottom()});
+
+	return Point(distanceX, distanceY).length();
+}
+
 VCMI_LIB_NAMESPACE_END

+ 3 - 0
lib/Rect.h

@@ -142,6 +142,9 @@ public:
 		return x == other.x && y == other.y && w == other.w && h == other.h;
 	}
 
+	/// returns distance from this rect to point, or 0 if inside
+	DLL_LINKAGE int distanceTo(const Point & target) const;
+
 	/// returns true if this rect intersects with another rect
 	DLL_LINKAGE bool intersectionTest(const Rect & other) const;
 

+ 1 - 1
lib/mapObjects/CGCreature.cpp

@@ -53,7 +53,7 @@ std::string CGCreature::getHoverText(const CGHeroInstance * hero) const
 		ms.appendRawString(" ");
 		ms.appendLocalString(EMetaText::CRE_PL_NAMES,subID);
 
-		ms.appendRawString("\n");
+		ms.appendRawString("\n\n");
 
 		int decision = takenAction(hero, true);
 

+ 2 - 1
lib/mapObjects/MiscObjects.h

@@ -196,6 +196,8 @@ public:
 	GameResID producedResource;
 	ui32 producedQuantity;
 	std::set<GameResID> abandonedMineResources;
+	
+	bool isAbandoned() const;
 
 private:
 	void onHeroVisit(const CGHeroInstance * h) const override;
@@ -209,7 +211,6 @@ private:
 	std::string getObjectName() const override;
 	std::string getHoverText(PlayerColor player) const override;
 
-	bool isAbandoned() const;
 public:
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{

+ 3 - 5
lib/rewardable/Interface.cpp

@@ -140,13 +140,11 @@ void Rewardable::Interface::grantRewardAfterLevelup(IGameCallback * cb, const Re
 		caster.setActualCaster(hero);
 		caster.setSpellSchoolLevel(info.reward.spellCast.second);
 		cb->castSpell(&caster, info.reward.spellCast.first, int3{-1, -1, -1});
-		
-		if(info.reward.removeObject)
-			logMod->warn("Removal of object with spell casts is not supported!");
 	}
-	else if(info.reward.removeObject) //FIXME: object can't track spell cancel or finish, so removeObject leads to crash
+
+	if(info.reward.removeObject)
 		if(auto * instance = dynamic_cast<const CGObjectInstance*>(this))
-			cb->removeObject(instance);
+			cb->removeAfterVisit(instance);
 }
 
 VCMI_LIB_NAMESPACE_END

+ 1 - 1
lib/spells/effects/Teleport.cpp

@@ -49,7 +49,7 @@ void Teleport::adjustTargetTypes(std::vector<TargetType> & types) const
 bool Teleport::applicable(Problem & problem, const Mechanics * m, const EffectTarget & target) const
 {
 	if(target.size() == 1) //Assume, this is check only for selecting a unit
-		return UnitEffect::applicable(problem, m);
+		return UnitEffect::applicable(problem, m, target);
 
 	if(target.size() != 2)
 		return m->adaptProblem(ESpellCastProblem::WRONG_SPELL_TARGET, problem);

+ 13 - 2
mapeditor/inspector/inspector.cpp

@@ -118,7 +118,10 @@ void Initializer::initialize(CGHeroInstance * o)
 	
 	o->tempOwner = defaultPlayer;
 	if(o->ID == Obj::PRISON)
+	{
+		o->subID = 0;
 		o->tempOwner = PlayerColor::NEUTRAL;
+	}
 	
 	if(o->ID == Obj::HERO)
 	{
@@ -184,8 +187,16 @@ void Initializer::initialize(CGMine * o)
 	if(!o) return;
 	
 	o->tempOwner = defaultPlayer;
-	o->producedResource = GameResID(o->subID);
-	o->producedQuantity = o->defaultResProduction();
+	if(o->isAbandoned())
+	{
+		for(auto r = 0; r < GameConstants::RESOURCE_QUANTITY - 1; ++r)
+			o->abandonedMineResources.insert(GameResID(r));
+	}
+	else
+	{
+		o->producedResource = GameResID(o->subID);
+		o->producedQuantity = o->defaultResProduction();
+	}
 }
 
 void Initializer::initialize(CGResource * o)

+ 1 - 0
mapeditor/mapcontroller.cpp

@@ -142,6 +142,7 @@ void MapController::repairMap()
 			{
 				nih->typeName = "prison";
 				nih->subTypeName = "prison";
+				nih->subID = 0;
 			}
 			
 			nih->type = type;

+ 5 - 0
server/battles/BattleFlowProcessor.cpp

@@ -177,6 +177,11 @@ void BattleFlowProcessor::trySummonGuardians(const CBattleInfoCallback & battle,
 			gameHandler->sendAndApply(&pack);
 		}
 	}
+
+	// send empty event to client
+	// temporary(?) workaround to force animations to trigger
+	StacksInjured fakeEvent;
+	gameHandler->sendAndApply(&fakeEvent);
 }
 
 void BattleFlowProcessor::castOpeningSpells(const CBattleInfoCallback & battle)