Quellcode durchsuchen

Merge pull request #5817 from IvanSavenko/unit_spellbook

Integrated alternative actions & unit spellbook support
Ivan Savenko vor 4 Monaten
Ursprung
Commit
27c64528b9
60 geänderte Dateien mit 2208 neuen und 1715 gelöschten Zeilen
  1. BIN
      Mods/vcmi/Content/Sprites/battle/actionAttack.png
  2. BIN
      Mods/vcmi/Content/Sprites/battle/actionGenie.png
  3. BIN
      Mods/vcmi/Content/Sprites/battle/actionInfo.png
  4. BIN
      Mods/vcmi/Content/Sprites/battle/actionMove.png
  5. BIN
      Mods/vcmi/Content/Sprites/battle/actionReturn.png
  6. BIN
      Mods/vcmi/Content/Sprites/battle/actionShoot.png
  7. BIN
      Mods/vcmi/Content/Sprites/battle/queueWait.png
  8. BIN
      Mods/vcmi/Content/Sprites2x/battle/actionAttack.png
  9. BIN
      Mods/vcmi/Content/Sprites2x/battle/actionGenie.png
  10. BIN
      Mods/vcmi/Content/Sprites2x/battle/actionInfo.png
  11. BIN
      Mods/vcmi/Content/Sprites2x/battle/actionMove.png
  12. BIN
      Mods/vcmi/Content/Sprites2x/battle/actionReturn.png
  13. BIN
      Mods/vcmi/Content/Sprites2x/battle/actionShoot.png
  14. BIN
      Mods/vcmi/Content/Sprites2x/battle/queueWait.png
  15. 7 0
      Mods/vcmi/Content/config/english.json
  16. 9 0
      Mods/vcmi/Content/config/ukrainian.json
  17. 18 5
      client/CMakeLists.txt
  18. 1 1
      client/CPlayerInterface.cpp
  19. 14 42
      client/battle/BattleActionsController.cpp
  20. 2 3
      client/battle/BattleActionsController.h
  21. 4 4
      client/battle/BattleAnimationClasses.cpp
  22. 215 0
      client/battle/BattleConsole.cpp
  23. 79 0
      client/battle/BattleConsole.h
  24. 4 5
      client/battle/BattleEffectsController.cpp
  25. 10 10
      client/battle/BattleFieldController.cpp
  26. 206 0
      client/battle/BattleHero.cpp
  27. 66 0
      client/battle/BattleHero.h
  28. 12 11
      client/battle/BattleInterface.cpp
  29. 0 1187
      client/battle/BattleInterfaceClasses.cpp
  30. 0 293
      client/battle/BattleInterfaceClasses.h
  31. 5 5
      client/battle/BattleRenderer.cpp
  32. 265 0
      client/battle/BattleResultWindow.cpp
  33. 60 0
      client/battle/BattleResultWindow.h
  34. 2 3
      client/battle/BattleSiegeController.cpp
  35. 6 7
      client/battle/BattleStacksController.cpp
  36. 40 118
      client/battle/BattleWindow.cpp
  37. 4 8
      client/battle/BattleWindow.h
  38. 97 0
      client/battle/HeroInfoWindow.cpp
  39. 44 0
      client/battle/HeroInfoWindow.h
  40. 151 0
      client/battle/QuickSpellPanel.cpp
  41. 43 0
      client/battle/QuickSpellPanel.h
  42. 147 0
      client/battle/StackInfoBasicPanel.cpp
  43. 38 0
      client/battle/StackInfoBasicPanel.h
  44. 241 0
      client/battle/StackQueue.cpp
  45. 69 0
      client/battle/StackQueue.h
  46. 154 0
      client/battle/UnitActionPanel.cpp
  47. 46 0
      client/battle/UnitActionPanel.h
  48. 5 0
      client/gui/CIntObject.cpp
  49. 3 0
      client/gui/CIntObject.h
  50. 0 1
      client/gui/Shortcut.h
  51. 0 1
      client/gui/ShortcutHandler.cpp
  52. 11 1
      client/widgets/Buttons.cpp
  53. 2 0
      client/widgets/Buttons.h
  54. 0 1
      client/widgets/Images.cpp
  55. 0 1
      client/windows/CWindowObject.cpp
  56. 0 1
      config/shortcutsConfig.json
  57. 3 0
      config/spells/vcmiAbility.json
  58. 114 0
      config/widgets/buttons/battleUnitAction.json
  59. 10 6
      config/widgets/buttons/heroBackpack.json
  60. 1 1
      lib/battle/CBattleInfoCallback.cpp

BIN
Mods/vcmi/Content/Sprites/battle/actionAttack.png


BIN
Mods/vcmi/Content/Sprites/battle/actionGenie.png


BIN
Mods/vcmi/Content/Sprites/battle/actionInfo.png


BIN
Mods/vcmi/Content/Sprites/battle/actionMove.png


BIN
Mods/vcmi/Content/Sprites/battle/actionReturn.png


BIN
Mods/vcmi/Content/Sprites/battle/actionShoot.png


BIN
Mods/vcmi/Content/Sprites/battle/queueWait.png


BIN
Mods/vcmi/Content/Sprites2x/battle/actionAttack.png


BIN
Mods/vcmi/Content/Sprites2x/battle/actionGenie.png


BIN
Mods/vcmi/Content/Sprites2x/battle/actionInfo.png


BIN
Mods/vcmi/Content/Sprites2x/battle/actionMove.png


BIN
Mods/vcmi/Content/Sprites2x/battle/actionReturn.png


BIN
Mods/vcmi/Content/Sprites2x/battle/actionShoot.png


BIN
Mods/vcmi/Content/Sprites2x/battle/queueWait.png


+ 7 - 0
Mods/vcmi/Content/config/english.json

@@ -32,6 +32,13 @@
 	"vcmi.adventureMap.dwelling3" : "{%s}\n\nWould you like to recruit %s, %s, or %s?",
 	
 	"vcmi.artifact.charges" : "Charges",
+	
+	"vcmi.battle.action.move" : "Move unit to specified location",
+	"vcmi.battle.action.info" : "Show unit information",
+	"vcmi.battle.action.shoot" : "Use ranged attack",
+	"vcmi.battle.action.attack" : "Use melee attack",
+	"vcmi.battle.action.return" : "Use melee attack and return",
+	"vcmi.battle.action.genie" : "Cast random beneficial spell",
 
 	"vcmi.bonusSource.artifact" : "Artifact",
 	"vcmi.bonusSource.creature" : "Ability",

+ 9 - 0
Mods/vcmi/Content/config/ukrainian.json

@@ -28,6 +28,15 @@
 	"vcmi.adventureMap.movementPointsHeroInfo" : "(Очки руху: %REMAINING / %POINTS)",
 	"vcmi.adventureMap.replayOpponentTurnNotImplemented" : "Вибачте, функція повтору ходу суперника ще не реалізована!",
 
+	"vcmi.artifact.charges" : "Заряди",
+	
+	"vcmi.battle.action.move" : "Перемістити загін у вказане місце",
+	"vcmi.battle.action.info" : "Показати інформацію про загін",
+	"vcmi.battle.action.shoot" : "Застосовувати дистанційну атаку",
+	"vcmi.battle.action.attack" : "Застосувати атаку в ближньому бою",
+	"vcmi.battle.action.return" : "Застосовувати атаку ближнього бою та повернутися",
+	"vcmi.battle.action.genie" : "Накласти випадкове корисне закляття",
+
 	"vcmi.bonusSource.artifact" : "Артифакт",
 	"vcmi.bonusSource.creature" : "Здібність",
 	"vcmi.bonusSource.spell" : "Закляття",

+ 18 - 5
client/CMakeLists.txt

@@ -15,18 +15,25 @@ set(vcmiclientcommon_SRCS
 
 	battle/BattleActionsController.cpp
 	battle/BattleAnimationClasses.cpp
+	battle/BattleConsole.cpp
 	battle/BattleEffectsController.cpp
 	battle/BattleFieldController.cpp
+	battle/BattleHero.cpp
 	battle/BattleInterface.cpp
-	battle/BattleInterfaceClasses.cpp
 	battle/BattleObstacleController.cpp
+	battle/BattleOverlayLogVisualizer.cpp
 	battle/BattleProjectileController.cpp
 	battle/BattleRenderer.cpp
+	battle/BattleResultWindow.cpp
 	battle/BattleSiegeController.cpp
 	battle/BattleStacksController.cpp
 	battle/BattleWindow.cpp
 	battle/CreatureAnimation.cpp
-	battle/BattleOverlayLogVisualizer.cpp
+	battle/HeroInfoWindow.cpp
+	battle/QuickSpellPanel.cpp
+	battle/StackInfoBasicPanel.cpp
+	battle/StackQueue.cpp
+	battle/UnitActionPanel.cpp
 
 	eventsSDL/NotificationHandler.cpp
 	eventsSDL/InputHandler.cpp
@@ -211,19 +218,25 @@ set(vcmiclientcommon_HEADERS
 
 	battle/BattleActionsController.h
 	battle/BattleAnimationClasses.h
-	battle/BattleConstants.h
+	battle/BattleConsole.h
 	battle/BattleEffectsController.h
 	battle/BattleFieldController.h
+	battle/BattleHero.h
 	battle/BattleInterface.h
-	battle/BattleInterfaceClasses.h
 	battle/BattleObstacleController.h
+	battle/BattleOverlayLogVisualizer.h
 	battle/BattleProjectileController.h
 	battle/BattleRenderer.h
+	battle/BattleResultWindow.h
 	battle/BattleSiegeController.h
 	battle/BattleStacksController.h
 	battle/BattleWindow.h
 	battle/CreatureAnimation.h
-	battle/BattleOverlayLogVisualizer.h
+	battle/HeroInfoWindow.h
+	battle/QuickSpellPanel.h
+	battle/StackInfoBasicPanel.h
+	battle/StackQueue.h
+	battle/UnitActionPanel.h
 
 	eventsSDL/NotificationHandler.h
 	eventsSDL/InputHandler.h

+ 1 - 1
client/CPlayerInterface.cpp

@@ -24,7 +24,7 @@
 #include "battle/BattleEffectsController.h"
 #include "battle/BattleFieldController.h"
 #include "battle/BattleInterface.h"
-#include "battle/BattleInterfaceClasses.h"
+#include "battle/BattleResultWindow.h"
 #include "battle/BattleWindow.h"
 
 #include "eventsSDL/InputHandler.h"

+ 14 - 42
client/battle/BattleActionsController.cpp

@@ -10,33 +10,33 @@
 #include "StdInc.h"
 #include "BattleActionsController.h"
 
-#include "BattleWindow.h"
-#include "BattleStacksController.h"
-#include "BattleInterface.h"
 #include "BattleFieldController.h"
+#include "BattleHero.h"
+#include "BattleInterface.h"
 #include "BattleSiegeController.h"
-#include "BattleInterfaceClasses.h"
+#include "BattleStacksController.h"
+#include "BattleWindow.h"
 
 #include "../CPlayerInterface.h"
-#include "../gui/CursorHandler.h"
 #include "../GameEngine.h"
 #include "../GameInstance.h"
 #include "../gui/CIntObject.h"
+#include "../gui/CursorHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../windows/CCreatureWindow.h"
 #include "../windows/InfoWindows.h"
 
 #include "../../lib/CConfigHandler.h"
-#include "../../lib/GameLibrary.h"
-#include "../../lib/texts/CGeneralTextHandler.h"
 #include "../../lib/CRandomGenerator.h"
 #include "../../lib/CStack.h"
+#include "../../lib/GameLibrary.h"
 #include "../../lib/battle/BattleAction.h"
 #include "../../lib/battle/CPlayerBattleCallback.h"
 #include "../../lib/callback/CCallback.h"
 #include "../../lib/spells/CSpellHandler.h"
 #include "../../lib/spells/ISpellMechanics.h"
 #include "../../lib/spells/Problem.h"
+#include "../../lib/texts/CGeneralTextHandler.h"
 
 struct TextReplacement
 {
@@ -148,7 +148,10 @@ void BattleActionsController::endCastingSpell()
 	}
 
 	if(owner.stacksController->getActiveStack())
+	{
 		possibleActions = getPossibleActionsForStack(owner.stacksController->getActiveStack()); //restore actions after they were cleared
+		owner.windowObject->setPossibleActions(possibleActions);
+	}
 
 	selectedStack = nullptr;
 	ENGINE->fakeMouseMove();
@@ -624,7 +627,7 @@ bool BattleActionsController::actionIsLegal(PossiblePlayerBattleAction action, c
 			return (targetStack && targetStackOwned && targetStack->getMovementRange() > 0);
 
 		case PossiblePlayerBattleAction::CREATURE_INFO:
-			return (targetStack && targetStackOwned && targetStack->alive());
+			return (targetStack && targetStack->alive());
 
 		case PossiblePlayerBattleAction::HERO_INFO:
 			if (targetHex == BattleHex::HERO_ATTACKER)
@@ -1037,33 +1040,7 @@ void BattleActionsController::activateStack()
 		tryActivateStackSpellcasting(s);
 
 		possibleActions = getPossibleActionsForStack(s);
-		std::list<PossiblePlayerBattleAction> actionsToSelect;
-		if(!possibleActions.empty())
-		{
-			auto primaryAction = possibleActions.front().get();
-
-			if(primaryAction == PossiblePlayerBattleAction::SHOOT || primaryAction == PossiblePlayerBattleAction::AIMED_SPELL_CREATURE
-				|| primaryAction == PossiblePlayerBattleAction::ANY_LOCATION || primaryAction == PossiblePlayerBattleAction::ATTACK_AND_RETURN)
-			{
-				actionsToSelect.push_back(possibleActions.front());
-
-				auto shootActionPredicate = [](const PossiblePlayerBattleAction& action)
-				{
-					return action.get() == PossiblePlayerBattleAction::SHOOT;
-				};
-				bool hasShootSecondaryAction = std::any_of(possibleActions.begin() + 1, possibleActions.end(), shootActionPredicate);
-
-				if(hasShootSecondaryAction) //casters may have shooting capabilities, for example storm elementals
-					actionsToSelect.emplace_back(PossiblePlayerBattleAction::SHOOT);
-
-				/* TODO: maybe it would also make sense to check spellcast as non-top priority action ("NO_SPELLCAST_BY_DEFAULT" bonus)?
-				 * it would require going beyond this "if" block for melee casters
-				 * F button helps, but some mod creatures may have that bonus and more than 1 castable spell */
-
-				actionsToSelect.emplace_back(PossiblePlayerBattleAction::ATTACK); //always allow melee attack as last option
-			}
-		}
-		owner.windowObject->setAlternativeActions(actionsToSelect);
+		owner.windowObject->setPossibleActions(possibleActions);
 	}
 }
 
@@ -1123,14 +1100,9 @@ const std::vector<PossiblePlayerBattleAction> & BattleActionsController::getPoss
 	return possibleActions;
 }
 
-void BattleActionsController::removePossibleAction(PossiblePlayerBattleAction action)
-{
-	vstd::erase(possibleActions, action);
-}
-
-void BattleActionsController::pushFrontPossibleAction(PossiblePlayerBattleAction action)
+void BattleActionsController::setPriorityActions(const std::vector<PossiblePlayerBattleAction> & actions)
 {
-	possibleActions.insert(possibleActions.begin(), action);
+	possibleActions = actions;
 }
 
 void BattleActionsController::resetCurrentStackPossibleActions()

+ 2 - 3
client/battle/BattleActionsController.h

@@ -120,10 +120,9 @@ public:
 
 	/// methods to work with array of possible actions, needed to control special creatures abilities
 	const std::vector<PossiblePlayerBattleAction> & getPossibleActions() const;
-	void removePossibleAction(PossiblePlayerBattleAction);
 	
-	/// inserts possible action in the beginning in order to prioritize it
-	void pushFrontPossibleAction(PossiblePlayerBattleAction);
+	/// sets list of high-priority actions that should be selected before any other actions
+	void setPriorityActions(const std::vector<PossiblePlayerBattleAction> &);
 
 	/// resets possible actions to original state
 	void resetCurrentStackPossibleActions();

+ 4 - 4
client/battle/BattleAnimationClasses.cpp

@@ -10,18 +10,18 @@
 #include "StdInc.h"
 #include "BattleAnimationClasses.h"
 
+#include "BattleEffectsController.h"
+#include "BattleFieldController.h"
+#include "BattleHero.h"
 #include "BattleInterface.h"
-#include "BattleInterfaceClasses.h"
 #include "BattleProjectileController.h"
 #include "BattleSiegeController.h"
-#include "BattleFieldController.h"
-#include "BattleEffectsController.h"
 #include "BattleStacksController.h"
 #include "CreatureAnimation.h"
 
 #include "../CPlayerInterface.h"
-#include "../gui/CursorHandler.h"
 #include "../GameEngine.h"
+#include "../gui/CursorHandler.h"
 #include "../media/ISoundPlayer.h"
 #include "../render/CAnimation.h"
 #include "../render/IRenderHandler.h"

+ 215 - 0
client/battle/BattleConsole.cpp

@@ -0,0 +1,215 @@
+/*
+ * BattleConsole.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 "BattleConsole.h"
+
+#include "BattleInterface.h"
+
+#include "../CPlayerInterface.h"
+#include "../GameEngine.h"
+#include "../GameInstance.h"
+#include "../adventureMap/CInGameConsole.h"
+#include "../eventsSDL/InputHandler.h"
+#include "../gui/Shortcut.h"
+#include "../gui/WindowHandler.h"
+#include "../render/Canvas.h"
+#include "../render/IFont.h"
+#include "../render/IRenderHandler.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../widgets/Images.h"
+#include "../widgets/Slider.h"
+#include "../widgets/TextControls.h"
+#include "../windows/CMessage.h"
+
+BattleConsoleWindow::BattleConsoleWindow(const std::string & text)
+	: CWindowObject(BORDERED)
+{
+	OBJECT_CONSTRUCTION;
+
+	pos.w = 429;
+	pos.h = 434;
+
+	updateShadow();
+	center();
+
+	backgroundTexture = std::make_shared<CFilledTexture>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
+	buttonOk = std::make_shared<CButton>(Point(183, 388), AnimationPath::builtin("IOKAY"), CButton::tooltip(), [this](){ close(); }, EShortcut::GLOBAL_ACCEPT);
+	Rect textArea(18, 17, 393, 354);
+	textBoxBackgroundBorder = std::make_shared<TransparentFilledRectangle>(textArea, ColorRGBA(0, 0, 0, 75), ColorRGBA(128, 100, 75));
+	textBox = std::make_shared<CTextBox>(text, textArea.resize(-5), CSlider::BROWN);
+	if(textBox->slider)
+		textBox->slider->scrollToMax();
+}
+
+void BattleConsole::showAll(Canvas & to)
+{
+	CIntObject::showAll(to);
+
+	Point line1(pos.x + pos.w / 2, pos.y + 8);
+	Point line2(pos.x + pos.w / 2, pos.y + 24);
+
+	auto visibleText = getVisibleText();
+
+	if(visibleText.size() > 0)
+		to.drawText(line1, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[0]);
+
+	if(visibleText.size() > 1)
+		to.drawText(line2, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[1]);
+}
+
+std::vector<std::string> BattleConsole::getVisibleText() const
+{
+	// high priority texts that hide battle log entries
+	for(const auto & text : {consoleText, hoverText})
+	{
+		if(text.empty())
+			continue;
+
+		auto result = CMessage::breakText(text, pos.w, FONT_SMALL);
+
+		if(result.size() > 2 && text.find('\n') != std::string::npos)
+		{
+			// Text has too many lines to fit into console, but has line breaks. Try ignore them and fit text that way
+			std::string cleanText = boost::algorithm::replace_all_copy(text, "\n", " ");
+			result = CMessage::breakText(cleanText, pos.w, FONT_SMALL);
+		}
+
+		if(result.size() > 2)
+			result.resize(2);
+		return result;
+	}
+
+	// log is small enough to fit entirely - display it as such
+	if(logEntries.size() < 3)
+		return logEntries;
+
+	return {logEntries[scrollPosition - 1], logEntries[scrollPosition]};
+}
+
+std::vector<std::string> BattleConsole::splitText(const std::string & text) const
+{
+	std::vector<std::string> lines;
+	std::vector<std::string> output;
+
+	boost::split(lines, text, boost::is_any_of("\n"));
+
+	const auto & font = ENGINE->renderHandler().loadFont(FONT_SMALL);
+	for(const auto & line : lines)
+	{
+		if(font->getStringWidth(text) < pos.w)
+		{
+			output.push_back(line);
+		}
+		else
+		{
+			std::vector<std::string> substrings = CMessage::breakText(line, pos.w, FONT_SMALL);
+			output.insert(output.end(), substrings.begin(), substrings.end());
+		}
+	}
+	return output;
+}
+
+bool BattleConsole::addText(const std::string & text)
+{
+	logGlobal->trace("CBattleConsole message: %s", text);
+
+	auto newLines = splitText(text);
+
+	logEntries.insert(logEntries.end(), newLines.begin(), newLines.end());
+	scrollPosition = static_cast<int>(logEntries.size()) - 1;
+	redraw();
+	return true;
+}
+void BattleConsole::scrollUp(ui32 by)
+{
+	if(scrollPosition > static_cast<int>(by))
+		scrollPosition -= by;
+	redraw();
+}
+
+void BattleConsole::scrollDown(ui32 by)
+{
+	if(scrollPosition + by < logEntries.size())
+		scrollPosition += by;
+	redraw();
+}
+
+BattleConsole::BattleConsole(const BattleInterface & owner, const std::shared_ptr<CPicture> & backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size)
+	: CIntObject(LCLICK)
+	, owner(owner)
+	, scrollPosition(-1)
+	, enteringText(false)
+{
+	OBJECT_CONSTRUCTION;
+	pos += objectPos;
+	pos.w = size.x;
+	pos.h = size.y;
+
+	background = std::make_shared<CPicture>(backgroundSource->getSurface(), Rect(imagePos, size), 0, 0);
+}
+
+void BattleConsole::deactivate()
+{
+	if(enteringText)
+		GAME->interface()->cingconsole->endEnteringText(false);
+
+	CIntObject::deactivate();
+}
+
+void BattleConsole::clickPressed(const Point & cursorPosition)
+{
+	if(owner.makingTurn() && !owner.openingPlaying())
+	{
+		ENGINE->windows().createAndPushWindow<BattleConsoleWindow>(boost::algorithm::join(logEntries, "\n"));
+	}
+}
+
+void BattleConsole::setEnteringMode(bool on)
+{
+	consoleText.clear();
+
+	if(on)
+	{
+		assert(enteringText == false);
+		ENGINE->input().startTextInput(pos);
+	}
+	else
+	{
+		assert(enteringText == true);
+		ENGINE->input().stopTextInput();
+	}
+	enteringText = on;
+	redraw();
+}
+
+void BattleConsole::setEnteredText(const std::string & text)
+{
+	assert(enteringText == true);
+	consoleText = text;
+	redraw();
+}
+
+void BattleConsole::write(const std::string & Text)
+{
+	hoverText = Text;
+	redraw();
+}
+
+void BattleConsole::clearIfMatching(const std::string & Text)
+{
+	if(hoverText == Text)
+		clear();
+}
+
+void BattleConsole::clear()
+{
+	write({});
+}

+ 79 - 0
client/battle/BattleConsole.h

@@ -0,0 +1,79 @@
+/*
+ * BattleConsole.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 "../windows/CWindowObject.h"
+
+class BattleInterface;
+class CFilledTexture;
+class CButton;
+class TransparentFilledRectangle;
+class CTextBox;
+
+class BattleConsoleWindow : public CWindowObject
+{
+private:
+	std::shared_ptr<CFilledTexture> backgroundTexture;
+	std::shared_ptr<CButton> buttonOk;
+	std::shared_ptr<TransparentFilledRectangle> textBoxBackgroundBorder;
+	std::shared_ptr<CTextBox> textBox;
+
+public:
+	BattleConsoleWindow(const std::string & text);
+};
+
+/// Class which shows the console at the bottom of the battle screen and manages the text of the console
+class BattleConsole : public CIntObject, public IStatusBar
+{
+private:
+	const BattleInterface & owner;
+
+	std::shared_ptr<CPicture> background;
+
+	/// List of all texts added during battle, essentially - log of entire battle
+	std::vector<std::string> logEntries;
+
+	/// Current scrolling position, to allow showing older entries via scroll buttons
+	int scrollPosition;
+
+	/// current hover text set on mouse move, takes priority over log entries
+	std::string hoverText;
+
+	/// current text entered via in-game console, takes priority over both log entries and hover text
+	std::string consoleText;
+
+	/// if true then we are currently entering console text
+	bool enteringText;
+
+	/// splits text into individual strings for battle log
+	std::vector<std::string> splitText(const std::string & text) const;
+
+	/// select line(s) that will be visible in UI
+	std::vector<std::string> getVisibleText() const;
+
+public:
+	BattleConsole(const BattleInterface & owner, const std::shared_ptr<CPicture> & backgroundSource, const Point & objectPos, const Point & imagePos, const Point & size);
+
+	void showAll(Canvas & to) override;
+	void deactivate() override;
+
+	void clickPressed(const Point & cursorPosition) override;
+
+	bool addText(const std::string & text); //adds text at the last position; returns false if failed (e.g. text longer than 70 characters)
+	void scrollUp(ui32 by = 1); //scrolls console up by 'by' positions
+	void scrollDown(ui32 by = 1); //scrolls console up by 'by' positions
+
+	// IStatusBar interface
+	void write(const std::string & Text) override;
+	void clearIfMatching(const std::string & Text) override;
+	void clear() override;
+	void setEnteringMode(bool on) override;
+	void setEnteredText(const std::string & text) override;
+};

+ 4 - 5
client/battle/BattleEffectsController.cpp

@@ -11,18 +11,17 @@
 #include "BattleEffectsController.h"
 
 #include "BattleAnimationClasses.h"
-#include "BattleWindow.h"
-#include "BattleInterface.h"
-#include "BattleInterfaceClasses.h"
 #include "BattleFieldController.h"
-#include "BattleStacksController.h"
+#include "BattleInterface.h"
 #include "BattleRenderer.h"
+#include "BattleStacksController.h"
+#include "BattleWindow.h"
 
 #include "../CPlayerInterface.h"
 #include "../GameEngine.h"
 #include "../media/ISoundPlayer.h"
-#include "../render/Canvas.h"
 #include "../render/CAnimation.h"
+#include "../render/Canvas.h"
 #include "../render/Graphics.h"
 
 #include "../../lib/CStack.h"

+ 10 - 10
client/battle/BattleFieldController.cpp

@@ -10,27 +10,27 @@
 #include "StdInc.h"
 #include "BattleFieldController.h"
 
-#include "BattleInterface.h"
-#include "BattleWindow.h"
 #include "BattleActionsController.h"
-#include "BattleInterfaceClasses.h"
 #include "BattleEffectsController.h"
-#include "BattleSiegeController.h"
-#include "BattleStacksController.h"
+#include "BattleInterface.h"
+#include "BattleHero.h"
 #include "BattleObstacleController.h"
 #include "BattleProjectileController.h"
 #include "BattleRenderer.h"
+#include "BattleSiegeController.h"
+#include "BattleStacksController.h"
+#include "BattleWindow.h"
 
 #include "../CPlayerInterface.h"
-#include "../render/CAnimation.h"
-#include "../render/Canvas.h"
-#include "../render/IImage.h"
-#include "../render/IRenderHandler.h"
 #include "../GameEngine.h"
 #include "../GameInstance.h"
-#include "../gui/CursorHandler.h"
 #include "../adventureMap/CInGameConsole.h"
 #include "../client/render/CAnimation.h"
+#include "../gui/CursorHandler.h"
+#include "../render/CAnimation.h"
+#include "../render/Canvas.h"
+#include "../render/IImage.h"
+#include "../render/IRenderHandler.h"
 
 #include "../../lib/BattleFieldHandler.h"
 #include "../../lib/CConfigHandler.h"

+ 206 - 0
client/battle/BattleHero.cpp

@@ -0,0 +1,206 @@
+/*
+ * BattleHero.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 "BattleHero.h"
+
+#include "BattleActionsController.h"
+#include "BattleFieldController.h"
+#include "BattleInterface.h"
+#include "BattleRenderer.h"
+#include "HeroInfoWindow.h"
+
+#include "../GameEngine.h"
+#include "../gui/CursorHandler.h"
+#include "../gui/WindowHandler.h"
+#include "../render/CAnimation.h"
+#include "../render/Canvas.h"
+#include "../render/IRenderHandler.h"
+#include "../windows/CSpellWindow.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/battle/CPlayerBattleCallback.h"
+#include "../../lib/entities/hero/CHero.h"
+#include "../../lib/entities/hero/CHeroClass.h"
+#include "../../lib/gameState/InfoAboutArmy.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+
+const CGHeroInstance * BattleHero::instance() const
+{
+	return hero;
+}
+
+void BattleHero::tick(uint32_t msPassed)
+{
+	size_t groupIndex = static_cast<size_t>(phase);
+
+	float timePassed = msPassed / 1000.f;
+
+	flagCurrentFrame += currentSpeed * timePassed;
+	currentFrame += currentSpeed * timePassed;
+
+	if(flagCurrentFrame >= flagAnimation->size(0))
+		flagCurrentFrame -= flagAnimation->size(0);
+
+	if(currentFrame >= animation->size(groupIndex))
+	{
+		currentFrame -= animation->size(groupIndex);
+		switchToNextPhase();
+	}
+}
+
+void BattleHero::render(Canvas & canvas)
+{
+	size_t groupIndex = static_cast<size_t>(phase);
+
+	auto flagFrame = flagAnimation->getImage(flagCurrentFrame, 0, true);
+	auto heroFrame = animation->getImage(currentFrame, groupIndex, true);
+
+	Point heroPosition = pos.center() - parent->pos.topLeft() - heroFrame->dimensions() / 2;
+	Point flagPosition = pos.center() - parent->pos.topLeft() - flagFrame->dimensions() / 2;
+
+	if(defender)
+		flagPosition += Point(-4, -41);
+	else
+		flagPosition += Point(4, -41);
+
+	canvas.draw(flagFrame, flagPosition);
+	canvas.draw(heroFrame, heroPosition);
+}
+
+void BattleHero::pause()
+{
+	currentSpeed = 0.f;
+}
+
+void BattleHero::play()
+{
+	//H3 speed: 10 fps ( 100 ms per frame)
+	currentSpeed = 10.f;
+}
+
+float BattleHero::getFrame() const
+{
+	return currentFrame;
+}
+
+void BattleHero::collectRenderableObjects(BattleRenderer & renderer)
+{
+	auto hex = defender ? BattleHex(GameConstants::BFIELD_WIDTH-1) : BattleHex(0);
+
+	renderer.insert(EBattleFieldLayer::HEROES, hex, [this](BattleRenderer::RendererRef canvas)
+	{
+		render(canvas);
+	});
+}
+
+void BattleHero::onPhaseFinished(const std::function<void()> & callback)
+{
+	phaseFinishedCallback = callback;
+}
+
+void BattleHero::setPhase(EHeroAnimType newPhase)
+{
+	nextPhase = newPhase;
+	switchToNextPhase(); //immediately switch to next phase and then restore idling phase
+	nextPhase = EHeroAnimType::HOLDING;
+}
+
+void BattleHero::heroLeftClicked()
+{
+	if(owner.actionsController->heroSpellcastingModeActive()) //we are casting a spell
+		return;
+
+	if(!hero || !owner.makingTurn())
+		return;
+
+	if(owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions
+	{
+		ENGINE->cursor().set(Cursor::Map::POINTER);
+		ENGINE->windows().createAndPushWindow<CSpellWindow>(hero, owner.getCurrentPlayerInterface());
+	}
+}
+
+void BattleHero::heroRightClicked() const
+{
+	if(settings["battle"]["stickyHeroInfoWindows"].Bool())
+		return;
+
+	Point windowPosition;
+	if(ENGINE->screenDimensions().x < 1000)
+	{
+		windowPosition.x = (!defender) ? owner.fieldController->pos.left() + 1 : owner.fieldController->pos.right() - 79;
+		windowPosition.y = owner.fieldController->pos.y + 135;
+	}
+	else
+	{
+		windowPosition.x = (!defender) ? owner.fieldController->pos.left() - 93 : owner.fieldController->pos.right() + 15;
+		windowPosition.y = owner.fieldController->pos.y;
+	}
+
+	InfoAboutHero targetHero;
+	if(owner.makingTurn() || settings["session"]["spectate"].Bool())
+	{
+		const auto * h = defender ? owner.defendingHeroInstance : owner.attackingHeroInstance;
+		targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE);
+		ENGINE->windows().createAndPushWindow<HeroInfoWindow>(targetHero, &windowPosition);
+	}
+}
+
+void BattleHero::switchToNextPhase()
+{
+	phase = nextPhase;
+	currentFrame = 0.f;
+
+	auto copy = phaseFinishedCallback;
+	phaseFinishedCallback.clear();
+	copy();
+}
+
+BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender)
+	: defender(defender)
+	, hero(hero)
+	, owner(owner)
+	, phase(EHeroAnimType::HOLDING)
+	, nextPhase(EHeroAnimType::HOLDING)
+	, currentSpeed(0.f)
+	, currentFrame(0.f)
+	, flagCurrentFrame(0.f)
+{
+	AnimationPath animationPath;
+
+	if(!hero->getHeroType()->battleImage.empty())
+		animationPath = hero->getHeroType()->battleImage;
+	else if(hero->gender == EHeroGender::FEMALE)
+		animationPath = hero->getHeroClass()->imageBattleFemale;
+	else
+		animationPath = hero->getHeroClass()->imageBattleMale;
+
+	animation = ENGINE->renderHandler().loadAnimation(animationPath, EImageBlitMode::WITH_SHADOW);
+
+	pos.w = 64;
+	pos.h = 136;
+	pos.x = owner.fieldController->pos.x + (defender ? (owner.fieldController->pos.w - pos.w) : 0);
+	pos.y = owner.fieldController->pos.y;
+
+	if(defender)
+		animation->verticalFlip();
+
+	if(defender)
+		flagAnimation = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGR"), EImageBlitMode::COLORKEY);
+	else
+		flagAnimation = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGL"), EImageBlitMode::COLORKEY);
+
+	flagAnimation->playerColored(hero->tempOwner);
+
+	switchToNextPhase();
+	play();
+
+	addUsedEvents(TIME);
+}

+ 66 - 0
client/battle/BattleHero.h

@@ -0,0 +1,66 @@
+/*
+ * BattleHero.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 "BattleConstants.h"
+
+#include "../gui/CIntObject.h"
+#include "../../lib/FunctionList.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+class CGHeroInstance;
+VCMI_LIB_NAMESPACE_END
+
+class CAnimation;
+class BattleInterface;
+class BattleRenderer;
+
+/// Hero battle animation
+class BattleHero : public CIntObject
+{
+	bool defender;
+
+	CFunctionList<void()> phaseFinishedCallback;
+
+	std::shared_ptr<CAnimation> animation;
+	std::shared_ptr<CAnimation> flagAnimation;
+
+	const CGHeroInstance * hero; //this animation's hero instance
+	const BattleInterface & owner; //battle interface to which this animation is assigned
+
+	EHeroAnimType phase; //stage of animation
+	EHeroAnimType nextPhase; //stage of animation to be set after current phase is fully displayed
+
+	float currentSpeed;
+	float currentFrame; //frame of animation
+	float flagCurrentFrame;
+
+	void switchToNextPhase();
+
+	void render(Canvas & canvas); //prints next frame of animation to to
+public:
+	const CGHeroInstance * instance() const;
+
+	void setPhase(EHeroAnimType newPhase); //sets phase of hero animation
+
+	void collectRenderableObjects(BattleRenderer & renderer);
+	void tick(uint32_t msPassed) override;
+
+	float getFrame() const;
+	void onPhaseFinished(const std::function<void()> &);
+
+	void pause();
+	void play();
+
+	void heroLeftClicked();
+	void heroRightClicked() const;
+
+	BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender);
+};

+ 12 - 11
client/battle/BattleInterface.cpp

@@ -10,30 +10,31 @@
 #include "StdInc.h"
 #include "BattleInterface.h"
 
-#include "BattleAnimationClasses.h"
 #include "BattleActionsController.h"
-#include "BattleInterfaceClasses.h"
-#include "CreatureAnimation.h"
-#include "BattleProjectileController.h"
+#include "BattleAnimationClasses.h"
+#include "BattleConsole.h"
 #include "BattleEffectsController.h"
+#include "BattleFieldController.h"
+#include "BattleHero.h"
 #include "BattleObstacleController.h"
+#include "BattleProjectileController.h"
+#include "BattleRenderer.h"
+#include "BattleResultWindow.h"
 #include "BattleSiegeController.h"
-#include "BattleFieldController.h"
-#include "BattleWindow.h"
 #include "BattleStacksController.h"
-#include "BattleRenderer.h"
+#include "BattleWindow.h"
+#include "CreatureAnimation.h"
 
 #include "../CPlayerInterface.h"
-#include "../gui/CursorHandler.h"
 #include "../GameEngine.h"
 #include "../GameInstance.h"
+#include "../adventureMap/AdventureMapInterface.h"
+#include "../gui/CursorHandler.h"
 #include "../gui/WindowHandler.h"
 #include "../media/IMusicPlayer.h"
 #include "../media/ISoundPlayer.h"
-#include "../windows/CTutorialWindow.h"
 #include "../render/Canvas.h"
-#include "../adventureMap/AdventureMapInterface.h"
-
+#include "../windows/CTutorialWindow.h"
 
 #include "../../lib/BattleFieldHandler.h"
 #include "../../lib/CConfigHandler.h"

+ 0 - 1187
client/battle/BattleInterfaceClasses.cpp

@@ -1,1187 +0,0 @@
-/*
- * BattleInterfaceClasses.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 "BattleInterfaceClasses.h"
-
-#include "BattleInterface.h"
-#include "BattleActionsController.h"
-#include "BattleRenderer.h"
-#include "BattleSiegeController.h"
-#include "BattleFieldController.h"
-#include "BattleStacksController.h"
-#include "BattleWindow.h"
-
-#include "../CPlayerInterface.h"
-#include "../gui/CursorHandler.h"
-#include "../GameEngine.h"
-#include "../GameInstance.h"
-#include "../gui/Shortcut.h"
-#include "../gui/MouseButton.h"
-#include "../gui/WindowHandler.h"
-#include "../media/IMusicPlayer.h"
-#include "../render/Canvas.h"
-#include "../render/IImage.h"
-#include "../render/IFont.h"
-#include "../render/Graphics.h"
-#include "../widgets/Buttons.h"
-#include "../widgets/CComponent.h"
-#include "../widgets/Images.h"
-#include "../widgets/Slider.h"
-#include "../widgets/TextControls.h"
-#include "../widgets/VideoWidget.h"
-#include "../widgets/GraphicalPrimitiveCanvas.h"
-#include "../windows/CMessage.h"
-#include "../windows/CCreatureWindow.h"
-#include "../windows/CSpellWindow.h"
-#include "../windows/InfoWindows.h"
-#include "../render/CAnimation.h"
-#include "../render/IRenderHandler.h"
-#include "../adventureMap/CInGameConsole.h"
-#include "../eventsSDL/InputHandler.h"
-
-#include "../../lib/CStack.h"
-#include "../../lib/CConfigHandler.h"
-#include "../../lib/CCreatureHandler.h"
-#include "../../lib/battle/CPlayerBattleCallback.h"
-#include "../../lib/callback/CCallback.h"
-#include "../../lib/entities/hero/CHeroClass.h"
-#include "../../lib/entities/hero/CHero.h"
-#include "../../lib/GameLibrary.h"
-#include "../../lib/gameState/InfoAboutArmy.h"
-#include "../../lib/texts/CGeneralTextHandler.h"
-#include "../../lib/texts/TextOperations.h"
-#include "../../lib/StartInfo.h"
-#include "../../lib/mapObjects/CGTownInstance.h"
-#include "../../lib/networkPacks/PacksForClientBattle.h"
-#include "../../lib/json/JsonUtils.h"
-
-
-void BattleConsole::showAll(Canvas & to)
-{
-	CIntObject::showAll(to);
-
-	Point line1 (pos.x + pos.w/2, pos.y +  8);
-	Point line2 (pos.x + pos.w/2, pos.y + 24);
-
-	auto visibleText = getVisibleText();
-
-	if(visibleText.size() > 0)
-		to.drawText(line1, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[0]);
-
-	if(visibleText.size() > 1)
-		to.drawText(line2, FONT_SMALL, Colors::WHITE, ETextAlignment::CENTER, visibleText[1]);
-}
-
-std::vector<std::string> BattleConsole::getVisibleText() const
-{
-	// high priority texts that hide battle log entries
-	for(const auto & text : {consoleText, hoverText})
-	{
-		if (text.empty())
-			continue;
-
-		auto result = CMessage::breakText(text, pos.w, FONT_SMALL);
-
-		if(result.size() > 2 && text.find('\n') != std::string::npos)
-		{
-			// Text has too many lines to fit into console, but has line breaks. Try ignore them and fit text that way
-			std::string cleanText = boost::algorithm::replace_all_copy(text, "\n", " ");
-			result = CMessage::breakText(cleanText, pos.w, FONT_SMALL);
-		}
-
-		if(result.size() > 2)
-			result.resize(2);
-		return result;
-	}
-
-	// log is small enough to fit entirely - display it as such
-	if (logEntries.size() < 3)
-		return logEntries;
-
-	return { logEntries[scrollPosition - 1], logEntries[scrollPosition] };
-}
-
-std::vector<std::string> BattleConsole::splitText(const std::string &text)
-{
-	std::vector<std::string> lines;
-	std::vector<std::string> output;
-
-	boost::split(lines, text, boost::is_any_of("\n"));
-
-	const auto & font = ENGINE->renderHandler().loadFont(FONT_SMALL);
-	for(const auto & line : lines)
-	{
-		if (font->getStringWidth(text) < pos.w)
-		{
-			output.push_back(line);
-		}
-		else
-		{
-			std::vector<std::string> substrings = CMessage::breakText(line, pos.w, FONT_SMALL);
-			output.insert(output.end(), substrings.begin(), substrings.end());
-		}
-	}
-	return output;
-}
-
-bool BattleConsole::addText(const std::string & text)
-{
-	logGlobal->trace("CBattleConsole message: %s", text);
-
-	auto newLines = splitText(text);
-
-	logEntries.insert(logEntries.end(), newLines.begin(), newLines.end());
-	scrollPosition = (int)logEntries.size()-1;
-	redraw();
-	return true;
-}
-void BattleConsole::scrollUp(ui32 by)
-{
-	if(scrollPosition > static_cast<int>(by))
-		scrollPosition -= by;
-	redraw();
-}
-
-void BattleConsole::scrollDown(ui32 by)
-{
-	if(scrollPosition + by < logEntries.size())
-		scrollPosition += by;
-	redraw();
-}
-
-BattleConsole::BattleConsole(const BattleInterface & owner, std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size)
-	: CIntObject(LCLICK)
-	, owner(owner)
-	, scrollPosition(-1)
-	, enteringText(false)
-{
-	OBJECT_CONSTRUCTION;
-	pos += objectPos;
-	pos.w = size.x;
-	pos.h = size.y;
-
-	background = std::make_shared<CPicture>(backgroundSource->getSurface(), Rect(imagePos, size), 0, 0 );
-}
-
-void BattleConsole::deactivate()
-{
-	if (enteringText)
-		GAME->interface()->cingconsole->endEnteringText(false);
-
-	CIntObject::deactivate();
-}
-
-void BattleConsole::clickPressed(const Point & cursorPosition)
-{
-	if(owner.makingTurn() && !owner.openingPlaying())
-	{
-		ENGINE->windows().createAndPushWindow<BattleConsoleWindow>(boost::algorithm::join(logEntries, "\n"));
-	}
-}
-
-void BattleConsole::setEnteringMode(bool on)
-{
-	consoleText.clear();
-
-	if (on)
-	{
-		assert(enteringText == false);
-		ENGINE->input().startTextInput(pos);
-	}
-	else
-	{
-		assert(enteringText == true);
-		ENGINE->input().stopTextInput();
-	}
-	enteringText = on;
-	redraw();
-}
-
-void BattleConsole::setEnteredText(const std::string & text)
-{
-	assert(enteringText == true);
-	consoleText = text;
-	redraw();
-}
-
-void BattleConsole::write(const std::string & Text)
-{
-	hoverText = Text;
-	redraw();
-}
-
-void BattleConsole::clearIfMatching(const std::string & Text)
-{
-	if (hoverText == Text)
-		clear();
-}
-
-void BattleConsole::clear()
-{
-	write({});
-}
-
-BattleConsoleWindow::BattleConsoleWindow(const std::string & text)
-	: CWindowObject(BORDERED)
-{
-	OBJECT_CONSTRUCTION;
-
-	pos.w = 429;
-	pos.h = 434;
-
-	updateShadow();
-	center();
-
-	backgroundTexture = std::make_shared<CFilledTexture>(ImagePath::builtin("DiBoxBck"), Rect(0, 0, pos.w, pos.h));
-	buttonOk = std::make_shared<CButton>(Point(183, 388), AnimationPath::builtin("IOKAY"), CButton::tooltip(), [this](){ close(); }, EShortcut::GLOBAL_ACCEPT);
-	Rect textArea(18, 17, 393, 354);
-	textBoxBackgroundBorder = std::make_shared<TransparentFilledRectangle>(textArea, ColorRGBA(0, 0, 0, 75), ColorRGBA(128, 100, 75));
-	textBox = std::make_shared<CTextBox>(text, textArea.resize(-5), CSlider::BROWN);
-	if(textBox->slider)
-		textBox->slider->scrollToMax();
-}
-
-const CGHeroInstance * BattleHero::instance()
-{
-	return hero;
-}
-
-void BattleHero::tick(uint32_t msPassed)
-{
-	size_t groupIndex = static_cast<size_t>(phase);
-
-	float timePassed = msPassed / 1000.f;
-
-	flagCurrentFrame += currentSpeed * timePassed;
-	currentFrame += currentSpeed * timePassed;
-
-	if(flagCurrentFrame >= flagAnimation->size(0))
-		flagCurrentFrame -= flagAnimation->size(0);
-
-	if(currentFrame >= animation->size(groupIndex))
-	{
-		currentFrame -= animation->size(groupIndex);
-		switchToNextPhase();
-	}
-}
-
-void BattleHero::render(Canvas & canvas)
-{
-	size_t groupIndex = static_cast<size_t>(phase);
-
-	auto flagFrame = flagAnimation->getImage(flagCurrentFrame, 0, true);
-	auto heroFrame = animation->getImage(currentFrame, groupIndex, true);
-
-	Point heroPosition = pos.center() - parent->pos.topLeft() - heroFrame->dimensions() / 2;
-	Point flagPosition = pos.center() - parent->pos.topLeft() - flagFrame->dimensions() / 2;
-
-	if(defender)
-		flagPosition += Point(-4, -41);
-	else
-		flagPosition += Point(4, -41);
-
-	canvas.draw(flagFrame, flagPosition);
-	canvas.draw(heroFrame, heroPosition);
-}
-
-void BattleHero::pause()
-{
-	currentSpeed = 0.f;
-}
-
-void BattleHero::play()
-{
-	//H3 speed: 10 fps ( 100 ms per frame)
-	currentSpeed = 10.f;
-}
-
-float BattleHero::getFrame() const
-{
-	return currentFrame;
-}
-
-void BattleHero::collectRenderableObjects(BattleRenderer & renderer)
-{
-	auto hex = defender ? BattleHex(GameConstants::BFIELD_WIDTH-1) : BattleHex(0);
-
-	renderer.insert(EBattleFieldLayer::HEROES, hex, [this](BattleRenderer::RendererRef canvas)
-	{
-		render(canvas);
-	});
-}
-
-void BattleHero::onPhaseFinished(const std::function<void()> & callback)
-{
-	phaseFinishedCallback = callback;
-}
-
-void BattleHero::setPhase(EHeroAnimType newPhase)
-{
-	nextPhase = newPhase;
-	switchToNextPhase(); //immediately switch to next phase and then restore idling phase
-	nextPhase = EHeroAnimType::HOLDING;
-}
-
-void BattleHero::heroLeftClicked()
-{
-	if(owner.actionsController->heroSpellcastingModeActive()) //we are casting a spell
-		return;
-
-	if(!hero || !owner.makingTurn())
-		return;
-
-	if(owner.getBattle()->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK) //check conditions
-	{
-		ENGINE->cursor().set(Cursor::Map::POINTER);
-		ENGINE->windows().createAndPushWindow<CSpellWindow>(hero, owner.getCurrentPlayerInterface());
-	}
-}
-
-void BattleHero::heroRightClicked()
-{
-	if(settings["battle"]["stickyHeroInfoWindows"].Bool())
-		return;
-
-	Point windowPosition;
-	if(ENGINE->screenDimensions().x < 1000)
-	{
-		windowPosition.x = (!defender) ? owner.fieldController->pos.left() + 1 : owner.fieldController->pos.right() - 79;
-		windowPosition.y = owner.fieldController->pos.y + 135;
-	}
-	else
-	{
-		windowPosition.x = (!defender) ? owner.fieldController->pos.left() - 93 : owner.fieldController->pos.right() + 15;
-		windowPosition.y = owner.fieldController->pos.y;
-	}
-
-	InfoAboutHero targetHero;
-	if(owner.makingTurn() || settings["session"]["spectate"].Bool())
-	{
-		auto h = defender ? owner.defendingHeroInstance : owner.attackingHeroInstance;
-		targetHero.initFromHero(h, InfoAboutHero::EInfoLevel::INBATTLE);
-		ENGINE->windows().createAndPushWindow<HeroInfoWindow>(targetHero, &windowPosition);
-	}
-}
-
-void BattleHero::switchToNextPhase()
-{
-	phase = nextPhase;
-	currentFrame = 0.f;
-
-	auto copy = phaseFinishedCallback;
-	phaseFinishedCallback.clear();
-	copy();
-}
-
-BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender):
-	defender(defender),
-	hero(hero),
-	owner(owner),
-	phase(EHeroAnimType::HOLDING),
-	nextPhase(EHeroAnimType::HOLDING),
-	currentSpeed(0.f),
-	currentFrame(0.f),
-	flagCurrentFrame(0.f)
-{
-	AnimationPath animationPath;
-
-	if(!hero->getHeroType()->battleImage.empty())
-		animationPath = hero->getHeroType()->battleImage;
-	else
-	if(hero->gender == EHeroGender::FEMALE)
-		animationPath = hero->getHeroClass()->imageBattleFemale;
-	else
-		animationPath = hero->getHeroClass()->imageBattleMale;
-
-	animation = ENGINE->renderHandler().loadAnimation(animationPath, EImageBlitMode::WITH_SHADOW);
-
-	pos.w = 64;
-	pos.h = 136;
-	pos.x = owner.fieldController->pos.x + (defender ? (owner.fieldController->pos.w - pos.w) : 0);
-	pos.y = owner.fieldController->pos.y;
-
-	if(defender)
-		animation->verticalFlip();
-
-	if(defender)
-		flagAnimation = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGR"), EImageBlitMode::COLORKEY);
-	else
-		flagAnimation = ENGINE->renderHandler().loadAnimation(AnimationPath::builtin("CMFLAGL"), EImageBlitMode::COLORKEY);
-
-	flagAnimation->playerColored(hero->tempOwner);
-
-	switchToNextPhase();
-	play();
-
-	addUsedEvents(TIME);
-}
-
-QuickSpellPanel::QuickSpellPanel(BattleInterface & owner)
-	: CIntObject(0), owner(owner)
-{
-	OBJECT_CONSTRUCTION;
-
-	addUsedEvents(LCLICK | SHOW_POPUP | MOVE | INPUT_MODE_CHANGE);
-
-	pos = Rect(0, 0, 52, 600);
-	background = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), pos);
-	rect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w + 1, pos.h + 1), ColorRGBA(0, 0, 0, 0), ColorRGBA(241, 216, 120, 255));
-
-	create();
-}
-
-std::vector<std::tuple<SpellID, bool>> QuickSpellPanel::getSpells() const
-{
-	std::vector<SpellID> spellIds;
-	std::vector<bool> spellIdsFromSetting;
-	for(int i = 0; i < QUICKSPELL_SLOTS; i++)
-	{
-		std::string spellIdentifier = persistentStorage["quickSpell"][std::to_string(i)].String();
-		SpellID id;
-		try
-		{
-			id = SpellID::decode(spellIdentifier);
-		}
-		catch(const IdentifierResolutionException& e)
-		{
-			id = SpellID::NONE;
-		}	
-		spellIds.push_back(id);	
-		spellIdsFromSetting.push_back(id != SpellID::NONE);	
-	}
-
-	// autofill empty slots with spells if possible
-	auto hero = owner.getBattle()->battleGetMyHero();
-	for(int i = 0; i < QUICKSPELL_SLOTS; i++)
-	{
-		if(spellIds[i] != SpellID::NONE)
-			continue;
-
-		for(const auto & availableSpellID : LIBRARY->spellh->getDefaultAllowed())
-		{
-			const auto * availableSpell = availableSpellID.toSpell();
-			if(!availableSpell->isAdventure() && !availableSpell->isCreatureAbility() && hero->canCastThisSpell(availableSpell) && !vstd::contains(spellIds, availableSpell->getId()))
-			{
-				spellIds[i] = availableSpell->getId();
-				break;
-			}	
-		}
-	}
-
-	std::vector<std::tuple<SpellID, bool>> ret;
-	for(int i = 0; i < QUICKSPELL_SLOTS; i++)
-		ret.push_back(std::make_tuple(spellIds[i], spellIdsFromSetting[i]));
-	return ret;
-}
-
-void QuickSpellPanel::create()
-{
-	OBJECT_CONSTRUCTION;
-
-	const JsonNode config = JsonUtils::assembleFromFiles("config/shortcutsConfig");
-
-	labels.clear();
-	buttons.clear();
-	buttonsDisabled.clear();
-
-	auto hero = owner.getBattle()->battleGetMyHero();
-	if(!hero)
-		return;
-
-	auto spells = getSpells();
-	for(int i = 0; i < QUICKSPELL_SLOTS; i++) {
-		SpellID id;
-		bool fromSettings;
-		std::tie(id, fromSettings) = spells[i];
-
-		auto button = std::make_shared<CButton>(Point(2, 7 + 50 * i), AnimationPath::builtin("spellint"), CButton::tooltip(), [this, id, hero](){
-			if(id.hasValue() && id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, hero))
-			{
-				owner.castThisSpell(id);
-			}
-		});
-		button->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("spellint"), id != SpellID::NONE ? id.num + 1 : 0));
-		button->addPopupCallback([this, i, hero](){
-			ENGINE->input().hapticFeedback();
-			ENGINE->windows().createAndPushWindow<CSpellWindow>(hero, owner.curInt.get(), true, [this, i](SpellID spell){
-				Settings configID = persistentStorage.write["quickSpell"][std::to_string(i)];
-				configID->String() = spell == SpellID::NONE ? "" : spell.toSpell()->identifier;
-				create();
-			});
-		});
-
-		if(fromSettings)
-			buttonsIsAutoGenerated.push_back(std::make_shared<TransparentFilledRectangle>(Rect(45, 37 + 50 * i, 5, 5), Colors::ORANGE));
-
-		if(!id.hasValue() || !id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, hero))
-		{
-			buttonsDisabled.push_back(std::make_shared<TransparentFilledRectangle>(Rect(2, 7 + 50 * i, 48, 36), ColorRGBA(0, 0, 0, 172)));
-		}
-		if(ENGINE->input().getCurrentInputMode() == InputMode::KEYBOARD_AND_MOUSE)
-			labels.push_back(std::make_shared<CLabel>(7, 10 + 50 * i, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, config["keyboard"]["battleSpellShortcut" + std::to_string(i)].String()));
-
-		buttons.push_back(button);
-	}
-}
-
-void QuickSpellPanel::show(Canvas & to)
-{
-	showAll(to);
-	CIntObject::show(to);
-}
-
-void QuickSpellPanel::inputModeChanged(InputMode modi)
-{
-	create();
-	redraw();
-}
-
-HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground)
-	: CIntObject(0)
-{
-	OBJECT_CONSTRUCTION;
-	if (position != nullptr)
-		moveTo(*position);
-
-	if(initializeBackground)
-	{
-		background = std::make_shared<CPicture>(ImagePath::builtin("CHRPOP"));
-		background->setPlayerColor(hero.owner);
-	}
-
-	initializeData(hero);
-}
-
-void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero)
-{
-	OBJECT_CONSTRUCTION;
-	auto attack = hero.details->primskills[0];
-	auto defense = hero.details->primskills[1];
-	auto power = hero.details->primskills[2];
-	auto knowledge = hero.details->primskills[3];
-	auto morale = hero.details->morale;
-	auto luck = hero.details->luck;
-	auto currentSpellPoints = hero.details->mana;
-	auto maxSpellPoints = hero.details->manaLimit;
-
-	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsLarge"), hero.getIconIndex(), 0, 10, 6));
-
-	//primary stats
-	labels.push_back(std::make_shared<CLabel>(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[380] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 87, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[381] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 99, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[382] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 111, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[383] + ":"));
-
-	labels.push_back(std::make_shared<CLabel>(69, 87, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(attack)));
-	labels.push_back(std::make_shared<CLabel>(69, 99, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(defense)));
-	labels.push_back(std::make_shared<CLabel>(69, 111, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(power)));
-	labels.push_back(std::make_shared<CLabel>(69, 123, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(knowledge)));
-
-	//morale+luck
-	labels.push_back(std::make_shared<CLabel>(9, 131, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[384] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 143, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[385] + ":"));
-
-	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("IMRL22"), std::clamp(morale + 3, 0, 6), 0, 47, 131));
-	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("ILCK22"), std::clamp(luck + 3, 0, 6), 0, 47, 143));
-
-	//spell points
-	labels.push_back(std::make_shared<CLabel>(39, 174, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[387]));
-	labels.push_back(std::make_shared<CLabel>(39, 186, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints)));
-}
-
-void HeroInfoBasicPanel::update(const InfoAboutHero & updatedInfo)
-{
-	icons.clear();
-	labels.clear();
-
-	initializeData(updatedInfo);
-}
-
-void HeroInfoBasicPanel::show(Canvas & to)
-{
-	showAll(to);
-	CIntObject::show(to);
-}
-
-
-StackInfoBasicPanel::StackInfoBasicPanel(const CStack * stack, bool initializeBackground)
-	: CIntObject(0)
-{
-	OBJECT_CONSTRUCTION;
-
-	if(initializeBackground)
-	{
-		background = std::make_shared<CPicture>(ImagePath::builtin("CCRPOP"));
-		background->pos.y += 37;
-		background->setPlayerColor(stack->getOwner());
-		background2 = std::make_shared<CPicture>(ImagePath::builtin("CHRPOP"));
-		background2->setPlayerColor(stack->getOwner());
-	}
-
-	initializeData(stack);
-}
-
-void StackInfoBasicPanel::initializeData(const CStack * stack)
-{
-	OBJECT_CONSTRUCTION;
-
-	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("TWCRPORT"), stack->creatureId().getNum() + 2, 0, 10, 6));
-	labels.push_back(std::make_shared<CLabel>(10 + 58, 6 + 64, FONT_MEDIUM, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, TextOperations::formatMetric(stack->getCount(), 4)));
-
-	int damageMultiplier = 1;
-	if (stack->hasBonusOfType(BonusType::SIEGE_WEAPON))
-	{
-		static const auto bonusSelector =
-			Selector::sourceTypeSel(BonusSource::ARTIFACT).Or(
-			Selector::sourceTypeSel(BonusSource::HERO_BASE_SKILL)).And(
-			Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)));
-
-		damageMultiplier += stack->valOfBonuses(bonusSelector);
-	}
-
-	auto attack = std::to_string(LIBRARY->creatures()->getByIndex(stack->creatureIndex())->getAttack(stack->isShooter())) + "(" + std::to_string(stack->getAttack(stack->isShooter())) + ")";
-	auto defense = std::to_string(LIBRARY->creatures()->getByIndex(stack->creatureIndex())->getDefense(stack->isShooter())) + "(" + std::to_string(stack->getDefense(stack->isShooter())) + ")";
-	auto damage = std::to_string(damageMultiplier * stack->getMinDamage(stack->isShooter())) + "-" + std::to_string(damageMultiplier * stack->getMaxDamage(stack->isShooter()));
-	auto health = stack->getMaxHealth();
-	auto morale = stack->moraleVal();
-	auto luck = stack->luckVal();
-
-	auto killed = stack->getKilled();
-	auto healthRemaining = TextOperations::formatMetric(std::max(stack->getAvailableHealth() - (stack->getCount() - 1) * health, (si64)0), 4);
-
-	//primary stats*/
-	labels.push_back(std::make_shared<CLabel>(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[380] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 87, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[381] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 99, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[386] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 111, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[389] + ":"));
-
-	labels.push_back(std::make_shared<CLabel>(69, 87, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, attack));
-	labels.push_back(std::make_shared<CLabel>(69, 99, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, defense));
-	labels.push_back(std::make_shared<CLabel>(69, 111, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, damage));
-	labels.push_back(std::make_shared<CLabel>(69, 123, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(health)));
-
-	//morale+luck
-	labels.push_back(std::make_shared<CLabel>(9, 131, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[384] + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 143, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[385] + ":"));
-
-	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("IMRL22"), std::clamp(morale + 3, 0, 6), 0, 47, 131));
-	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("ILCK22"), std::clamp(luck + 3, 0, 6), 0, 47, 143));
-
-	//extra information
-	labels.push_back(std::make_shared<CLabel>(9, 168, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->translate("vcmi.battleWindow.killed") + ":"));
-	labels.push_back(std::make_shared<CLabel>(9, 180, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[389] + ":"));
-
-	labels.push_back(std::make_shared<CLabel>(69, 180, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(killed)));
-	labels.push_back(std::make_shared<CLabel>(69, 192, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, healthRemaining));
-
-	//spells
-	static const Point firstPos(15, 206); // position of 1st spell box
-	static const Point offset(0, 38);  // offset of each spell box from previous
-
-	for(int i = 0; i < 3; i++)
-		icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("SpellInt"), 78, 0, firstPos.x + offset.x * i, firstPos.y + offset.y * i));
-
-	int printed=0; //how many effect pics have been printed
-	std::vector<SpellID> spells = stack->activeSpells();
-	for(SpellID effect : spells)
-	{
-		//not all effects have graphics (for eg. Acid Breath)
-		//for modded spells iconEffect is added to SpellInt.def
-		const bool hasGraphics = (effect < SpellID::THUNDERBOLT) || (effect >= SpellID::AFTER_LAST);
-
-		if (hasGraphics)
-		{
-			//FIXME: support permanent duration
-			auto spellBonuses = stack->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)));
-
-			if (spellBonuses->empty())
-				throw std::runtime_error("Failed to find effects for spell " + effect.toSpell()->getJsonKey());
-
-			int duration = spellBonuses->front()->turnsRemain;
-
-			icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("SpellInt"), effect.getNum() + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed));
-			if(settings["general"]["enableUiEnhancements"].Bool())
-				labels.push_back(std::make_shared<CLabel>(firstPos.x + offset.x * printed + 46, firstPos.y + offset.y * printed + 36, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(duration)));
-			if(++printed >= 3 || (printed == 2 && spells.size() > 3)) // interface limit reached
-				break;
-		}
-	}
-
-	if(spells.size() == 0)
-		labelsMultiline.push_back(std::make_shared<CMultiLineLabel>(Rect(firstPos.x, firstPos.y, 48, 36), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[674]));
-	if(spells.size() > 3)
-		labelsMultiline.push_back(std::make_shared<CMultiLineLabel>(Rect(firstPos.x + offset.x * 2, firstPos.y + offset.y * 2 - 4, 48, 36), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "..."));
-}
-
-void StackInfoBasicPanel::update(const CStack * updatedInfo)
-{
-	icons.clear();
-	labels.clear();
-	labelsMultiline.clear();
-
-	initializeData(updatedInfo);
-}
-
-void StackInfoBasicPanel::show(Canvas & to)
-{
-	showAll(to);
-	CIntObject::show(to);
-}
-
-HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, Point * position)
-	: CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("CHRPOP"))
-{
-	OBJECT_CONSTRUCTION;
-	if (position != nullptr)
-		moveTo(*position);
-
-	background->setPlayerColor(hero.owner); //maybe add this functionality to base class?
-
-	content = std::make_shared<HeroInfoBasicPanel>(hero, nullptr, false);
-}
-
-BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay)
-	: owner(_owner)
-{
-	OBJECT_CONSTRUCTION;
-
-	background = std::make_shared<CPicture>(ImagePath::builtin("CPRESULT"));
-	background->setPlayerColor(owner.playerID);
-	pos = center(background->pos);
-
-	exit = std::make_shared<CButton>(Point(384, 505), AnimationPath::builtin("iok6432.def"), std::make_pair("", ""), [&](){ bExitf();}, EShortcut::GLOBAL_ACCEPT);
-	exit->setBorderColor(Colors::METALLIC_GOLD);
-	
-	if(allowReplay || owner.cb->getStartInfo()->extraOptionsInfo.unlimitedReplay)
-	{
-		repeat = std::make_shared<CButton>(Point(24, 505), AnimationPath::builtin("icn6432.def"), std::make_pair("", ""), [&](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL);
-		repeat->setBorderColor(Colors::METALLIC_GOLD);
-		labels.push_back(std::make_shared<CLabel>(232, 520, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->translate("vcmi.battleResultsWindow.applyResultsLabel")));
-	}
-
-	if(br.winner == BattleSide::ATTACKER)
-	{
-		labels.push_back(std::make_shared<CLabel>(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[410]));
-	}
-	else
-	{
-		labels.push_back(std::make_shared<CLabel>(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[411]));
-	}
-	
-	if(br.winner == BattleSide::DEFENDER)
-	{
-		labels.push_back(std::make_shared<CLabel>(412, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[410]));
-	}
-	else
-	{
-		labels.push_back(std::make_shared<CLabel>(408, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[411]));
-	}
-
-	labels.push_back(std::make_shared<CLabel>(232, 302, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW,  LIBRARY->generaltexth->allTexts[407]));
-	labels.push_back(std::make_shared<CLabel>(232, 332, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[408]));
-	labels.push_back(std::make_shared<CLabel>(232, 428, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[409]));
-
-	std::string sideNames[2] = {"N/A", "N/A"};
-
-	for(auto i : {BattleSide::ATTACKER, BattleSide::DEFENDER})
-	{
-		auto heroInfo = owner.cb->getBattle(br.battleID)->battleGetHeroInfo(i);
-		const int xs[] = {21, 392};
-
-		if(heroInfo.portraitSource.isValid()) //attacking hero
-		{
-			icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsLarge"), heroInfo.getIconIndex(), 0, xs[static_cast<int>(i)], 38));
-			sideNames[static_cast<int>(i)] = heroInfo.name;
-		}
-		else
-		{
-			auto stacks = owner.cb->getBattle(br.battleID)->battleGetAllStacks();
-			vstd::erase_if(stacks, [i](const CStack * stack) //erase stack of other side and not coming from garrison
-			{
-				return stack->unitSide() != i || !stack->base;
-			});
-
-			auto best = vstd::maxElementByFun(stacks, [](const CStack * stack)
-			{
-				return stack->unitType()->getAIValue();
-			});
-
-			if(best != stacks.end()) //should be always but to be safe...
-			{
-				icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("TWCRPORT"), (*best)->unitType()->getIconIndex(), 0, xs[static_cast<int>(i)], 38));
-				sideNames[static_cast<int>(i)] = (*best)->unitType()->getNamePluralTranslated();
-			}
-		}
-	}
-
-	//printing attacker and defender's names
-	labels.push_back(std::make_shared<CLabel>(89, 37, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, sideNames[0]));
-	labels.push_back(std::make_shared<CLabel>(381, 53, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, sideNames[1]));
-
-	//printing casualties
-	for(auto step : {BattleSide::ATTACKER, BattleSide::DEFENDER})
-	{
-		if(br.casualties[step].size()==0)
-		{
-			labels.push_back(std::make_shared<CLabel>(235, 360 + 97 * static_cast<int>(step), FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[523]));
-		}
-		else
-		{
-			int xPos = 235 - ((int)br.casualties[step].size()*32 + ((int)br.casualties[step].size() - 1)*10)/2; //increment by 42 with each picture
-			int yPos = 344 + static_cast<int>(step) * 97;
-			for(auto & elem : br.casualties[step])
-			{
-				auto creature = elem.first.toEntity(LIBRARY);
-				if (creature->getId() == CreatureID::ARROW_TOWERS )
-					continue; // do not show destroyed towers in battle results
-
-				icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("CPRSMALL"), creature->getIconIndex(), 0, xPos, yPos));
-				std::ostringstream amount;
-				amount<<elem.second;
-				labels.push_back(std::make_shared<CLabel>(xPos + 16, yPos + 42, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, amount.str()));
-				xPos += 42;
-			}
-		}
-	}
-
-	auto resources = getResources(br);
-
-	description = std::make_shared<CTextBox>(resources.resultText.toString(), Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
-	videoPlayer = std::make_shared<VideoWidget>(Point(107, 70), resources.prologueVideo, resources.loopedVideo, false);
-
-	ENGINE->music().playMusic(resources.musicName, false, true);
-}
-
-BattleResultResources BattleResultWindow::getResources(const BattleResult & br)
-{
-	//printing result description
-	bool weAreAttacker = owner.cb->getBattle(br.battleID)->battleGetMySide() == BattleSide::ATTACKER;
-	bool weAreDefender = !weAreAttacker;
-	bool weWon = (br.winner == BattleSide::ATTACKER && weAreAttacker) || (br.winner == BattleSide::DEFENDER && !weAreAttacker);
-	bool isSiege = owner.cb->getBattle(br.battleID)->battleGetDefendedTown() != nullptr;
-
-	BattleResultResources resources;
-
-	if(weWon)
-	{
-		if(isSiege && weAreDefender)
-		{
-			resources.musicName = AudioPath::builtin("Music/Defend Castle");
-			resources.prologueVideo = VideoPath::builtin("DEFENDALL.BIK");
-			resources.loopedVideo = VideoPath::builtin("defendloop.bik");
-		}
-		else
-		{
-			resources.musicName = AudioPath::builtin("Music/Win Battle");
-			resources.prologueVideo = VideoPath::builtin("WIN3.BIK");
-			resources.loopedVideo = VideoPath::builtin("WIN3.BIK");
-		}
-
-		switch(br.result)
-		{
-		case EBattleResult::NORMAL:
-			resources.resultText.appendTextID("core.genrltxt.304");
-			break;
-		case EBattleResult::ESCAPE:
-			resources.resultText.appendTextID("core.genrltxt.303");
-			break;
-		case EBattleResult::SURRENDER:
-			resources.resultText.appendTextID("core.genrltxt.302");
-			break;
-		default:
-			throw std::runtime_error("Invalid battle result!");
-		}
-
-		const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero();
-		if (ourHero)
-		{
-			resources.resultText.appendTextID("core.genrltxt.305");
-			resources.resultText.replaceTextID(ourHero->getNameTextID());
-			resources.resultText.replaceNumber(br.exp[weAreAttacker ? BattleSide::ATTACKER : BattleSide::DEFENDER]);
-		}
-	}
-	else // we lose
-	{
-		switch(br.result)
-		{
-		case EBattleResult::NORMAL:
-			resources.resultText.appendTextID("core.genrltxt.311");
-			resources.musicName = AudioPath::builtin("Music/LoseCombat");
-			resources.prologueVideo = VideoPath::builtin("LBSTART.BIK");
-			resources.loopedVideo = VideoPath::builtin("LBLOOP.BIK");
-			break;
-		case EBattleResult::ESCAPE:
-			resources.resultText.appendTextID("core.genrltxt.310");
-			resources.musicName = AudioPath::builtin("Music/Retreat Battle");
-			resources.prologueVideo = VideoPath::builtin("RTSTART.BIK");
-			resources.loopedVideo = VideoPath::builtin("RTLOOP.BIK");
-			break;
-		case EBattleResult::SURRENDER:
-			resources.resultText.appendTextID("core.genrltxt.309");
-			resources.musicName = AudioPath::builtin("Music/Surrender Battle");
-			resources.prologueVideo = VideoPath::builtin("SURRENDER.BIK");
-			resources.loopedVideo = VideoPath::builtin("SURRENDER.BIK");
-			break;
-		default:
-				throw std::runtime_error("Invalid battle result!");
-		}
-
-		if(isSiege && weAreDefender)
-		{
-			resources.musicName = AudioPath::builtin("Music/LoseCastle");
-			resources.prologueVideo = VideoPath::builtin("LOSECSTL.BIK");
-			resources.loopedVideo = VideoPath::builtin("LOSECSLP.BIK");
-		}
-	}
-
-	return resources;
-}
-
-void BattleResultWindow::activate()
-{
-	owner.showingDialog->setBusy();
-	CIntObject::activate();
-}
-
-void BattleResultWindow::buttonPressed(int button)
-{
-	if (resultCallback)
-		resultCallback(button);
-
-	CPlayerInterface &intTmp = owner; //copy reference because "this" will be destructed soon
-
-	close();
-
-	if(ENGINE->windows().topWindow<BattleWindow>())
-		ENGINE->windows().popWindows(1); //pop battle interface if present
-
-	//Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle,
-	//so we can be sure that there is no dialogs left on GUI stack.
-	intTmp.showingDialog->setFree();
-}
-
-void BattleResultWindow::bExitf()
-{
-	buttonPressed(0);
-}
-
-void BattleResultWindow::bRepeatf()
-{
-	buttonPressed(1);
-}
-
-StackQueue::StackQueue(bool Embedded, BattleInterface & owner)
-	: embedded(Embedded),
-	owner(owner)
-{
-	OBJECT_CONSTRUCTION;
-
-	uint32_t queueSize = QUEUE_SIZE_BIG;
-
-	if(embedded)
-	{
-		int32_t queueSmallOutsideYOffset = 65;
-		bool queueSmallOutside = settings["battle"]["queueSmallOutside"].Bool() && (pos.y - queueSmallOutsideYOffset) >= 0;
-		queueSize = std::clamp(static_cast<int>(settings["battle"]["queueSmallSlots"].Float()), 1, queueSmallOutside ? ENGINE->screenDimensions().x / 41 : 19);
-
-		pos.w = queueSize * 41;
-		pos.h = 49;
-		pos.x += parent->pos.w/2 - pos.w/2;
-		pos.y += queueSmallOutside ? -queueSmallOutsideYOffset : 10;
-	}
-	else
-	{
-		pos.w = 800;
-		pos.h = 85;
-		pos.x += 0;
-		pos.y -= pos.h;
-
-		background = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h));
-	}
-
-	stackBoxes.resize(queueSize);
-	for (int i = 0; i < stackBoxes.size(); i++)
-	{
-		stackBoxes[i] = std::make_shared<StackBox>(this);
-		stackBoxes[i]->moveBy(Point(1 + (embedded ? 41 : 80) * i, 0));
-	}
-}
-
-void StackQueue::show(Canvas & to)
-{
-	if (embedded)
-		showAll(to);
-	CIntObject::show(to);
-}
-
-void StackQueue::update()
-{
-	std::vector<battle::Units> queueData;
-
-	owner.getBattle()->battleGetTurnOrder(queueData, stackBoxes.size(), 0);
-
-	size_t boxIndex = 0;
-	ui32 tmpTurn = -1;
-
-	for(size_t turn = 0; turn < queueData.size() && boxIndex < stackBoxes.size(); turn++)
-	{
-		for(size_t unitIndex = 0; unitIndex < queueData[turn].size() && boxIndex < stackBoxes.size(); boxIndex++, unitIndex++)
-		{
-			ui32 currentTurn = owner.round + turn;
-			stackBoxes[boxIndex]->setUnit(queueData[turn][unitIndex], turn, tmpTurn != currentTurn && owner.round != 0 && (!embedded || tmpTurn != -1) ? (std::optional<ui32>)currentTurn : std::nullopt);
-			tmpTurn = currentTurn;
-		}
-	}
-
-	for(; boxIndex < stackBoxes.size(); boxIndex++)
-		stackBoxes[boxIndex]->setUnit(nullptr);
-}
-
-int32_t StackQueue::getSiegeShooterIconID()
-{
-	return owner.siegeController->getSiegedTown()->getFactionID().getNum();
-}
-
-std::optional<uint32_t> StackQueue::getHoveredUnitIdIfAny() const
-{
-	for(const auto & stackBox : stackBoxes)
-	{
-		if(stackBox->isHovered())
-		{
-			return stackBox->getBoundUnitID();
-		}
-	}
-
-	return std::nullopt;
-}
-
-StackQueue::StackBox::StackBox(StackQueue * owner):
-	CIntObject(SHOW_POPUP | HOVER), owner(owner)
-{
-	OBJECT_CONSTRUCTION;
-	background = std::make_shared<CPicture>(ImagePath::builtin(owner->embedded ? "StackQueueSmall" : "StackQueueLarge"));
-
-	pos.w = background->pos.w;
-	pos.h = background->pos.h;
-
-	if(owner->embedded)
-	{
-		icon = std::make_shared<CAnimImage>(AnimationPath::builtin("CPRSMALL"), 0, 0, 5, 2);
-		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 7, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
-		roundRect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, 2, 48), ColorRGBA(0, 0, 0, 255), ColorRGBA(0, 255, 0, 255));
-	}
-	else
-	{
-		icon = std::make_shared<CAnimImage>(AnimationPath::builtin("TWCRPORT"), 0, 0, 9, 1);
-		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
-		roundRect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, 15, 18), ColorRGBA(0, 0, 0, 255), ColorRGBA(241, 216, 120, 255));
-		round = std::make_shared<CLabel>(6, 9, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
-
-		Point iconPos(pos.w - 16, pos.h - 16);
-
-		defendIcon = std::make_shared<CPicture>(ImagePath::builtin("battle/QueueDefend"), iconPos);
-		waitIcon = std::make_shared<CPicture>(ImagePath::builtin("battle/QueueWait"), iconPos);
-
-		defendIcon->setEnabled(false);
-		waitIcon->setEnabled(false);
-	}
-	roundRect->disable();
-}
-
-void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std::optional<ui32> currentTurn)
-{
-	if(unit)
-	{
-		boundUnitID = unit->unitId();
-		background->setPlayerColor(unit->unitOwner());
-		icon->visible = true;
-
-		// temporary code for mod compatibility:
-		// first, set icon that should definitely exist (arrow tower icon in base vcmi mod)
-		// second, try to switch to icon that should be provided by mod
-		// if mod is not up to date and does have arrow tower icon yet - second setFrame call will fail and retain previously set image
-		// for 1.2 release & later next line should be moved into 'else' block
-		icon->setFrame(unit->creatureIconIndex(), 0);
-		if (unit->unitType()->getId() == CreatureID::ARROW_TOWERS)
-			icon->setFrame(owner->getSiegeShooterIconID(), 1);
-
-		roundRect->setEnabled(currentTurn.has_value());
-		if(!owner->embedded)
-			round->setEnabled(currentTurn.has_value());
-
-		amount->setText(TextOperations::formatMetric(unit->getCount(), 4));
-		if(currentTurn && !owner->embedded)
-		{
-			std::string tmp = std::to_string(*currentTurn);
-			const auto & font = ENGINE->renderHandler().loadFont(FONT_SMALL);
-			int len = font->getStringWidth(tmp);
-			roundRect->pos.w = len + 6;
-			round->pos = Rect(roundRect->pos.center().x, roundRect->pos.center().y, 0, 0);
-			round->setText(tmp);
-		}
-
-		if(!owner->embedded)
-		{
-			bool defended = unit->defended(turn) || (turn > 0 && unit->defended(turn - 1));
-			bool waited = unit->waited(turn) && !defended;
-
-			defendIcon->setEnabled(defended);
-			waitIcon->setEnabled(waited);
-		}
-	}
-	else
-	{
-		boundUnitID = std::nullopt;
-		background->setPlayerColor(PlayerColor::NEUTRAL);
-		icon->visible = false;
-		icon->setFrame(0);
-		amount->setText("");
-		if(!owner->embedded)
-		{
-			defendIcon->setEnabled(false);
-			waitIcon->setEnabled(false);
-		}
-	}
-}
-
-std::optional<uint32_t> StackQueue::StackBox::getBoundUnitID() const
-{
-	return boundUnitID;
-}
-
-bool StackQueue::StackBox::isBoundUnitHighlighted() const
-{
-	auto unitIdsToHighlight = owner->owner.stacksController->getHoveredStacksUnitIds();
-	return vstd::contains(unitIdsToHighlight, getBoundUnitID());
-}
-
-void StackQueue::StackBox::showAll(Canvas & to)
-{
-	CIntObject::showAll(to);
-
-	if(isBoundUnitHighlighted())
-		to.drawBorder(background->pos, Colors::CYAN, 2);
-}
-
-void StackQueue::StackBox::show(Canvas & to)
-{
-	CIntObject::show(to);
-
-	if(isBoundUnitHighlighted())
-		to.drawBorder(background->pos, Colors::CYAN, 2);
-}
-
-void StackQueue::StackBox::showPopupWindow(const Point & cursorPosition)
-{
-	auto stacks = owner->owner.getBattle()->battleGetAllStacks();
-	for(const CStack * stack : stacks)
-		if(boundUnitID.has_value() && stack->unitId() == *boundUnitID)
-			ENGINE->windows().createAndPushWindow<CStackWindow>(stack, true);
-}

+ 0 - 293
client/battle/BattleInterfaceClasses.h

@@ -1,293 +0,0 @@
-/*
- * BattleInterfaceClasses.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 "BattleConstants.h"
-#include "../gui/CIntObject.h"
-#include "../../lib/FunctionList.h"
-#include "../../lib/battle/BattleHex.h"
-#include "../../lib/texts/MetaString.h"
-#include "../windows/CWindowObject.h"
-
-VCMI_LIB_NAMESPACE_BEGIN
-
-class CGHeroInstance;
-struct BattleResult;
-struct InfoAboutHero;
-class CStack;
-class CPlayerBattleCallback;
-
-namespace battle
-{
-class Unit;
-}
-
-VCMI_LIB_NAMESPACE_END
-
-class CAnimation;
-class Canvas;
-class BattleInterface;
-class CPicture;
-class CFilledTexture;
-class CButton;
-class CLabel;
-class CMultiLineLabel;
-class CTextBox;
-class CAnimImage;
-class TransparentFilledRectangle;
-class CPlayerInterface;
-class BattleRenderer;
-class VideoWidget;
-class QuickSpellPanel;
-
-/// Class which shows the console at the bottom of the battle screen and manages the text of the console
-class BattleConsole : public CIntObject, public IStatusBar
-{
-private:
-	const BattleInterface & owner;
-
-	std::shared_ptr<CPicture> background;
-
-	/// List of all texts added during battle, essentially - log of entire battle
-	std::vector< std::string > logEntries;
-
-	/// Current scrolling position, to allow showing older entries via scroll buttons
-	int scrollPosition;
-
-	/// current hover text set on mouse move, takes priority over log entries
-	std::string hoverText;
-
-	/// current text entered via in-game console, takes priority over both log entries and hover text
-	std::string consoleText;
-
-	/// if true then we are currently entering console text
-	bool enteringText;
-
-	/// splits text into individual strings for battle log
-	std::vector<std::string> splitText(const std::string &text);
-
-	/// select line(s) that will be visible in UI
-	std::vector<std::string> getVisibleText() const;
-public:
-	BattleConsole(const BattleInterface & owner, std::shared_ptr<CPicture> backgroundSource, const Point & objectPos, const Point & imagePos, const Point &size);
-
-	void showAll(Canvas & to) override;
-	void deactivate() override;
-
-	void clickPressed(const Point & cursorPosition) override;
-
-	bool addText(const std::string &text); //adds text at the last position; returns false if failed (e.g. text longer than 70 characters)
-	void scrollUp(ui32 by = 1); //scrolls console up by 'by' positions
-	void scrollDown(ui32 by = 1); //scrolls console up by 'by' positions
-
-	// IStatusBar interface
-	void write(const std::string & Text) override;
-	void clearIfMatching(const std::string & Text) override;
-	void clear() override;
-	void setEnteringMode(bool on) override;
-	void setEnteredText(const std::string & text) override;
-};
-
-class BattleConsoleWindow : public CWindowObject
-{
-private:
-	std::shared_ptr<CFilledTexture> backgroundTexture;
-	std::shared_ptr<CButton> buttonOk;
-	std::shared_ptr<TransparentFilledRectangle> textBoxBackgroundBorder;
-	std::shared_ptr<CTextBox> textBox;
-public:
-	BattleConsoleWindow(const std::string & text);
-};
-
-/// Hero battle animation
-class BattleHero : public CIntObject
-{
-	bool defender;
-
-	CFunctionList<void()> phaseFinishedCallback;
-
-	std::shared_ptr<CAnimation> animation;
-	std::shared_ptr<CAnimation> flagAnimation;
-
-	const CGHeroInstance * hero; //this animation's hero instance
-	const BattleInterface & owner; //battle interface to which this animation is assigned
-
-	EHeroAnimType phase; //stage of animation
-	EHeroAnimType nextPhase; //stage of animation to be set after current phase is fully displayed
-
-	float currentSpeed;
-	float currentFrame; //frame of animation
-	float flagCurrentFrame;
-
-	void switchToNextPhase();
-
-	void render(Canvas & canvas); //prints next frame of animation to to
-public:
-	const CGHeroInstance * instance();
-
-	void setPhase(EHeroAnimType newPhase); //sets phase of hero animation
-
-	void collectRenderableObjects(BattleRenderer & renderer);
-	void tick(uint32_t msPassed) override;
-
-	float getFrame() const;
-	void onPhaseFinished(const std::function<void()> &);
-
-	void pause();
-	void play();
-
-	void heroLeftClicked();
-	void heroRightClicked();
-
-	BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender);
-};
-
-class QuickSpellPanel : public CIntObject
-{
-private:
-	std::shared_ptr<CFilledTexture> background;
-	std::shared_ptr<TransparentFilledRectangle> rect;
-	std::vector<std::shared_ptr<CButton>> buttons;
-	std::vector<std::shared_ptr<TransparentFilledRectangle>> buttonsIsAutoGenerated;
-	std::vector<std::shared_ptr<TransparentFilledRectangle>> buttonsDisabled;
-	std::vector<std::shared_ptr<CLabel>> labels;
-
-	BattleInterface & owner;
-public:
-	int QUICKSPELL_SLOTS = 12;
-
-	bool isEnabled; // isActive() is not working on multiple conditions, because of this we need a seperate flag
-
-	QuickSpellPanel(BattleInterface & owner);
-
-	void create();
-
-	std::vector<std::tuple<SpellID, bool>> getSpells() const;
-
-	void show(Canvas & to) override;
-	void inputModeChanged(InputMode modi) override;
-};
-
-class HeroInfoBasicPanel : public CIntObject //extracted from InfoWindow to fit better as non-popup embed element
-{
-private:
-	std::shared_ptr<CPicture> background;
-	std::vector<std::shared_ptr<CLabel>> labels;
-	std::vector<std::shared_ptr<CAnimImage>> icons;
-public:
-	HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground = true);
-
-	void show(Canvas & to) override;
-
-	void initializeData(const InfoAboutHero & hero);
-	void update(const InfoAboutHero & updatedInfo);
-};
-
-class StackInfoBasicPanel : public CIntObject
-{
-private:
-	std::shared_ptr<CPicture> background;
-	std::shared_ptr<CPicture> background2;
-	std::vector<std::shared_ptr<CLabel>> labels;
-	std::vector<std::shared_ptr<CMultiLineLabel>> labelsMultiline;
-	std::vector<std::shared_ptr<CAnimImage>> icons;
-public:
-	StackInfoBasicPanel(const CStack * stack, bool initializeBackground = true);
-
-	void show(Canvas & to) override;
-
-	void initializeData(const CStack * stack);
-	void update(const CStack * updatedInfo);
-};
-
-class HeroInfoWindow : public CWindowObject
-{
-private:
-	std::shared_ptr<HeroInfoBasicPanel> content;
-public:
-	HeroInfoWindow(const InfoAboutHero & hero, Point * position);
-};
-
-struct BattleResultResources
-{
-	VideoPath prologueVideo;
-	VideoPath loopedVideo;
-	AudioPath musicName;
-	MetaString resultText;
-};
-
-/// Class which is responsible for showing the battle result window
-class BattleResultWindow : public WindowBase
-{
-private:
-	std::shared_ptr<CPicture> background;
-	std::vector<std::shared_ptr<CLabel>> labels;
-	std::shared_ptr<CButton> exit;
-	std::shared_ptr<CButton> repeat;
-	std::vector<std::shared_ptr<CAnimImage>> icons;
-	std::shared_ptr<CTextBox> description;
-	std::shared_ptr<VideoWidget> videoPlayer;
-	CPlayerInterface & owner;
-
-	BattleResultResources getResources(const BattleResult & br);
-	
-	void buttonPressed(int button); //internal function for button callbacks
-public:
-	BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay = false);
-
-	void bExitf(); //exit button callback
-	void bRepeatf(); //repeat button callback
-	std::function<void(int result)> resultCallback; //callback receiving which button was pressed
-
-	void activate() override;
-};
-
-/// Shows the stack queue
-class StackQueue : public CIntObject
-{
-	class StackBox : public CIntObject
-	{
-		StackQueue * owner;
-		std::optional<uint32_t> boundUnitID;
-
-		std::shared_ptr<CPicture> background;
-		std::shared_ptr<CAnimImage> icon;
-		std::shared_ptr<CLabel> amount;
-		std::shared_ptr<CPicture> waitIcon;
-		std::shared_ptr<CPicture> defendIcon;
-		std::shared_ptr<CLabel> round;
-		std::shared_ptr<TransparentFilledRectangle> roundRect;
-
-		void show(Canvas & to) override;
-		void showAll(Canvas & to) override;
-		void showPopupWindow(const Point & cursorPosition) override;
-
-		bool isBoundUnitHighlighted() const;
-	public:
-		StackBox(StackQueue * owner);
-		void setUnit(const battle::Unit * unit, size_t turn = 0, std::optional<ui32> currentTurn = std::nullopt);
-		std::optional<uint32_t> getBoundUnitID() const;
-	};
-
-	static const int QUEUE_SIZE_BIG = 10;
-	std::shared_ptr<CFilledTexture> background;
-	std::vector<std::shared_ptr<StackBox>> stackBoxes;
-	BattleInterface & owner;
-
-	int32_t getSiegeShooterIconID();
-public:
-	const bool embedded;
-
-	StackQueue(bool Embedded, BattleInterface & owner);
-	void update();
-	std::optional<uint32_t> getHoveredUnitIdIfAny() const;
-
-	void show(Canvas & to) override;
-};

+ 5 - 5
client/battle/BattleRenderer.cpp

@@ -10,14 +10,14 @@
 #include "StdInc.h"
 #include "BattleRenderer.h"
 
-#include "BattleInterface.h"
-#include "BattleInterfaceClasses.h"
 #include "BattleEffectsController.h"
-#include "BattleWindow.h"
-#include "BattleSiegeController.h"
-#include "BattleStacksController.h"
+#include "BattleHero.h"
+#include "BattleInterface.h"
 #include "BattleObstacleController.h"
 #include "BattleOverlayLogVisualizer.h"
+#include "BattleSiegeController.h"
+#include "BattleStacksController.h"
+#include "BattleWindow.h"
 
 void BattleRenderer::collectObjects()
 {

+ 265 - 0
client/battle/BattleResultWindow.cpp

@@ -0,0 +1,265 @@
+/*
+ * BattleResultWindow.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 "BattleResultWindow.h"
+
+#include "BattleWindow.h"
+
+#include "../CPlayerInterface.h"
+#include "../GameEngine.h"
+#include "../gui/Shortcut.h"
+#include "../gui/WindowHandler.h"
+#include "../media/IMusicPlayer.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/Images.h"
+#include "../widgets/TextControls.h"
+#include "../widgets/VideoWidget.h"
+
+#include "../../lib/CStack.h"
+#include "../../lib/ConditionalWait.h"
+#include "../../lib/GameLibrary.h"
+#include "../../lib/StartInfo.h"
+#include "../../lib/battle/CPlayerBattleCallback.h"
+#include "../../lib/callback/CCallback.h"
+#include "../../lib/gameState/InfoAboutArmy.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/networkPacks/PacksForClientBattle.h"
+#include "../../lib/texts/CGeneralTextHandler.h"
+
+BattleResultWindow::BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay)
+	: owner(_owner)
+{
+	OBJECT_CONSTRUCTION;
+
+	background = std::make_shared<CPicture>(ImagePath::builtin("CPRESULT"));
+	background->setPlayerColor(owner.playerID);
+	pos = center(background->pos);
+
+	exit = std::make_shared<CButton>(Point(384, 505), AnimationPath::builtin("iok6432.def"), std::make_pair("", ""), [this](){ bExitf();}, EShortcut::GLOBAL_ACCEPT);
+	exit->setBorderColor(Colors::METALLIC_GOLD);
+
+	if(allowReplay || owner.cb->getStartInfo()->extraOptionsInfo.unlimitedReplay)
+	{
+		repeat = std::make_shared<CButton>(Point(24, 505), AnimationPath::builtin("icn6432.def"), std::make_pair("", ""), [this](){ bRepeatf();}, EShortcut::GLOBAL_CANCEL);
+		repeat->setBorderColor(Colors::METALLIC_GOLD);
+		labels.push_back(std::make_shared<CLabel>(232, 520, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->translate("vcmi.battleResultsWindow.applyResultsLabel")));
+	}
+
+	if(br.winner == BattleSide::ATTACKER)
+	{
+		labels.push_back(std::make_shared<CLabel>(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[410]));
+	}
+	else
+	{
+		labels.push_back(std::make_shared<CLabel>(59, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[411]));
+	}
+
+	if(br.winner == BattleSide::DEFENDER)
+	{
+		labels.push_back(std::make_shared<CLabel>(412, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[410]));
+	}
+	else
+	{
+		labels.push_back(std::make_shared<CLabel>(408, 124, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[411]));
+	}
+
+	labels.push_back(std::make_shared<CLabel>(232, 302, FONT_BIG, ETextAlignment::CENTER, Colors::YELLOW,  LIBRARY->generaltexth->allTexts[407]));
+	labels.push_back(std::make_shared<CLabel>(232, 332, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[408]));
+	labels.push_back(std::make_shared<CLabel>(232, 428, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[409]));
+
+	std::array<std::string, 2> sideNames = {"N/A", "N/A"};
+
+	for(auto i : {BattleSide::ATTACKER, BattleSide::DEFENDER})
+	{
+		auto heroInfo = owner.cb->getBattle(br.battleID)->battleGetHeroInfo(i);
+		const std::array xs = {21, 392};
+
+		if(heroInfo.portraitSource.isValid()) //attacking hero
+		{
+			icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsLarge"), heroInfo.getIconIndex(), 0, xs[static_cast<int>(i)], 38));
+			sideNames[static_cast<int>(i)] = heroInfo.name;
+		}
+		else
+		{
+			auto stacks = owner.cb->getBattle(br.battleID)->battleGetAllStacks();
+			vstd::erase_if(stacks, [i](const CStack * stack) //erase stack of other side and not coming from garrison
+						   {
+							   return stack->unitSide() != i || !stack->base;
+						   });
+
+			auto best = vstd::maxElementByFun(stacks, [](const CStack * stack)
+											  {
+												  return stack->unitType()->getAIValue();
+											  });
+
+			if(best != stacks.end()) //should be always but to be safe...
+			{
+				icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("TWCRPORT"), (*best)->unitType()->getIconIndex(), 0, xs[static_cast<int>(i)], 38));
+				sideNames[static_cast<int>(i)] = (*best)->unitType()->getNamePluralTranslated();
+			}
+		}
+	}
+
+	//printing attacker and defender's names
+	labels.push_back(std::make_shared<CLabel>(89, 37, FONT_SMALL, ETextAlignment::TOPLEFT, Colors::WHITE, sideNames[0]));
+	labels.push_back(std::make_shared<CLabel>(381, 53, FONT_SMALL, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, sideNames[1]));
+
+	//printing casualties
+	for(auto step : {BattleSide::ATTACKER, BattleSide::DEFENDER})
+	{
+		if(br.casualties[step].empty())
+		{
+			labels.push_back(std::make_shared<CLabel>(235, 360 + 97 * static_cast<int>(step), FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[523]));
+		}
+		else
+		{
+			int casualties = br.casualties[step].size();
+			int xPos = 235 - (casualties*32 + (casualties - 1)*10)/2; //increment by 42 with each picture
+			int yPos = 344 + static_cast<int>(step) * 97;
+			for(auto & elem : br.casualties[step])
+			{
+				const auto * creature = elem.first.toEntity(LIBRARY);
+				if (creature->getId() == CreatureID::ARROW_TOWERS )
+					continue; // do not show destroyed towers in battle results
+
+				icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("CPRSMALL"), creature->getIconIndex(), 0, xPos, yPos));
+				std::ostringstream amount;
+				amount<<elem.second;
+				labels.push_back(std::make_shared<CLabel>(xPos + 16, yPos + 42, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE, amount.str()));
+				xPos += 42;
+			}
+		}
+	}
+
+	auto resources = getResources(br);
+
+	description = std::make_shared<CTextBox>(resources.resultText.toString(), Rect(69, 203, 330, 68), 0, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+	videoPlayer = std::make_shared<VideoWidget>(Point(107, 70), resources.prologueVideo, resources.loopedVideo, false);
+
+	ENGINE->music().playMusic(resources.musicName, false, true);
+}
+
+BattleResultResources BattleResultWindow::getResources(const BattleResult & br)
+{
+	//printing result description
+	bool weAreAttacker = owner.cb->getBattle(br.battleID)->battleGetMySide() == BattleSide::ATTACKER;
+	bool weAreDefender = !weAreAttacker;
+	bool weWon = (br.winner == BattleSide::ATTACKER && weAreAttacker) || (br.winner == BattleSide::DEFENDER && !weAreAttacker);
+	bool isSiege = owner.cb->getBattle(br.battleID)->battleGetDefendedTown() != nullptr;
+
+	BattleResultResources resources;
+
+	if(weWon)
+	{
+		if(isSiege && weAreDefender)
+		{
+			resources.musicName = AudioPath::builtin("Music/Defend Castle");
+			resources.prologueVideo = VideoPath::builtin("DEFENDALL.BIK");
+			resources.loopedVideo = VideoPath::builtin("defendloop.bik");
+		}
+		else
+		{
+			resources.musicName = AudioPath::builtin("Music/Win Battle");
+			resources.prologueVideo = VideoPath::builtin("WIN3.BIK");
+			resources.loopedVideo = VideoPath::builtin("WIN3.BIK");
+		}
+
+		switch(br.result)
+		{
+			case EBattleResult::NORMAL:
+				resources.resultText.appendTextID("core.genrltxt.304");
+				break;
+			case EBattleResult::ESCAPE:
+				resources.resultText.appendTextID("core.genrltxt.303");
+				break;
+			case EBattleResult::SURRENDER:
+				resources.resultText.appendTextID("core.genrltxt.302");
+				break;
+			default:
+				throw std::runtime_error("Invalid battle result!");
+		}
+
+		const CGHeroInstance * ourHero = owner.cb->getBattle(br.battleID)->battleGetMyHero();
+		if(ourHero)
+		{
+			resources.resultText.appendTextID("core.genrltxt.305");
+			resources.resultText.replaceTextID(ourHero->getNameTextID());
+			resources.resultText.replaceNumber(br.exp[weAreAttacker ? BattleSide::ATTACKER : BattleSide::DEFENDER]);
+		}
+	}
+	else // we lose
+	{
+		switch(br.result)
+		{
+			case EBattleResult::NORMAL:
+				resources.resultText.appendTextID("core.genrltxt.311");
+				resources.musicName = AudioPath::builtin("Music/LoseCombat");
+				resources.prologueVideo = VideoPath::builtin("LBSTART.BIK");
+				resources.loopedVideo = VideoPath::builtin("LBLOOP.BIK");
+				break;
+			case EBattleResult::ESCAPE:
+				resources.resultText.appendTextID("core.genrltxt.310");
+				resources.musicName = AudioPath::builtin("Music/Retreat Battle");
+				resources.prologueVideo = VideoPath::builtin("RTSTART.BIK");
+				resources.loopedVideo = VideoPath::builtin("RTLOOP.BIK");
+				break;
+			case EBattleResult::SURRENDER:
+				resources.resultText.appendTextID("core.genrltxt.309");
+				resources.musicName = AudioPath::builtin("Music/Surrender Battle");
+				resources.prologueVideo = VideoPath::builtin("SURRENDER.BIK");
+				resources.loopedVideo = VideoPath::builtin("SURRENDER.BIK");
+				break;
+			default:
+				throw std::runtime_error("Invalid battle result!");
+		}
+
+		if(isSiege && weAreDefender)
+		{
+			resources.musicName = AudioPath::builtin("Music/LoseCastle");
+			resources.prologueVideo = VideoPath::builtin("LOSECSTL.BIK");
+			resources.loopedVideo = VideoPath::builtin("LOSECSLP.BIK");
+		}
+	}
+
+	return resources;
+}
+
+void BattleResultWindow::activate()
+{
+	owner.showingDialog->setBusy();
+	CIntObject::activate();
+}
+
+void BattleResultWindow::buttonPressed(int button)
+{
+	if(resultCallback)
+		resultCallback(button);
+
+	CPlayerInterface & intTmp = owner; //copy reference because "this" will be destructed soon
+
+	close();
+
+	if(ENGINE->windows().topWindow<BattleWindow>())
+		ENGINE->windows().popWindows(1); //pop battle interface if present
+
+	//Result window and battle interface are gone. We requested all dialogs to be closed before opening the battle,
+	//so we can be sure that there is no dialogs left on GUI stack.
+	intTmp.showingDialog->setFree();
+}
+
+void BattleResultWindow::bExitf()
+{
+	buttonPressed(0);
+}
+
+void BattleResultWindow::bRepeatf()
+{
+	buttonPressed(1);
+}

+ 60 - 0
client/battle/BattleResultWindow.h

@@ -0,0 +1,60 @@
+/*
+ * BattleResultWindow.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 "../gui/CIntObject.h"
+
+#include "../../lib/texts/MetaString.h"
+#include "../../lib/filesystem/ResourcePath.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+struct BattleResult;
+VCMI_LIB_NAMESPACE_END
+
+class CLabel;
+class CButton;
+class CAnimImage;
+class VideoWidget;
+class CPlayerInterface;
+class CTextBox;
+
+struct BattleResultResources
+{
+	VideoPath prologueVideo;
+	VideoPath loopedVideo;
+	AudioPath musicName;
+	MetaString resultText;
+};
+
+/// Class which is responsible for showing the battle result window
+class BattleResultWindow : public WindowBase
+{
+private:
+	std::shared_ptr<CPicture> background;
+	std::vector<std::shared_ptr<CLabel>> labels;
+	std::shared_ptr<CButton> exit;
+	std::shared_ptr<CButton> repeat;
+	std::vector<std::shared_ptr<CAnimImage>> icons;
+	std::shared_ptr<CTextBox> description;
+	std::shared_ptr<VideoWidget> videoPlayer;
+	CPlayerInterface & owner;
+
+	BattleResultResources getResources(const BattleResult & br);
+
+	void buttonPressed(int button); //internal function for button callbacks
+public:
+	BattleResultWindow(const BattleResult & br, CPlayerInterface & _owner, bool allowReplay = false);
+
+	void bExitf(); //exit button callback
+	void bRepeatf(); //repeat button callback
+	std::function<void(int result)> resultCallback; //callback receiving which button was pressed
+
+	void activate() override;
+};

+ 2 - 3
client/battle/BattleSiegeController.cpp

@@ -11,11 +11,10 @@
 #include "BattleSiegeController.h"
 
 #include "BattleAnimationClasses.h"
-#include "BattleInterface.h"
-#include "BattleInterfaceClasses.h"
-#include "BattleStacksController.h"
 #include "BattleFieldController.h"
+#include "BattleInterface.h"
 #include "BattleRenderer.h"
+#include "BattleStacksController.h"
 
 #include "../CPlayerInterface.h"
 #include "../GameEngine.h"

+ 6 - 7
client/battle/BattleStacksController.cpp

@@ -10,27 +10,26 @@
 #include "StdInc.h"
 #include "BattleStacksController.h"
 
-#include "BattleSiegeController.h"
-#include "BattleInterfaceClasses.h"
-#include "BattleInterface.h"
 #include "BattleActionsController.h"
 #include "BattleAnimationClasses.h"
-#include "BattleFieldController.h"
 #include "BattleEffectsController.h"
+#include "BattleFieldController.h"
+#include "BattleInterface.h"
 #include "BattleProjectileController.h"
-#include "BattleWindow.h"
 #include "BattleRenderer.h"
+#include "BattleSiegeController.h"
+#include "BattleWindow.h"
 #include "CreatureAnimation.h"
 
 #include "../CPlayerInterface.h"
 #include "../GameEngine.h"
 #include "../gui/WindowHandler.h"
 #include "../media/ISoundPlayer.h"
-#include "../render/Colors.h"
 #include "../render/Canvas.h"
-#include "../render/IRenderHandler.h"
+#include "../render/Colors.h"
 #include "../render/Graphics.h"
 #include "../render/IFont.h"
+#include "../render/IRenderHandler.h"
 
 #include "../../lib/CConfigHandler.h"
 #include "../../lib/CRandomGenerator.h"

+ 40 - 118
client/battle/BattleWindow.cpp

@@ -10,28 +10,33 @@
 #include "StdInc.h"
 #include "BattleWindow.h"
 
-#include "BattleInterface.h"
-#include "BattleInterfaceClasses.h"
+#include "BattleActionsController.h"
+#include "BattleConsole.h"
 #include "BattleFieldController.h"
+#include "BattleInterface.h"
 #include "BattleStacksController.h"
-#include "BattleActionsController.h"
+#include "HeroInfoWindow.h"
+#include "QuickSpellPanel.h"
+#include "StackInfoBasicPanel.h"
+#include "StackQueue.h"
+#include "UnitActionPanel.h"
 
 #include "../CPlayerInterface.h"
-#include "../gui/CursorHandler.h"
 #include "../GameEngine.h"
 #include "../GameInstance.h"
+#include "../adventureMap/CInGameConsole.h"
+#include "../adventureMap/TurnTimerWidget.h"
+#include "../gui/CursorHandler.h"
 #include "../gui/Shortcut.h"
 #include "../gui/WindowHandler.h"
-#include "../windows/CSpellWindow.h"
-#include "../widgets/Buttons.h"
-#include "../widgets/Images.h"
-#include "../windows/CMessage.h"
-#include "../windows/CCreatureWindow.h"
 #include "../render/CAnimation.h"
 #include "../render/Canvas.h"
 #include "../render/IRenderHandler.h"
-#include "../adventureMap/CInGameConsole.h"
-#include "../adventureMap/TurnTimerWidget.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/Images.h"
+#include "../windows/CCreatureWindow.h"
+#include "../windows/CMessage.h"
+#include "../windows/CSpellWindow.h"
 #include "../windows/settings/SettingsMainWindow.h"
 
 #include "../../lib/CConfigHandler.h"
@@ -49,9 +54,8 @@
 #include "../../lib/mapObjects/CGHeroInstance.h"
 #include "../../lib/texts/CGeneralTextHandler.h"
 
-BattleWindow::BattleWindow(BattleInterface & Owner):
-	owner(Owner),
-	lastAlternativeAction(PossiblePlayerBattleAction::INVALID)
+BattleWindow::BattleWindow(BattleInterface & Owner)
+	: owner(Owner)
 {
 	OBJECT_CONSTRUCTION;
 	pos.w = 800;
@@ -94,7 +98,6 @@ BattleWindow::BattleWindow(BattleInterface & Owner):
 	addShortcut(EShortcut::BATTLE_CONSOLE_DOWN, std::bind(&BattleWindow::bConsoleDownf, this));
 	addShortcut(EShortcut::BATTLE_TACTICS_NEXT, std::bind(&BattleWindow::bTacticNextStack, this));
 	addShortcut(EShortcut::BATTLE_TACTICS_END, std::bind(&BattleWindow::bTacticPhaseEnd, this));
-	addShortcut(EShortcut::BATTLE_SELECT_ACTION, std::bind(&BattleWindow::bSwitchActionf, this));
 	addShortcut(EShortcut::BATTLE_OPEN_ACTIVE_UNIT, std::bind(&BattleWindow::bOpenActiveUnit, this));
 	addShortcut(EShortcut::BATTLE_OPEN_HOVERED_UNIT, std::bind(&BattleWindow::bOpenHoveredUnit, this));
 
@@ -190,6 +193,9 @@ void BattleWindow::createQuickSpellWindow()
 	quickSpellWindow = std::make_shared<QuickSpellPanel>(owner);
 	quickSpellWindow->moveTo(Point(pos.x - 67, pos.y));
 
+	unitActionWindow = std::make_shared<UnitActionPanel>(owner);
+	unitActionWindow->moveTo(Point(pos.x + pos.w + 15, pos.y));
+
 	if(settings["battle"]["enableQuickSpellPanel"].Bool())
 		showStickyQuickSpellWindow();
 	else
@@ -210,7 +216,7 @@ void BattleWindow::hideStickyQuickSpellWindow()
 	showStickyQuickSpellWindow->Bool() = false;
 
 	quickSpellWindow->disable();
-	quickSpellWindow->isEnabled = false;
+	unitActionWindow->disable();
 
 	setPositionInfoWindow();
 	createTimerInfoWindows();
@@ -224,16 +230,11 @@ void BattleWindow::showStickyQuickSpellWindow()
 
 	auto hero = owner.getBattle()->battleGetMyHero();
 
-	if(ENGINE->screenDimensions().x >= 1050 && hero != nullptr && hero->hasSpellbook())
-	{
-		quickSpellWindow->enable();
-		quickSpellWindow->isEnabled = true;
-	}
-	else
-	{
-		quickSpellWindow->disable();
-		quickSpellWindow->isEnabled = false;
-	}
+	bool quickSpellWindowVisible = ENGINE->screenDimensions().x >= 1050 && hero != nullptr && hero->hasSpellbook();
+	bool unitActionWindowVisible = ENGINE->screenDimensions().x >= 1050;
+
+	quickSpellWindow->setEnabled(quickSpellWindowVisible);
+	unitActionWindow->setEnabled(unitActionWindowVisible);
 
 	setPositionInfoWindow();
 	createTimerInfoWindows();
@@ -244,7 +245,8 @@ void BattleWindow::createTimerInfoWindows()
 {
 	OBJECT_CONSTRUCTION;
 
-	int xOffsetAttacker = quickSpellWindow->isEnabled ? -53 : 0;
+	int xOffsetAttacker = quickSpellWindow->isDisabled() ? 0 : -53;
+	int xOffsetDefender = unitActionWindow->isDisabled() ? 0 : 53;
 
 	if(GAME->interface()->cb->getStartInfo()->turnTimerInfo.battleTimer != 0 || GAME->interface()->cb->getStartInfo()->turnTimerInfo.unitTimer != 0)
 	{
@@ -262,7 +264,7 @@ void BattleWindow::createTimerInfoWindows()
 		if (defender.isValidPlayer())
 		{
 			if (ENGINE->screenDimensions().x >= 1000)
-				defenderTimerWidget = std::make_shared<TurnTimerWidget>(Point(pos.w + 16, 1), defender);
+				defenderTimerWidget = std::make_shared<TurnTimerWidget>(Point(pos.w + 16 + xOffsetDefender, 1), defender);
 			else
 				defenderTimerWidget = std::make_shared<TurnTimerWidget>(Point(pos.w - 78, 135), defender);
 		}
@@ -381,11 +383,13 @@ void BattleWindow::updateQueue()
 
 void BattleWindow::setPositionInfoWindow()
 {
-	int xOffsetAttacker = quickSpellWindow->isEnabled ? -53 : 0;
+	int xOffsetAttacker = quickSpellWindow->isDisabled() ? 0 : -53;
+	int xOffsetDefender = unitActionWindow->isDisabled() ? 0 : 53;
+
 	if(defenderHeroWindow)
 	{
 		Point position = (ENGINE->screenDimensions().x >= 1000)
-				? Point(pos.x + pos.w + 15, pos.y + 60)
+				? Point(pos.x + pos.w + 15 + xOffsetDefender, pos.y + 60)
 				: Point(pos.x + pos.w -79, pos.y + 195);
 		defenderHeroWindow->moveTo(position);
 	}
@@ -399,7 +403,7 @@ void BattleWindow::setPositionInfoWindow()
 	if(defenderStackWindow)
 	{
 		Point position = (ENGINE->screenDimensions().x >= 1000)
-				? Point(pos.x + pos.w + 15, defenderHeroWindow ? defenderHeroWindow->pos.y + 210 : pos.y + 60)
+				? Point(pos.x + pos.w + 15 + xOffsetDefender, defenderHeroWindow ? defenderHeroWindow->pos.y + 210 : pos.y + 60)
 				: Point(pos.x + pos.w -79, defenderHeroWindow ? defenderHeroWindow->pos.y : pos.y + 195);
 		defenderStackWindow->moveTo(position);
 	}
@@ -426,7 +430,7 @@ void BattleWindow::updateStackInfoWindow(const CStack * stack)
 
 	if(stack && stack->unitSide() == BattleSide::DEFENDER)
 	{
-		defenderStackWindow = std::make_shared<StackInfoBasicPanel>(stack);
+		defenderStackWindow = std::make_shared<StackInfoBasicPanel>(stack, true);
 		defenderStackWindow->setEnabled(showInfoWindows);
 	}
 	else
@@ -434,7 +438,7 @@ void BattleWindow::updateStackInfoWindow(const CStack * stack)
 	
 	if(stack && stack->unitSide() == BattleSide::ATTACKER)
 	{
-		attackerStackWindow = std::make_shared<StackInfoBasicPanel>(stack);
+		attackerStackWindow = std::make_shared<StackInfoBasicPanel>(stack, true);
 		attackerStackWindow->setEnabled(showInfoWindows);
 	}
 	else
@@ -505,12 +509,9 @@ void BattleWindow::tacticPhaseStarted()
 	auto menuTactics = widget<CIntObject>("menuTactics");
 	auto tacticNext = widget<CIntObject>("tacticNext");
 	auto tacticEnd = widget<CIntObject>("tacticEnd");
-	auto alternativeAction = widget<CIntObject>("alternativeAction");
 
 	menuBattle->disable();
 	console->disable();
-	if (alternativeAction)
-		alternativeAction->disable();
 
 	menuTactics->enable();
 	tacticNext->enable();
@@ -526,12 +527,9 @@ void BattleWindow::tacticPhaseEnded()
 	auto menuTactics = widget<CIntObject>("menuTactics");
 	auto tacticNext = widget<CIntObject>("tacticNext");
 	auto tacticEnd = widget<CIntObject>("tacticEnd");
-	auto alternativeAction = widget<CIntObject>("alternativeAction");
 
 	menuBattle->enable();
 	console->enable();
-	if (alternativeAction)
-		alternativeAction->enable();
 
 	menuTactics->disable();
 	tacticNext->disable();
@@ -618,63 +616,9 @@ void BattleWindow::reallySurrender()
 	}
 }
 
-void BattleWindow::showAlternativeActionIcon(PossiblePlayerBattleAction action)
+void BattleWindow::setPossibleActions(const std::vector<PossiblePlayerBattleAction> & actions)
 {
-	auto w = widget<CButton>("alternativeAction");
-	if(!w)
-		return;
-	
-	AnimationPath iconName = AnimationPath::fromJson(variables["actionIconDefault"]);
-	switch(action.get())
-	{
-		case PossiblePlayerBattleAction::ATTACK:
-			iconName = AnimationPath::fromJson(variables["actionIconAttack"]);
-			break;
-			
-		case PossiblePlayerBattleAction::SHOOT:
-			iconName = AnimationPath::fromJson(variables["actionIconShoot"]);
-			break;
-			
-		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-			iconName = AnimationPath::fromJson(variables["actionIconSpell"]);
-			break;
-
-		case PossiblePlayerBattleAction::ANY_LOCATION:
-			iconName = AnimationPath::fromJson(variables["actionIconSpell"]);
-			break;
-			
-		//TODO: figure out purpose of this icon
-		//case PossiblePlayerBattleAction::???:
-			//iconName = variables["actionIconWalk"].String();
-			//break;
-			
-		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
-			iconName = AnimationPath::fromJson(variables["actionIconReturn"]);
-			break;
-			
-		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
-			iconName = AnimationPath::fromJson(variables["actionIconNoReturn"]);
-			break;
-	}
-
-	w->setImage(iconName);
-	w->redraw();
-}
-
-void BattleWindow::setAlternativeActions(const std::list<PossiblePlayerBattleAction> & actions)
-{
-	assert(actions.size() != 1);
-
-	alternativeActions = actions;
-	lastAlternativeAction = PossiblePlayerBattleAction::INVALID;
-
-	if(alternativeActions.size() > 1)
-	{
-		lastAlternativeAction = alternativeActions.back();
-		showAlternativeActionIcon(alternativeActions.front());
-	}
-	else
-		showAlternativeActionIcon(PossiblePlayerBattleAction::INVALID);
+	unitActionWindow->setPossibleActions(actions);
 }
 
 void BattleWindow::bAutofightf()
@@ -764,28 +708,6 @@ void BattleWindow::bSpellf()
 	}
 }
 
-void BattleWindow::bSwitchActionf()
-{
-	if(alternativeActions.empty())
-		return;
-
-	auto actions = owner.actionsController->getPossibleActions();
-
-	if(!actions.empty() && actions.front() != lastAlternativeAction)
-	{
-		owner.actionsController->removePossibleAction(alternativeActions.front());
-		showAlternativeActionIcon(*std::next(alternativeActions.begin()));
-	}
-	else
-	{
-		owner.actionsController->resetCurrentStackPossibleActions();
-		showAlternativeActionIcon(owner.actionsController->getPossibleActions().front());
-	}
-	
-	alternativeActions.push_back(alternativeActions.front());
-	alternativeActions.pop_front();
-}
-
 void BattleWindow::bWaitf()
 {
 	if (owner.actionsController->heroSpellcastingModeActive())
@@ -853,7 +775,6 @@ void BattleWindow::blockUI(bool on)
 	setShortcutBlocked(EShortcut::BATTLE_CAST_SPELL, on || owner.tacticsMode || !canCastSpells);
 	setShortcutBlocked(EShortcut::BATTLE_WAIT, on || owner.tacticsMode || !canWait);
 	setShortcutBlocked(EShortcut::BATTLE_DEFEND, on || owner.tacticsMode);
-	setShortcutBlocked(EShortcut::BATTLE_SELECT_ACTION, on || owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_AUTOCOMBAT, (settings["battle"]["endWithAutocombat"].Bool() && onlyOnePlayerHuman) ? on || owner.tacticsMode || owner.actionsController->heroSpellcastingModeActive() : owner.actionsController->heroSpellcastingModeActive());
 	setShortcutBlocked(EShortcut::BATTLE_END_WITH_AUTOCOMBAT, on || !onlyOnePlayerHuman || owner.actionsController->heroSpellcastingModeActive());
 	setShortcutBlocked(EShortcut::BATTLE_TACTICS_END, on || !owner.tacticsMode);
@@ -862,6 +783,7 @@ void BattleWindow::blockUI(bool on)
 	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode);
 
 	quickSpellWindow->setInputEnabled(!on);
+	unitActionWindow->setInputEnabled(!on);
 }
 
 void BattleWindow::bOpenActiveUnit()

+ 4 - 8
client/battle/BattleWindow.h

@@ -28,6 +28,7 @@ class TurnTimerWidget;
 class HeroInfoBasicPanel;
 class StackInfoBasicPanel;
 class QuickSpellPanel;
+class UnitActionPanel;
 
 /// GUI object that handles functionality of panel at the bottom of combat screen
 class BattleWindow : public InterfaceObjectConfigurable
@@ -42,6 +43,7 @@ class BattleWindow : public InterfaceObjectConfigurable
 	std::shared_ptr<StackInfoBasicPanel> defenderStackWindow;
 
 	std::shared_ptr<QuickSpellPanel> quickSpellWindow;
+	std::shared_ptr<UnitActionPanel> unitActionWindow;
 
 	std::shared_ptr<TurnTimerWidget> attackerTimerWidget;
 	std::shared_ptr<TurnTimerWidget> defenderTimerWidget;
@@ -53,7 +55,6 @@ class BattleWindow : public InterfaceObjectConfigurable
 	void bAutofightf();
 	void bSpellf();
 	void bWaitf();
-	void bSwitchActionf();
 	void bDefencef();
 	void bConsoleUpf();
 	void bConsoleDownf();
@@ -66,11 +67,6 @@ class BattleWindow : public InterfaceObjectConfigurable
 	void reallyFlee();
 	void reallySurrender();
 	
-	/// management of alternative actions
-	std::list<PossiblePlayerBattleAction> alternativeActions;
-	PossiblePlayerBattleAction lastAlternativeAction;
-	void showAlternativeActionIcon(PossiblePlayerBattleAction);
-
 	void useSpellIfPossible(int slot);
 
 	/// flip battle queue visibility to opposite
@@ -140,8 +136,8 @@ public:
 	/// Toggle UI to displaying battle log in place of tactics UI
 	void tacticPhaseEnded();
 
-	/// Set possible alternative options. If more than 1 - the last will be considered as default option
-	void setAlternativeActions(const std::list<PossiblePlayerBattleAction> &);
+	/// Set possible alternative options to fill unit actions panel
+	void setPossibleActions(const std::vector<PossiblePlayerBattleAction> & allActions);
 
 	/// ends battle with autocombat
 	void endWithAutocombat();

+ 97 - 0
client/battle/HeroInfoWindow.cpp

@@ -0,0 +1,97 @@
+/*
+ * HeroInfoWindow.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 "HeroInfoWindow.h"
+
+#include "../widgets/Images.h"
+#include "../widgets/TextControls.h"
+
+#include "../../lib/GameLibrary.h"
+#include "../../lib/gameState/InfoAboutArmy.h"
+#include "../../lib/texts/CGeneralTextHandler.h"
+
+HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, const Point * position, bool initializeBackground)
+	: CIntObject(0)
+{
+	OBJECT_CONSTRUCTION;
+	if(position != nullptr)
+		moveTo(*position);
+
+	if(initializeBackground)
+	{
+		background = std::make_shared<CPicture>(ImagePath::builtin("CHRPOP"));
+		background->setPlayerColor(hero.owner);
+	}
+
+	initializeData(hero);
+}
+
+void HeroInfoBasicPanel::initializeData(const InfoAboutHero & hero)
+{
+	OBJECT_CONSTRUCTION;
+	auto attack = hero.details->primskills[0];
+	auto defense = hero.details->primskills[1];
+	auto power = hero.details->primskills[2];
+	auto knowledge = hero.details->primskills[3];
+	auto morale = hero.details->morale;
+	auto luck = hero.details->luck;
+	auto currentSpellPoints = hero.details->mana;
+	auto maxSpellPoints = hero.details->manaLimit;
+
+	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("PortraitsLarge"), hero.getIconIndex(), 0, 10, 6));
+
+	//primary stats
+	labels.push_back(std::make_shared<CLabel>(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[380] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 87, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[381] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 99, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[382] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 111, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[383] + ":"));
+
+	labels.push_back(std::make_shared<CLabel>(69, 87, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(attack)));
+	labels.push_back(std::make_shared<CLabel>(69, 99, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(defense)));
+	labels.push_back(std::make_shared<CLabel>(69, 111, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(power)));
+	labels.push_back(std::make_shared<CLabel>(69, 123, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(knowledge)));
+
+	//morale+luck
+	labels.push_back(std::make_shared<CLabel>(9, 131, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[384] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 143, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[385] + ":"));
+
+	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("IMRL22"), std::clamp(morale + 3, 0, 6), 0, 47, 131));
+	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("ILCK22"), std::clamp(luck + 3, 0, 6), 0, 47, 143));
+
+	//spell points
+	labels.push_back(std::make_shared<CLabel>(39, 174, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[387]));
+	labels.push_back(std::make_shared<CLabel>(39, 186, EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, std::to_string(currentSpellPoints) + "/" + std::to_string(maxSpellPoints)));
+}
+
+void HeroInfoBasicPanel::update(const InfoAboutHero & updatedInfo)
+{
+	icons.clear();
+	labels.clear();
+
+	initializeData(updatedInfo);
+}
+
+void HeroInfoBasicPanel::show(Canvas & to)
+{
+	showAll(to);
+	CIntObject::show(to);
+}
+
+HeroInfoWindow::HeroInfoWindow(const InfoAboutHero & hero, const Point * position)
+	: CWindowObject(RCLICK_POPUP | SHADOW_DISABLED, ImagePath::builtin("CHRPOP"))
+{
+	OBJECT_CONSTRUCTION;
+	if(position != nullptr)
+		moveTo(*position);
+
+	background->setPlayerColor(hero.owner); //maybe add this functionality to base class?
+
+	content = std::make_shared<HeroInfoBasicPanel>(hero, nullptr, false);
+}

+ 44 - 0
client/battle/HeroInfoWindow.h

@@ -0,0 +1,44 @@
+/*
+ * HeroInfoWindow.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 "../windows/CWindowObject.h"
+
+class CLabel;
+class CAnimImage;
+
+VCMI_LIB_NAMESPACE_BEGIN
+struct InfoAboutHero;
+VCMI_LIB_NAMESPACE_END
+
+class HeroInfoBasicPanel : public CIntObject //extracted from InfoWindow to fit better as non-popup embed element
+{
+private:
+	std::shared_ptr<CPicture> background;
+	std::vector<std::shared_ptr<CLabel>> labels;
+	std::vector<std::shared_ptr<CAnimImage>> icons;
+
+public:
+	HeroInfoBasicPanel(const InfoAboutHero & hero, const Point * position, bool initializeBackground = true);
+
+	void show(Canvas & to) override;
+
+	void initializeData(const InfoAboutHero & hero);
+	void update(const InfoAboutHero & updatedInfo);
+};
+
+class HeroInfoWindow : public CWindowObject
+{
+private:
+	std::shared_ptr<HeroInfoBasicPanel> content;
+
+public:
+	HeroInfoWindow(const InfoAboutHero & hero, const Point * position);
+};

+ 151 - 0
client/battle/QuickSpellPanel.cpp

@@ -0,0 +1,151 @@
+/*
+ * QuickSpellPanel.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 "QuickSpellPanel.h"
+
+#include "BattleInterface.h"
+
+#include "../GameEngine.h"
+#include "../eventsSDL/InputHandler.h"
+#include "../gui/WindowHandler.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../widgets/Images.h"
+#include "../widgets/TextControls.h"
+#include "../windows/CSpellWindow.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/GameLibrary.h"
+#include "../../lib/battle/CPlayerBattleCallback.h"
+#include "../../lib/json/JsonUtils.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/spells/CSpellHandler.h"
+
+QuickSpellPanel::QuickSpellPanel(BattleInterface & owner)
+	: CIntObject(0)
+	, owner(owner)
+{
+	OBJECT_CONSTRUCTION;
+
+	addUsedEvents(LCLICK | SHOW_POPUP | MOVE | INPUT_MODE_CHANGE);
+
+	pos = Rect(0, 0, 52, 600);
+	background = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), pos);
+	rect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w + 1, pos.h + 1), ColorRGBA(0, 0, 0, 0), ColorRGBA(241, 216, 120, 255));
+
+	create();
+}
+
+std::vector<std::tuple<SpellID, bool>> QuickSpellPanel::getSpells() const
+{
+	std::vector<SpellID> spellIds;
+	std::vector<bool> spellIdsFromSetting;
+	for(int i = 0; i < QUICKSPELL_SLOTS; i++)
+	{
+		std::string spellIdentifier = persistentStorage["quickSpell"][std::to_string(i)].String();
+		SpellID id;
+		try
+		{
+			id = SpellID::decode(spellIdentifier);
+		}
+		catch(const IdentifierResolutionException &)
+		{
+			id = SpellID::NONE;
+		}
+		spellIds.push_back(id);
+		spellIdsFromSetting.push_back(id != SpellID::NONE);
+	}
+
+	// autofill empty slots with spells if possible
+	const auto * hero = owner.getBattle()->battleGetMyHero();
+	for(int i = 0; i < QUICKSPELL_SLOTS; i++)
+	{
+		if(spellIds[i] != SpellID::NONE)
+			continue;
+
+		for(const auto & availableSpellID : LIBRARY->spellh->getDefaultAllowed())
+		{
+			const auto * availableSpell = availableSpellID.toSpell();
+			if(!availableSpell->isAdventure() && !availableSpell->isCreatureAbility() && hero->canCastThisSpell(availableSpell) && !vstd::contains(spellIds, availableSpell->getId()))
+			{
+				spellIds[i] = availableSpell->getId();
+				break;
+			}
+		}
+	}
+
+	std::vector<std::tuple<SpellID, bool>> ret;
+	for(int i = 0; i < QUICKSPELL_SLOTS; i++)
+		ret.push_back(std::make_tuple(spellIds[i], spellIdsFromSetting[i]));
+	return ret;
+}
+
+void QuickSpellPanel::create()
+{
+	OBJECT_CONSTRUCTION;
+
+	const JsonNode config = JsonUtils::assembleFromFiles("config/shortcutsConfig");
+
+	labels.clear();
+	buttons.clear();
+	buttonsDisabled.clear();
+
+	const auto * hero = owner.getBattle()->battleGetMyHero();
+	if(!hero)
+		return;
+
+	auto spells = getSpells();
+	for(int i = 0; i < QUICKSPELL_SLOTS; i++)
+	{
+		SpellID id;
+		bool fromSettings;
+		std::tie(id, fromSettings) = spells[i];
+
+		auto button = std::make_shared<CButton>(Point(2, 7 + 50 * i), AnimationPath::builtin("spellint"), CButton::tooltip(), [this, id, hero](){
+													if(id.hasValue() && id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, hero))
+													{
+														owner.castThisSpell(id);
+													}
+												});
+		button->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("spellint"), id != SpellID::NONE ? id.num + 1 : 0));
+		button->addPopupCallback([this, i, hero](){
+									 ENGINE->input().hapticFeedback();
+									 ENGINE->windows().createAndPushWindow<CSpellWindow>(hero, owner.curInt.get(), true, [this, i](SpellID spell){
+																							 Settings configID = persistentStorage.write["quickSpell"][std::to_string(i)];
+																							 configID->String() = spell == SpellID::NONE ? "" : spell.toSpell()->identifier;
+																							 create();
+																						 });
+								 });
+
+		if(fromSettings)
+			buttonsIsAutoGenerated.push_back(std::make_shared<TransparentFilledRectangle>(Rect(45, 37 + 50 * i, 5, 5), Colors::ORANGE));
+
+		if(!id.hasValue() || !id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, hero))
+		{
+			buttonsDisabled.push_back(std::make_shared<TransparentFilledRectangle>(Rect(2, 7 + 50 * i, 48, 36), ColorRGBA(0, 0, 0, 172)));
+		}
+		if(ENGINE->input().getCurrentInputMode() == InputMode::KEYBOARD_AND_MOUSE)
+			labels.push_back(std::make_shared<CLabel>(7, 10 + 50 * i, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, config["keyboard"]["battleSpellShortcut" + std::to_string(i)].String()));
+
+		buttons.push_back(button);
+	}
+}
+
+void QuickSpellPanel::show(Canvas & to)
+{
+	showAll(to);
+	CIntObject::show(to);
+}
+
+void QuickSpellPanel::inputModeChanged(InputMode modi)
+{
+	create();
+	redraw();
+}

+ 43 - 0
client/battle/QuickSpellPanel.h

@@ -0,0 +1,43 @@
+/*
+ * QuickSpellPanel.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 "../gui/CIntObject.h"
+
+class CFilledTexture;
+class TransparentFilledRectangle;
+class CButton;
+class CLabel;
+class BattleInterface;
+
+class QuickSpellPanel : public CIntObject
+{
+private:
+	std::shared_ptr<CFilledTexture> background;
+	std::shared_ptr<TransparentFilledRectangle> rect;
+	std::vector<std::shared_ptr<CButton>> buttons;
+	std::vector<std::shared_ptr<TransparentFilledRectangle>> buttonsIsAutoGenerated;
+	std::vector<std::shared_ptr<TransparentFilledRectangle>> buttonsDisabled;
+	std::vector<std::shared_ptr<CLabel>> labels;
+
+	BattleInterface & owner;
+
+public:
+	static constexpr int QUICKSPELL_SLOTS = 12;
+
+	QuickSpellPanel(BattleInterface & owner);
+
+	void create();
+
+	std::vector<std::tuple<SpellID, bool>> getSpells() const;
+
+	void show(Canvas & to) override;
+	void inputModeChanged(InputMode modi) override;
+};

+ 147 - 0
client/battle/StackInfoBasicPanel.cpp

@@ -0,0 +1,147 @@
+/*
+ * StackInfoBasicPanel.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 "StackInfoBasicPanel.h"
+
+#include "../widgets/Images.h"
+#include "../widgets/TextControls.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CStack.h"
+#include "../../lib/GameLibrary.h"
+#include "../../lib/spells/CSpellHandler.h"
+#include "../../lib/texts/CGeneralTextHandler.h"
+#include "../../lib/texts/TextOperations.h"
+
+StackInfoBasicPanel::StackInfoBasicPanel(const CStack * stack, bool initializeBackground)
+	: CIntObject(0)
+{
+	OBJECT_CONSTRUCTION;
+
+	if(initializeBackground)
+	{
+		background = std::make_shared<CPicture>(ImagePath::builtin("CCRPOP"));
+		background->pos.y += 37;
+		background->setPlayerColor(stack->getOwner());
+		background2 = std::make_shared<CPicture>(ImagePath::builtin("CHRPOP"));
+		background2->setPlayerColor(stack->getOwner());
+	}
+
+	initializeData(stack);
+}
+
+void StackInfoBasicPanel::initializeData(const CStack * stack)
+{
+	OBJECT_CONSTRUCTION;
+
+	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("TWCRPORT"), stack->creatureId().getNum() + 2, 0, 10, 6));
+	labels.push_back(std::make_shared<CLabel>(10 + 58, 6 + 64, FONT_MEDIUM, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, TextOperations::formatMetric(stack->getCount(), 4)));
+
+	int damageMultiplier = 1;
+	if (stack->hasBonusOfType(BonusType::SIEGE_WEAPON))
+	{
+		static const auto bonusSelector =
+			Selector::sourceTypeSel(BonusSource::ARTIFACT).Or(
+															  Selector::sourceTypeSel(BonusSource::HERO_BASE_SKILL)).And(
+					Selector::typeSubtype(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK)));
+
+		damageMultiplier += stack->valOfBonuses(bonusSelector);
+	}
+
+	auto attack = std::to_string(LIBRARY->creatures()->getByIndex(stack->creatureIndex())->getAttack(stack->isShooter())) + "(" + std::to_string(stack->getAttack(stack->isShooter())) + ")";
+	auto defense = std::to_string(LIBRARY->creatures()->getByIndex(stack->creatureIndex())->getDefense(stack->isShooter())) + "(" + std::to_string(stack->getDefense(stack->isShooter())) + ")";
+	auto damage = std::to_string(damageMultiplier * stack->getMinDamage(stack->isShooter())) + "-" + std::to_string(damageMultiplier * stack->getMaxDamage(stack->isShooter()));
+	auto health = stack->getMaxHealth();
+	auto morale = stack->moraleVal();
+	auto luck = stack->luckVal();
+
+	auto killed = stack->getKilled();
+	auto healthRemaining = TextOperations::formatMetric(std::max<int64_t>(stack->getAvailableHealth() - static_cast<int64_t>(stack->getCount() - 1) * health, 0), 4);
+
+	//primary stats*/
+	labels.push_back(std::make_shared<CLabel>(9, 75, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[380] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 87, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[381] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 99, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[386] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 111, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[389] + ":"));
+
+	labels.push_back(std::make_shared<CLabel>(69, 87, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, attack));
+	labels.push_back(std::make_shared<CLabel>(69, 99, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, defense));
+	labels.push_back(std::make_shared<CLabel>(69, 111, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, damage));
+	labels.push_back(std::make_shared<CLabel>(69, 123, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(health)));
+
+	//morale+luck
+	labels.push_back(std::make_shared<CLabel>(9, 131, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[384] + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 143, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[385] + ":"));
+
+	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("IMRL22"), std::clamp(morale + 3, 0, 6), 0, 47, 131));
+	icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("ILCK22"), std::clamp(luck + 3, 0, 6), 0, 47, 143));
+
+	//extra information
+	labels.push_back(std::make_shared<CLabel>(9, 168, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->translate("vcmi.battleWindow.killed") + ":"));
+	labels.push_back(std::make_shared<CLabel>(9, 180, EFonts::FONT_TINY, ETextAlignment::TOPLEFT, Colors::WHITE, LIBRARY->generaltexth->allTexts[389] + ":"));
+
+	labels.push_back(std::make_shared<CLabel>(69, 180, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(killed)));
+	labels.push_back(std::make_shared<CLabel>(69, 192, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, healthRemaining));
+
+	//spells
+	static const Point firstPos(15, 206); // position of 1st spell box
+	static const Point offset(0, 38);  // offset of each spell box from previous
+
+	for(int i = 0; i < 3; i++)
+		icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("SpellInt"), 78, 0, firstPos.x + offset.x * i, firstPos.y + offset.y * i));
+
+	int printed=0; //how many effect pics have been printed
+	std::vector<SpellID> spells = stack->activeSpells();
+	for(SpellID effect : spells)
+	{
+		//not all effects have graphics (for eg. Acid Breath)
+		//for modded spells iconEffect is added to SpellInt.def
+		const bool hasGraphics = (effect < SpellID::THUNDERBOLT) || (effect >= SpellID::AFTER_LAST);
+
+		if (hasGraphics)
+		{
+			//FIXME: support permanent duration
+			auto spellBonuses = stack->getBonuses(Selector::source(BonusSource::SPELL_EFFECT, BonusSourceID(effect)));
+
+			if (spellBonuses->empty())
+				throw std::runtime_error("Failed to find effects for spell " + effect.toSpell()->getJsonKey());
+
+			int duration = spellBonuses->front()->turnsRemain;
+
+			icons.push_back(std::make_shared<CAnimImage>(AnimationPath::builtin("SpellInt"), effect.getNum() + 1, 0, firstPos.x + offset.x * printed, firstPos.y + offset.y * printed));
+			if(settings["general"]["enableUiEnhancements"].Bool())
+				labels.push_back(std::make_shared<CLabel>(firstPos.x + offset.x * printed + 46, firstPos.y + offset.y * printed + 36, EFonts::FONT_TINY, ETextAlignment::BOTTOMRIGHT, Colors::WHITE, std::to_string(duration)));
+
+			++printed;
+			if(printed >= 3 || (printed == 2 && spells.size() > 3)) // interface limit reached
+				break;
+		}
+	}
+
+	if(spells.empty())
+		labelsMultiline.push_back(std::make_shared<CMultiLineLabel>(Rect(firstPos.x, firstPos.y, 48, 36), EFonts::FONT_TINY, ETextAlignment::CENTER, Colors::WHITE, LIBRARY->generaltexth->allTexts[674]));
+	if(spells.size() > 3)
+		labelsMultiline.push_back(std::make_shared<CMultiLineLabel>(Rect(firstPos.x + offset.x * 2, firstPos.y + offset.y * 2 - 4, 48, 36), EFonts::FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE, "..."));
+}
+
+void StackInfoBasicPanel::update(const CStack * updatedInfo)
+{
+	icons.clear();
+	labels.clear();
+	labelsMultiline.clear();
+
+	initializeData(updatedInfo);
+}
+
+void StackInfoBasicPanel::show(Canvas & to)
+{
+	showAll(to);
+	CIntObject::show(to);
+}

+ 38 - 0
client/battle/StackInfoBasicPanel.h

@@ -0,0 +1,38 @@
+/*
+ * StackInfoBasicPanel.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 "../gui/CIntObject.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+class CStack;
+VCMI_LIB_NAMESPACE_END
+
+class CLabel;
+class CMultiLineLabel;
+class CAnimImage;
+
+class StackInfoBasicPanel : public CIntObject
+{
+private:
+	std::shared_ptr<CPicture> background;
+	std::shared_ptr<CPicture> background2;
+	std::vector<std::shared_ptr<CLabel>> labels;
+	std::vector<std::shared_ptr<CMultiLineLabel>> labelsMultiline;
+	std::vector<std::shared_ptr<CAnimImage>> icons;
+
+public:
+	StackInfoBasicPanel(const CStack * stack, bool initializeBackground);
+
+	void show(Canvas & to) override;
+
+	void initializeData(const CStack * stack);
+	void update(const CStack * updatedInfo);
+};

+ 241 - 0
client/battle/StackQueue.cpp

@@ -0,0 +1,241 @@
+/*
+ * StackQueue.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 "StackQueue.h"
+
+#include "BattleInterface.h"
+#include "BattleSiegeController.h"
+#include "BattleStacksController.h"
+
+#include "../GameEngine.h"
+#include "../gui/WindowHandler.h"
+#include "../render/Canvas.h"
+#include "../render/IFont.h"
+#include "../render/IRenderHandler.h"
+#include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../widgets/Images.h"
+#include "../widgets/TextControls.h"
+#include "../windows/CCreatureWindow.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/CStack.h"
+#include "../../lib/battle/CPlayerBattleCallback.h"
+#include "../../lib/mapObjects/CGTownInstance.h"
+#include "../../lib/texts/TextOperations.h"
+
+StackQueue::StackQueue(bool Embedded, BattleInterface & owner)
+	: owner(owner)
+	, embedded(Embedded)
+{
+	OBJECT_CONSTRUCTION;
+
+	uint32_t queueSize = QUEUE_SIZE_BIG;
+
+	if(embedded)
+	{
+		int32_t queueSmallOutsideYOffset = 65;
+		bool queueSmallOutside = settings["battle"]["queueSmallOutside"].Bool() && (pos.y - queueSmallOutsideYOffset) >= 0;
+		queueSize = std::clamp(static_cast<int>(settings["battle"]["queueSmallSlots"].Float()), 1, queueSmallOutside ? ENGINE->screenDimensions().x / 41 : 19);
+
+		pos.w = queueSize * 41;
+		pos.h = 49;
+		pos.x += parent->pos.w/2 - pos.w/2;
+		pos.y += queueSmallOutside ? -queueSmallOutsideYOffset : 10;
+	}
+	else
+	{
+		pos.w = 800;
+		pos.h = 85;
+		pos.x += 0;
+		pos.y -= pos.h;
+
+		background = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), Rect(0, 0, pos.w, pos.h));
+	}
+
+	stackBoxes.resize(queueSize);
+	for (int i = 0; i < stackBoxes.size(); i++)
+	{
+		stackBoxes[i] = std::make_shared<StackBox>(this);
+		stackBoxes[i]->moveBy(Point(1 + (embedded ? 41 : 80) * i, 0));
+	}
+}
+
+void StackQueue::show(Canvas & to)
+{
+	if(embedded)
+		showAll(to);
+	CIntObject::show(to);
+}
+
+void StackQueue::update()
+{
+	std::vector<battle::Units> queueData;
+
+	owner.getBattle()->battleGetTurnOrder(queueData, stackBoxes.size(), 0);
+
+	size_t boxIndex = 0;
+	ui32 tmpTurn = -1;
+
+	for(size_t turn = 0; turn < queueData.size() && boxIndex < stackBoxes.size(); turn++)
+	{
+		for(size_t unitIndex = 0; unitIndex < queueData[turn].size() && boxIndex < stackBoxes.size(); boxIndex++, unitIndex++)
+		{
+			ui32 currentTurn = owner.round + turn;
+			stackBoxes[boxIndex]->setUnit(queueData[turn][unitIndex], turn, tmpTurn != currentTurn && owner.round != 0 && (!embedded || tmpTurn != -1) ? (std::optional<ui32>)currentTurn : std::nullopt);
+			tmpTurn = currentTurn;
+		}
+	}
+
+	for(; boxIndex < stackBoxes.size(); boxIndex++)
+		stackBoxes[boxIndex]->setUnit(nullptr);
+}
+
+int32_t StackQueue::getSiegeShooterIconID() const
+{
+	return owner.siegeController->getSiegedTown()->getFactionID().getNum();
+}
+
+std::optional<uint32_t> StackQueue::getHoveredUnitIdIfAny() const
+{
+	for(const auto & stackBox : stackBoxes)
+	{
+		if(stackBox->isHovered())
+		{
+			return stackBox->getBoundUnitID();
+		}
+	}
+
+	return std::nullopt;
+}
+
+StackQueue::StackBox::StackBox(StackQueue * owner)
+	: CIntObject(SHOW_POPUP | HOVER)
+	, owner(owner)
+{
+	OBJECT_CONSTRUCTION;
+	background = std::make_shared<CPicture>(ImagePath::builtin(owner->embedded ? "StackQueueSmall" : "StackQueueLarge"));
+
+	pos.w = background->pos.w;
+	pos.h = background->pos.h;
+
+	if(owner->embedded)
+	{
+		icon = std::make_shared<CAnimImage>(AnimationPath::builtin("CPRSMALL"), 0, 0, 5, 2);
+		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 7, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+		roundRect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, 2, 48), ColorRGBA(0, 0, 0, 255), ColorRGBA(0, 255, 0, 255));
+	}
+	else
+	{
+		icon = std::make_shared<CAnimImage>(AnimationPath::builtin("TWCRPORT"), 0, 0, 9, 1);
+		amount = std::make_shared<CLabel>(pos.w/2, pos.h - 8, FONT_MEDIUM, ETextAlignment::CENTER, Colors::WHITE);
+		roundRect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, 15, 18), ColorRGBA(0, 0, 0, 255), ColorRGBA(241, 216, 120, 255));
+		round = std::make_shared<CLabel>(6, 9, FONT_SMALL, ETextAlignment::CENTER, Colors::WHITE);
+
+		Point iconPos(pos.w - 16, pos.h - 16);
+
+		defendIcon = std::make_shared<CPicture>(ImagePath::builtin("battle/QueueDefend"), iconPos);
+		waitIcon = std::make_shared<CPicture>(ImagePath::builtin("battle/QueueWait"), iconPos);
+
+		defendIcon->setEnabled(false);
+		waitIcon->setEnabled(false);
+	}
+	roundRect->disable();
+}
+
+void StackQueue::StackBox::setUnit(const battle::Unit * unit, size_t turn, std::optional<ui32> currentTurn)
+{
+	if(unit)
+	{
+		boundUnitID = unit->unitId();
+		background->setPlayerColor(unit->unitOwner());
+		icon->visible = true;
+
+		// temporary code for mod compatibility:
+		// first, set icon that should definitely exist (arrow tower icon in base vcmi mod)
+		// second, try to switch to icon that should be provided by mod
+		// if mod is not up to date and does have arrow tower icon yet - second setFrame call will fail and retain previously set image
+		// for 1.2 release & later next line should be moved into 'else' block
+		icon->setFrame(unit->creatureIconIndex(), 0);
+		if(unit->unitType()->getId() == CreatureID::ARROW_TOWERS)
+			icon->setFrame(owner->getSiegeShooterIconID(), 1);
+
+		roundRect->setEnabled(currentTurn.has_value());
+		if(!owner->embedded)
+			round->setEnabled(currentTurn.has_value());
+
+		amount->setText(TextOperations::formatMetric(unit->getCount(), 4));
+		if(currentTurn && !owner->embedded)
+		{
+			std::string tmp = std::to_string(*currentTurn);
+			const auto & font = ENGINE->renderHandler().loadFont(FONT_SMALL);
+			int len = font->getStringWidth(tmp);
+			roundRect->pos.w = len + 6;
+			round->pos = Rect(roundRect->pos.center().x, roundRect->pos.center().y, 0, 0);
+			round->setText(tmp);
+		}
+
+		if(!owner->embedded)
+		{
+			bool defended = unit->defended(turn) || (turn > 0 && unit->defended(turn - 1));
+			bool waited = unit->waited(turn) && !defended;
+
+			defendIcon->setEnabled(defended);
+			waitIcon->setEnabled(waited);
+		}
+	}
+	else
+	{
+		boundUnitID = std::nullopt;
+		background->setPlayerColor(PlayerColor::NEUTRAL);
+		icon->visible = false;
+		icon->setFrame(0);
+		amount->setText("");
+		if(!owner->embedded)
+		{
+			defendIcon->setEnabled(false);
+			waitIcon->setEnabled(false);
+		}
+	}
+}
+
+std::optional<uint32_t> StackQueue::StackBox::getBoundUnitID() const
+{
+	return boundUnitID;
+}
+
+bool StackQueue::StackBox::isBoundUnitHighlighted() const
+{
+	auto unitIdsToHighlight = owner->owner.stacksController->getHoveredStacksUnitIds();
+	return vstd::contains(unitIdsToHighlight, getBoundUnitID());
+}
+
+void StackQueue::StackBox::showAll(Canvas & to)
+{
+	CIntObject::showAll(to);
+
+	if(isBoundUnitHighlighted())
+		to.drawBorder(background->pos, Colors::CYAN, 2);
+}
+
+void StackQueue::StackBox::show(Canvas & to)
+{
+	CIntObject::show(to);
+
+	if(isBoundUnitHighlighted())
+		to.drawBorder(background->pos, Colors::CYAN, 2);
+}
+
+void StackQueue::StackBox::showPopupWindow(const Point & cursorPosition)
+{
+	auto stacks = owner->owner.getBattle()->battleGetAllStacks();
+	for(const CStack * stack : stacks)
+		if(boundUnitID.has_value() && stack->unitId() == *boundUnitID)
+			ENGINE->windows().createAndPushWindow<CStackWindow>(stack, true);
+}

+ 69 - 0
client/battle/StackQueue.h

@@ -0,0 +1,69 @@
+/*
+ * StackQueue.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 "../gui/CIntObject.h"
+
+VCMI_LIB_NAMESPACE_BEGIN
+namespace battle
+{
+class Unit;
+}
+VCMI_LIB_NAMESPACE_END
+
+class CLabel;
+class TransparentFilledRectangle;
+class CAnimImage;
+class CFilledTexture;
+class BattleInterface;
+
+/// Shows the stack queue
+class StackQueue : public CIntObject
+{
+	class StackBox : public CIntObject
+	{
+		StackQueue * owner;
+		std::optional<uint32_t> boundUnitID;
+
+		std::shared_ptr<CPicture> background;
+		std::shared_ptr<CAnimImage> icon;
+		std::shared_ptr<CLabel> amount;
+		std::shared_ptr<CPicture> waitIcon;
+		std::shared_ptr<CPicture> defendIcon;
+		std::shared_ptr<CLabel> round;
+		std::shared_ptr<TransparentFilledRectangle> roundRect;
+
+		void show(Canvas & to) override;
+		void showAll(Canvas & to) override;
+		void showPopupWindow(const Point & cursorPosition) override;
+		bool isBoundUnitHighlighted() const;
+
+	public:
+		StackBox(StackQueue * owner);
+		void setUnit(const battle::Unit * unit, size_t turn = 0, std::optional<ui32> currentTurn = std::nullopt);
+		std::optional<uint32_t> getBoundUnitID() const;
+	};
+
+	static const int QUEUE_SIZE_BIG = 10;
+	std::shared_ptr<CFilledTexture> background;
+	std::vector<std::shared_ptr<StackBox>> stackBoxes;
+	BattleInterface & owner;
+
+	int32_t getSiegeShooterIconID() const;
+
+public:
+	const bool embedded;
+
+	StackQueue(bool Embedded, BattleInterface & owner);
+	void update();
+	std::optional<uint32_t> getHoveredUnitIdIfAny() const;
+
+	void show(Canvas & to) override;
+};

+ 154 - 0
client/battle/UnitActionPanel.cpp

@@ -0,0 +1,154 @@
+/*
+ * UnitActionPanel.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 "UnitActionPanel.h"
+
+#include "BattleInterface.h"
+#include "BattleActionsController.h"
+
+#include "../GameEngine.h"
+#include "../eventsSDL/InputHandler.h"
+#include "../gui/WindowHandler.h"
+#include "../widgets/Buttons.h"
+#include "../widgets/GraphicalPrimitiveCanvas.h"
+#include "../widgets/Images.h"
+#include "../widgets/TextControls.h"
+#include "../windows/CSpellWindow.h"
+
+#include "../../lib/CConfigHandler.h"
+#include "../../lib/GameLibrary.h"
+#include "../../lib/battle/CPlayerBattleCallback.h"
+#include "../../lib/json/JsonUtils.h"
+#include "../../lib/mapObjects/CGHeroInstance.h"
+#include "../../lib/spells/CSpellHandler.h"
+
+UnitActionPanel::UnitActionPanel(BattleInterface & owner)
+	: CIntObject(0)
+	, owner(owner)
+{
+	OBJECT_CONSTRUCTION;
+
+	addUsedEvents(LCLICK | SHOW_POPUP | MOVE | INPUT_MODE_CHANGE);
+
+	pos = Rect(0, 0, 52, 600);
+	background = std::make_shared<CFilledTexture>(ImagePath::builtin("DIBOXBCK"), pos);
+	rect = std::make_shared<TransparentFilledRectangle>(Rect(0, 0, pos.w + 1, pos.h + 1), ColorRGBA(0, 0, 0, 0), ColorRGBA(241, 216, 120, 255));
+}
+
+void UnitActionPanel::restoreAllActions()
+{
+	owner.actionsController->resetCurrentStackPossibleActions();
+}
+
+void UnitActionPanel::setActions(int buttonIndex, const std::vector<PossiblePlayerBattleAction> & filteredActions)
+{
+	for (const auto & button : buttons)
+		if (button != buttons.at(buttonIndex))
+			button->setSelectedSilent(false);
+
+	owner.actionsController->setPriorityActions(filteredActions);
+	if (filteredActions.front().spellcast())
+		owner.actionsController->enterCreatureCastingMode();
+	owner.actionsController->setPriorityActions(filteredActions);
+}
+
+void UnitActionPanel::testAndAddAction(const std::vector<PossiblePlayerBattleAction> & allActions, const std::vector<PossiblePlayerBattleAction::Actions> & actionFilter, const ImagePath & iconPath, const std::string & descriptionTextID)
+{
+	std::vector<PossiblePlayerBattleAction> filteredActions;
+
+	for (const auto & action : allActions)
+		if (vstd::contains(actionFilter, action.get()))
+			filteredActions.push_back(action);
+
+	if (filteredActions.empty())
+		return;
+
+	int index = buttons.size();
+
+	const auto & callback = [this, filteredActions, index](bool isSelected){ if (isSelected) setActions(index, filteredActions); else restoreAllActions(); };
+
+	MetaString tooltip;
+	tooltip.appendTextID(descriptionTextID);
+
+	auto button = std::make_shared<CToggleButton>(Point(2, 7 + 50 * index), AnimationPath::builtin("battleUnitAction"), CButton::tooltip(tooltip.toString()), callback);
+	button->setOverlay(std::make_shared<CPicture>(iconPath));
+	button->setHighlightedBorderColor(Colors::WHITE);
+	button->setAllowDeselection(true);
+	buttons.push_back(button);
+}
+
+void UnitActionPanel::testAndAddSpell(const std::vector<PossiblePlayerBattleAction> & allActions, const SpellID & spellFilter)
+{
+	std::vector<PossiblePlayerBattleAction> filteredActions;
+
+	for (const auto & action : allActions)
+		if (action.spellcast() && action.spell() == spellFilter)
+			filteredActions.push_back(action);
+
+	if (filteredActions.empty())
+		return;
+
+	int index = buttons.size();
+	const auto & callback = [this, filteredActions, index](bool isSelected){ if (isSelected) setActions(index, filteredActions); else restoreAllActions();};
+
+	MetaString tooltip;
+	tooltip.appendTextID("core.genrltxt.26");
+	tooltip.replaceName(spellFilter);
+
+	std::string hoverText = tooltip.toString();
+	std::string description = spellFilter.toSpell()->getDescriptionTranslated(0);
+
+
+	auto button = std::make_shared<CToggleButton>(Point(2, 7 + 50 * index), AnimationPath::builtin("battleUnitAction"), CButton::tooltip(hoverText, description), callback);
+	button->setOverlay(std::make_shared<CAnimImage>(AnimationPath::builtin("spellint"), spellFilter.getNum() + 1));
+	button->setHighlightedBorderColor(Colors::WHITE);
+	buttons.push_back(button);
+}
+
+void UnitActionPanel::setPossibleActions(const std::vector<PossiblePlayerBattleAction> & newActions)
+{
+	OBJECT_CONSTRUCTION;
+
+	buttons.clear();
+
+	static const std::vector actionsMove = { PossiblePlayerBattleAction::MOVE_STACK };
+	static const std::vector actionsInfo = { PossiblePlayerBattleAction::CREATURE_INFO, PossiblePlayerBattleAction::HERO_INFO };
+	static const std::vector actionsShoot = { PossiblePlayerBattleAction::SHOOT };
+	static const std::vector actionsGenie = { PossiblePlayerBattleAction::RANDOM_GENIE_SPELL };
+	static const std::vector actionsAttack = { PossiblePlayerBattleAction::ATTACK, PossiblePlayerBattleAction::WALK_AND_ATTACK };
+	static const std::vector actionsReturn = { PossiblePlayerBattleAction::ATTACK_AND_RETURN };
+
+	testAndAddAction(newActions, actionsMove, ImagePath::builtin("battle/actionMove"), "vcmi.battle.action.move");
+	testAndAddAction(newActions, actionsReturn, ImagePath::builtin("battle/actionReturn"), "vcmi.battle.action.return");
+	testAndAddAction(newActions, actionsAttack, ImagePath::builtin("battle/actionAttack"), "vcmi.battle.action.attack");
+	testAndAddAction(newActions, actionsShoot, ImagePath::builtin("battle/actionShoot"), "vcmi.battle.action.shoot");
+	testAndAddAction(newActions, actionsGenie, ImagePath::builtin("battle/actionGenie"), "vcmi.battle.action.genie");
+
+	std::vector<SpellID> spells;
+
+	for (const auto & action : newActions)
+		if (action.spellcast())
+			spells.push_back(action.spell());
+
+
+	for (const auto & spell : spells)
+		testAndAddSpell(newActions, spell);
+
+	// Not really a unit action, so place it at the end
+	testAndAddAction(newActions, actionsInfo, ImagePath::builtin("battle/actionInfo"), "vcmi.battle.action.info");
+
+	redraw();
+}
+
+void UnitActionPanel::show(Canvas & to)
+{
+	showAll(to);
+	CIntObject::show(to);
+}

+ 46 - 0
client/battle/UnitActionPanel.h

@@ -0,0 +1,46 @@
+/*
+ * UnitActionPanel.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 "../gui/CIntObject.h"
+#include "../../lib/battle/PossiblePlayerBattleAction.h"
+#include "../../lib/filesystem/ResourcePath.h"
+
+class CFilledTexture;
+class TransparentFilledRectangle;
+class CToggleButton;
+class CLabel;
+class BattleInterface;
+
+class UnitActionPanel : public CIntObject
+{
+private:
+	std::shared_ptr<CFilledTexture> background;
+	std::shared_ptr<TransparentFilledRectangle> rect;
+	std::vector<std::shared_ptr<CToggleButton>> buttons;
+
+	BattleInterface & owner;
+
+	void testAndAddAction(const std::vector<PossiblePlayerBattleAction> & allActions, const std::vector<PossiblePlayerBattleAction::Actions> & actionFilter, const ImagePath & iconPath, const std::string & descriptionTextID );
+	void testAndAddSpell(const std::vector<PossiblePlayerBattleAction> & allActions, const SpellID & spellFilter );
+
+	void restoreAllActions();
+	void setActions(int buttonIndex, const std::vector<PossiblePlayerBattleAction> & newActions);
+public:
+	static constexpr int ACTION_SLOTS = 12;
+
+	UnitActionPanel(BattleInterface & owner);
+
+	void setPossibleActions(const std::vector<PossiblePlayerBattleAction> & actions);
+
+	std::vector<std::tuple<SpellID, bool>> getSpells() const;
+
+	void show(Canvas & to) override;
+};

+ 5 - 0
client/gui/CIntObject.cpp

@@ -121,6 +121,11 @@ void CIntObject::enable()
 	recActions = ALL_ACTIONS;
 }
 
+bool CIntObject::isDisabled() const
+{
+	return recActions == NO_ACTIONS;
+}
+
 void CIntObject::setEnabled(bool on)
 {
 	if (on)

+ 3 - 0
client/gui/CIntObject.h

@@ -73,6 +73,9 @@ public:
 	void disable();
 	/// activates if needed, all activity enabled (Warning: may not be symmetric with disable if recActions was limited!)
 	void enable();
+	/// returns true if element was disabled via disable() call
+	bool isDisabled() const;
+
 	/// deactivates or activates UI element based on flag
 	void setEnabled(bool on);
 

+ 0 - 1
client/gui/Shortcut.h

@@ -192,7 +192,6 @@ enum class EShortcut
 	BATTLE_CONSOLE_DOWN,
 	BATTLE_TACTICS_NEXT,
 	BATTLE_TACTICS_END,
-	BATTLE_SELECT_ACTION, // Alternative actions toggle
 	BATTLE_TOGGLE_HEROES_STATS,
 	BATTLE_OPEN_ACTIVE_UNIT,
 	BATTLE_OPEN_HOVERED_UNIT,

+ 0 - 1
client/gui/ShortcutHandler.cpp

@@ -229,7 +229,6 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"battleConsoleDown",        EShortcut::BATTLE_CONSOLE_DOWN       },
 		{"battleTacticsNext",        EShortcut::BATTLE_TACTICS_NEXT       },
 		{"battleTacticsEnd",         EShortcut::BATTLE_TACTICS_END        },
-		{"battleSelectAction",       EShortcut::BATTLE_SELECT_ACTION      },
 		{"battleToggleQuickSpell",   EShortcut::BATTLE_TOGGLE_QUICKSPELL   },
 		{"battleSpellShortcut0",     EShortcut::BATTLE_SPELL_SHORTCUT_0   },
 		{"battleSpellShortcut1",     EShortcut::BATTLE_SPELL_SHORTCUT_1   },

+ 11 - 1
client/widgets/Buttons.cpp

@@ -15,7 +15,6 @@
 
 #include "../CPlayerInterface.h"
 #include "../battle/BattleInterface.h"
-#include "../battle/BattleInterfaceClasses.h"
 #include "../eventsSDL/InputHandler.h"
 #include "../GameEngine.h"
 #include "../GameInstance.h"
@@ -63,6 +62,11 @@ void CButton::setBorderColor(std::optional<ColorRGBA> newBorderColor)
 	borderColor = newBorderColor;
 }
 
+void CButton::setHighlightedBorderColor(std::optional<ColorRGBA> newBorderColor)
+{
+	highlightedBorderColor = newBorderColor;
+}
+
 void CButton::addCallback(const std::function<void()> & callback)
 {
 	this->callback += callback;
@@ -379,6 +383,12 @@ void CButton::showAll(Canvas & to)
 
 	if (borderColor)
 		to.drawBorder(Rect::createAround(pos, 1), *borderColor);
+
+	if (highlightedBorderColor && isHighlighted())
+	{
+		to.drawBorder(pos, *highlightedBorderColor);
+		to.drawBorder(Rect(pos.topLeft() + Point(1,1), pos.dimensions() - Point(1,1)), *highlightedBorderColor);
+	}
 }
 
 std::pair<std::string, std::string> CButton::tooltip()

+ 2 - 0
client/widgets/Buttons.h

@@ -73,6 +73,7 @@ class CButton : public ButtonBase
 
 	std::array<std::string, 4> hoverTexts; //texts for statusbar, if empty - first entry will be used
 	std::optional<ColorRGBA> borderColor; // mapping of button state to border color
+	std::optional<ColorRGBA> highlightedBorderColor; // mapping of button state to border color
 	std::string helpBox; //for right-click help
 
 	bool actOnDown; //runs when mouse is pressed down over it, not when up
@@ -88,6 +89,7 @@ protected:
 public:
 	// sets the same border color for all button states.
 	void setBorderColor(std::optional<ColorRGBA> borderColor);
+	void setHighlightedBorderColor(std::optional<ColorRGBA> borderColor);
 
 	/// adds one more callback to on-click actions
 	void addCallback(const std::function<void()> & callback);

+ 0 - 1
client/widgets/Images.cpp

@@ -21,7 +21,6 @@
 #include "../render/Colors.h"
 
 #include "../battle/BattleInterface.h"
-#include "../battle/BattleInterfaceClasses.h"
 
 #include "../CPlayerInterface.h"
 

+ 0 - 1
client/windows/CWindowObject.cpp

@@ -17,7 +17,6 @@
 #include "../GameInstance.h"
 #include "../gui/CursorHandler.h"
 #include "../battle/BattleInterface.h"
-#include "../battle/BattleInterfaceClasses.h"
 #include "../windows/CMessage.h"
 #include "../renderSDL/SDL_PixelAccess.h"
 #include "../render/IImage.h"

+ 0 - 1
config/shortcutsConfig.json

@@ -67,7 +67,6 @@
 		"battleOpenActiveUnit":     "I",
 		"battleOpenHoveredUnit":    "V",
 		"battleRetreat":            "R",
-		"battleSelectAction":       "S",
 		"battleToggleQuickSpell":   "T",
 		"battleSpellShortcut0":     "1",
 		"battleSpellShortcut1":     "2",

+ 3 - 0
config/spells/vcmiAbility.json

@@ -14,6 +14,9 @@
 		"sounds": {
 			"cast": "RESURECT"
 		},
+		"graphics" : {
+			"iconEffect" : "battle/actionGenie.png" // generic icon since H3 does not have one
+		},
 		"levels" : {
 			"base": {
 				"description" : "",

+ 114 - 0
config/widgets/buttons/battleUnitAction.json

@@ -0,0 +1,114 @@
+{
+	"normal" : {
+		"width": 48,
+		"height": 36,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"rect": {"x": 0, "y": 0, "w": 48, "h": 36}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 48, "h": 36},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"pressed" : {
+		"width": 48,
+		"height": 36,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"rect": {"x": 1, "y": 1, "w": 47, "h": 35}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 48, "h": 36},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 3, "y" : 3}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 96 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 48 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 96 ] },
+
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : 2, "y" : -3}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : 2}, "color" : [ 255, 255, 255, 128 ] },
+				]
+			}
+		]
+	},
+	"blocked" : {
+		"width": 48,
+		"height": 36,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"rect": {"x": 0, "y": 0, "w": 48, "h": 36}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 48, "h": 36},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] },
+				
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+				]
+			}
+		]
+	},
+	"highlighted" : {
+		"width": 48,
+		"height": 36,
+		"items" : [
+			{
+				"type": "texture",
+				"image": "DiBoxBck",
+				"rect": {"x": 0, "y": 0, "w": 48, "h": 36}
+			},
+			{
+				"type": "graphicalPrimitive",
+				"rect": {"x": 0, "y": 0, "w": 48, "h": 36},
+				"primitives" : [
+					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] },
+				
+					{ "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 255 ] },
+					
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] },
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+				]
+			}
+		]
+	},
+}

+ 10 - 6
config/widgets/buttons/heroBackpack.json

@@ -100,13 +100,17 @@
 				"primitives" : [
 					{ "type" : "filledBox", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 0, 0, 0, 80 ] },
 				
-					{ "type" : "rectangle", "a" : { "x" : 2, "y" : 2}, "b" : { "x" : -3, "y" : -3}, "color" : [ 255, 255, 255, 255 ] },
-					
-					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 255 ] },
-					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 255 ] },
-					
-					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 160 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : 0, "y" : -1}, "color" : [ 255, 255, 255, 64 ] },
+					{ "type" : "line", "a" : { "x" : 0, "y" : 0}, "b" : { "x" : -1, "y" : 0}, "color" : [ 255, 255, 255, 128 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : 1, "y" : -2}, "color" : [ 255, 255, 255, 80 ] },
 					{ "type" : "line", "a" : { "x" : 1, "y" : 1}, "b" : { "x" : -2, "y" : 1}, "color" : [ 255, 255, 255, 160 ] },
+
+					{ "type" : "line", "a" : { "x" : 1, "y" : -2}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+					{ "type" : "line", "a" : { "x" : -2, "y" : 1}, "b" : { "x" : -2, "y" : -2}, "color" : [ 0, 0, 0, 192 ] },
+
+					{ "type" : "line", "a" : { "x" : 0, "y" : -1}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
+					{ "type" : "line", "a" : { "x" : -1, "y" : 0}, "b" : { "x" : -1, "y" : -1}, "color" : [ 0, 0, 0, 255 ] },
 				]
 			}
 		]

+ 1 - 1
lib/battle/CBattleInfoCallback.cpp

@@ -1901,7 +1901,7 @@ SpellID CBattleInfoCallback::getRandomCastedSpell(vstd::RNG & rand,const CStack
 	if (!bl->size())
 		return SpellID::NONE;
 
-	if(bl->size() == 1)
+	if(bl->size() == 1 && bl->front()->additionalInfo[0] > 0) // there is one random spell -> select it
 		return bl->front()->subtype.as<SpellID>();
 
 	int totalWeight = 0;