Browse Source

Merge pull request #4020 from Laserlicht/quickspell

[1.6] Quickspell support
Ivan Savenko 1 year ago
parent
commit
31caf73383

+ 78 - 0
client/battle/BattleInterfaceClasses.cpp

@@ -31,6 +31,7 @@
 #include "../render/IFont.h"
 #include "../render/IFont.h"
 #include "../render/Graphics.h"
 #include "../render/Graphics.h"
 #include "../widgets/Buttons.h"
 #include "../widgets/Buttons.h"
+#include "../widgets/CComponent.h"
 #include "../widgets/Images.h"
 #include "../widgets/Images.h"
 #include "../widgets/Slider.h"
 #include "../widgets/Slider.h"
 #include "../widgets/TextControls.h"
 #include "../widgets/TextControls.h"
@@ -39,9 +40,11 @@
 #include "../windows/CMessage.h"
 #include "../windows/CMessage.h"
 #include "../windows/CCreatureWindow.h"
 #include "../windows/CCreatureWindow.h"
 #include "../windows/CSpellWindow.h"
 #include "../windows/CSpellWindow.h"
+#include "../windows/InfoWindows.h"
 #include "../render/CAnimation.h"
 #include "../render/CAnimation.h"
 #include "../render/IRenderHandler.h"
 #include "../render/IRenderHandler.h"
 #include "../adventureMap/CInGameConsole.h"
 #include "../adventureMap/CInGameConsole.h"
+#include "../eventsSDL/InputHandler.h"
 
 
 #include "../../CCallback.h"
 #include "../../CCallback.h"
 #include "../../lib/CStack.h"
 #include "../../lib/CStack.h"
@@ -55,6 +58,8 @@
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/mapObjects/CGTownInstance.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
 #include "../../lib/networkPacks/PacksForClientBattle.h"
 #include "../../lib/TextOperations.h"
 #include "../../lib/TextOperations.h"
+#include "../../lib/json/JsonUtils.h"
+
 
 
 void BattleConsole::showAll(Canvas & to)
 void BattleConsole::showAll(Canvas & to)
 {
 {
@@ -417,6 +422,79 @@ BattleHero::BattleHero(const BattleInterface & owner, const CGHeroInstance * her
 	addUsedEvents(TIME);
 	addUsedEvents(TIME);
 }
 }
 
 
+QuickSpellPanel::QuickSpellPanel(BattleInterface & owner)
+	: CIntObject(0), owner(owner)
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+
+	addUsedEvents(LCLICK | SHOW_POPUP | MOVE);
+
+	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();
+}
+
+void QuickSpellPanel::create()
+{
+	OBJECT_CONSTRUCTION_CAPTURING(255-DISPOSE);
+
+	const JsonNode config = JsonUtils::assembleFromFiles("config/shortcutsConfig");
+
+	labels.clear();
+	buttons.clear();
+	buttonsDisabled.clear();
+
+	auto hero = owner.getBattle()->battleGetMyHero();
+	if(!hero)
+		return;
+
+	for(int i = 0; i < 12; 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;
+		}
+
+		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"), !spellIdentifier.empty() ? id.num + 1 : 0));
+		button->addPopupCallback([this, i, hero](){
+			GH.input().hapticFeedback();
+			GH.windows().createAndPushWindow<CSpellWindow>(hero, owner.curInt.get(), true, [this, i](SpellID spell){
+				Settings configID = persistentStorage.write["quickSpell"][std::to_string(i)];
+				configID->String() = spell.toSpell()->identifier;
+				create();
+			});
+		});
+
+		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)));
+		}
+		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);
+}
+
 HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground)
 HeroInfoBasicPanel::HeroInfoBasicPanel(const InfoAboutHero & hero, Point * position, bool initializeBackground)
 	: CIntObject(0)
 	: CIntObject(0)
 {
 {

+ 22 - 0
client/battle/BattleInterfaceClasses.h

@@ -22,6 +22,7 @@ class CGHeroInstance;
 struct BattleResult;
 struct BattleResult;
 struct InfoAboutHero;
 struct InfoAboutHero;
 class CStack;
 class CStack;
+class CPlayerBattleCallback;
 
 
 namespace battle
 namespace battle
 {
 {
@@ -44,6 +45,7 @@ class TransparentFilledRectangle;
 class CPlayerInterface;
 class CPlayerInterface;
 class BattleRenderer;
 class BattleRenderer;
 class VideoWidget;
 class VideoWidget;
+class QuickSpellPanel;
 
 
 /// Class which shows the console at the bottom of the battle screen and manages the text of the console
 /// 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
 class BattleConsole : public CIntObject, public IStatusBar
@@ -147,6 +149,26 @@ public:
 	BattleHero(const BattleInterface & owner, const CGHeroInstance * hero, bool defender);
 	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>> buttonsDisabled;
+	std::vector<std::shared_ptr<CLabel>> labels;
+
+	BattleInterface & owner;
+public:
+	bool isEnabled; // isActive() is not working on multiple conditions, because of this we need a seperate flag
+
+	QuickSpellPanel(BattleInterface & owner);
+
+	void create();
+
+	void show(Canvas & to) override;
+};
+
 class HeroInfoBasicPanel : public CIntObject //extracted from InfoWindow to fit better as non-popup embed element
 class HeroInfoBasicPanel : public CIntObject //extracted from InfoWindow to fit better as non-popup embed element
 {
 {
 private:
 private:

+ 100 - 5
client/battle/BattleWindow.cpp

@@ -45,8 +45,8 @@
 #include "../../lib/CPlayerState.h"
 #include "../../lib/CPlayerState.h"
 #include "../windows/settings/SettingsMainWindow.h"
 #include "../windows/settings/SettingsMainWindow.h"
 
 
-BattleWindow::BattleWindow(BattleInterface & owner):
-	owner(owner),
+BattleWindow::BattleWindow(BattleInterface & Owner):
+	owner(Owner),
 	lastAlternativeAction(PossiblePlayerBattleAction::INVALID)
 	lastAlternativeAction(PossiblePlayerBattleAction::INVALID)
 {
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
@@ -64,6 +64,20 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 	
 	
 	const JsonNode config(JsonPath::builtin("config/widgets/BattleWindow2.json"));
 	const JsonNode config(JsonPath::builtin("config/widgets/BattleWindow2.json"));
 	
 	
+	addShortcut(EShortcut::BATTLE_TOGGLE_QUICKSPELL, [this](){ this->toggleStickyQuickSpellVisibility();});
+	addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_0,  [this](){ useSpellIfPossible(0);  });
+	addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_1,  [this](){ useSpellIfPossible(1);  });
+	addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_2,  [this](){ useSpellIfPossible(2);  });
+	addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_3,  [this](){ useSpellIfPossible(3);  });
+	addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_4,  [this](){ useSpellIfPossible(4);  });
+	addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_5,  [this](){ useSpellIfPossible(5);  });
+	addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_6,  [this](){ useSpellIfPossible(6);  });
+	addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_7,  [this](){ useSpellIfPossible(7);  });
+	addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_8,  [this](){ useSpellIfPossible(8);  });
+	addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_9,  [this](){ useSpellIfPossible(9);  });
+	addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_10, [this](){ useSpellIfPossible(10); });
+	addShortcut(EShortcut::BATTLE_SPELL_SHORTCUT_11, [this](){ useSpellIfPossible(11); });
+
 	addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this));
 	addShortcut(EShortcut::GLOBAL_OPTIONS, std::bind(&BattleWindow::bOptionsf, this));
 	addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this));
 	addShortcut(EShortcut::BATTLE_SURRENDER, std::bind(&BattleWindow::bSurrenderf, this));
 	addShortcut(EShortcut::BATTLE_RETREAT, std::bind(&BattleWindow::bFleef, this));
 	addShortcut(EShortcut::BATTLE_RETREAT, std::bind(&BattleWindow::bFleef, this));
@@ -95,6 +109,7 @@ BattleWindow::BattleWindow(BattleInterface & owner):
 	owner.fieldController->createHeroes();
 	owner.fieldController->createHeroes();
 
 
 	createQueue();
 	createQueue();
+	createQuickSpellWindow();
 	createStickyHeroInfoWindows();
 	createStickyHeroInfoWindows();
 	createTimerInfoWindows();
 	createTimerInfoWindows();
 
 
@@ -164,10 +179,67 @@ void BattleWindow::createStickyHeroInfoWindows()
 	setPositionInfoWindow();
 	setPositionInfoWindow();
 }
 }
 
 
+void BattleWindow::createQuickSpellWindow()
+{
+	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
+
+	quickSpellWindow = std::make_shared<QuickSpellPanel>(owner);
+	quickSpellWindow->moveTo(Point(pos.x - 67, pos.y));
+
+	if(settings["battle"]["enableQuickSpellPanel"].Bool())
+		showStickyQuickSpellWindow();
+	else
+		hideStickyQuickSpellWindow();
+}
+
+void BattleWindow::toggleStickyQuickSpellVisibility()
+{
+	if(settings["battle"]["enableQuickSpellPanel"].Bool())
+		hideStickyQuickSpellWindow();
+	else
+		showStickyQuickSpellWindow();
+}
+
+void BattleWindow::hideStickyQuickSpellWindow()
+{
+	Settings showStickyQuickSpellWindow = settings.write["battle"]["enableQuickSpellPanel"];
+	showStickyQuickSpellWindow->Bool() = false;
+
+	quickSpellWindow->disable();
+	quickSpellWindow->isEnabled = false;
+
+	setPositionInfoWindow();
+	createTimerInfoWindows();
+	GH.windows().totalRedraw();
+}
+
+void BattleWindow::showStickyQuickSpellWindow()
+{
+	Settings showStickyQuickSpellWindow = settings.write["battle"]["enableQuickSpellPanel"];
+	showStickyQuickSpellWindow->Bool() = true;
+
+	if(GH.screenDimensions().x >= 1050)
+	{
+		quickSpellWindow->enable();
+		quickSpellWindow->isEnabled = true;
+	}
+	else
+	{
+		quickSpellWindow->disable();
+		quickSpellWindow->isEnabled = false;
+	}
+
+	setPositionInfoWindow();
+	createTimerInfoWindows();
+	GH.windows().totalRedraw();
+}
+
 void BattleWindow::createTimerInfoWindows()
 void BattleWindow::createTimerInfoWindows()
 {
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 
 
+	int xOffsetAttacker = quickSpellWindow->isEnabled ? -53 : 0;
+
 	if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.battleTimer != 0 || LOCPLINT->cb->getStartInfo()->turnTimerInfo.unitTimer != 0)
 	if(LOCPLINT->cb->getStartInfo()->turnTimerInfo.battleTimer != 0 || LOCPLINT->cb->getStartInfo()->turnTimerInfo.unitTimer != 0)
 	{
 	{
 		PlayerColor attacker = owner.getBattle()->sideToPlayer(BattleSide::ATTACKER);
 		PlayerColor attacker = owner.getBattle()->sideToPlayer(BattleSide::ATTACKER);
@@ -176,7 +248,7 @@ void BattleWindow::createTimerInfoWindows()
 		if (attacker.isValidPlayer())
 		if (attacker.isValidPlayer())
 		{
 		{
 			if (GH.screenDimensions().x >= 1000)
 			if (GH.screenDimensions().x >= 1000)
-				attackerTimerWidget = std::make_shared<TurnTimerWidget>(Point(-92, 1), attacker);
+				attackerTimerWidget = std::make_shared<TurnTimerWidget>(Point(-92 + xOffsetAttacker, 1), attacker);
 			else
 			else
 				attackerTimerWidget = std::make_shared<TurnTimerWidget>(Point(1, 135), attacker);
 				attackerTimerWidget = std::make_shared<TurnTimerWidget>(Point(1, 135), attacker);
 		}
 		}
@@ -199,6 +271,24 @@ std::shared_ptr<BattleConsole> BattleWindow::buildBattleConsole(const JsonNode &
 	return std::make_shared<BattleConsole>(owner, background, rect.topLeft(), offset, rect.dimensions() );
 	return std::make_shared<BattleConsole>(owner, background, rect.topLeft(), offset, rect.dimensions() );
 }
 }
 
 
+void BattleWindow::useSpellIfPossible(int slot)
+{
+	std::string spellIdentifier = persistentStorage["quickSpell"][std::to_string(slot)].String();
+	SpellID id;
+	try
+	{
+		id = SpellID::decode(spellIdentifier);
+	}
+	catch(const IdentifierResolutionException& e)
+	{
+		return;
+	}
+	if(id.hasValue() && owner.getBattle()->battleGetMyHero() && id.toSpell()->canBeCast(owner.getBattle().get(), spells::Mode::HERO, owner.getBattle()->battleGetMyHero()))
+	{
+		owner.castThisSpell(id);
+	}
+};
+
 void BattleWindow::toggleQueueVisibility()
 void BattleWindow::toggleQueueVisibility()
 {
 {
 	if(settings["battle"]["showQueue"].Bool())
 	if(settings["battle"]["showQueue"].Bool())
@@ -283,10 +373,12 @@ void BattleWindow::showStickyHeroWindows()
 void BattleWindow::updateQueue()
 void BattleWindow::updateQueue()
 {
 {
 	queue->update();
 	queue->update();
+	createQuickSpellWindow();
 }
 }
 
 
 void BattleWindow::setPositionInfoWindow()
 void BattleWindow::setPositionInfoWindow()
 {
 {
+	int xOffsetAttacker = quickSpellWindow->isEnabled ? -53 : 0;
 	if(defenderHeroWindow)
 	if(defenderHeroWindow)
 	{
 	{
 		Point position = (GH.screenDimensions().x >= 1000)
 		Point position = (GH.screenDimensions().x >= 1000)
@@ -297,7 +389,7 @@ void BattleWindow::setPositionInfoWindow()
 	if(attackerHeroWindow)
 	if(attackerHeroWindow)
 	{
 	{
 		Point position = (GH.screenDimensions().x >= 1000)
 		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x - 93, pos.y + 60)
+				? Point(pos.x - 93 + xOffsetAttacker, pos.y + 60)
 				: Point(pos.x + 1, pos.y + 195);
 				: Point(pos.x + 1, pos.y + 195);
 		attackerHeroWindow->moveTo(position);
 		attackerHeroWindow->moveTo(position);
 	}
 	}
@@ -311,7 +403,7 @@ void BattleWindow::setPositionInfoWindow()
 	if(attackerStackWindow)
 	if(attackerStackWindow)
 	{
 	{
 		Point position = (GH.screenDimensions().x >= 1000)
 		Point position = (GH.screenDimensions().x >= 1000)
-				? Point(pos.x - 93, attackerHeroWindow ? attackerHeroWindow->pos.y + 210 : pos.y + 60)
+				? Point(pos.x - 93 + xOffsetAttacker, attackerHeroWindow ? attackerHeroWindow->pos.y + 210 : pos.y + 60)
 				: Point(pos.x + 1, attackerHeroWindow ? attackerHeroWindow->pos.y : pos.y + 195);
 				: Point(pos.x + 1, attackerHeroWindow ? attackerHeroWindow->pos.y : pos.y + 195);
 		attackerStackWindow->moveTo(position);
 		attackerStackWindow->moveTo(position);
 	}
 	}
@@ -346,6 +438,7 @@ void BattleWindow::updateStackInfoWindow(const CStack * stack)
 		attackerStackWindow = nullptr;
 		attackerStackWindow = nullptr;
 	
 	
 	setPositionInfoWindow();
 	setPositionInfoWindow();
+	createTimerInfoWindows();
 }
 }
 
 
 void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero)
 void BattleWindow::heroManaPointsChanged(const CGHeroInstance * hero)
@@ -765,6 +858,8 @@ void BattleWindow::blockUI(bool on)
 	setShortcutBlocked(EShortcut::BATTLE_TACTICS_NEXT, on || !owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_TACTICS_NEXT, on || !owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_DOWN, on && !owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_DOWN, on && !owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode);
 	setShortcutBlocked(EShortcut::BATTLE_CONSOLE_UP, on && !owner.tacticsMode);
+
+	quickSpellWindow->setInputEnabled(!on);
 }
 }
 
 
 void BattleWindow::bOpenActiveUnit()
 void BattleWindow::bOpenActiveUnit()

+ 11 - 0
client/battle/BattleWindow.h

@@ -27,6 +27,7 @@ class StackQueue;
 class TurnTimerWidget;
 class TurnTimerWidget;
 class HeroInfoBasicPanel;
 class HeroInfoBasicPanel;
 class StackInfoBasicPanel;
 class StackInfoBasicPanel;
+class QuickSpellPanel;
 
 
 /// GUI object that handles functionality of panel at the bottom of combat screen
 /// GUI object that handles functionality of panel at the bottom of combat screen
 class BattleWindow : public InterfaceObjectConfigurable
 class BattleWindow : public InterfaceObjectConfigurable
@@ -40,6 +41,8 @@ class BattleWindow : public InterfaceObjectConfigurable
 	std::shared_ptr<StackInfoBasicPanel> attackerStackWindow;
 	std::shared_ptr<StackInfoBasicPanel> attackerStackWindow;
 	std::shared_ptr<StackInfoBasicPanel> defenderStackWindow;
 	std::shared_ptr<StackInfoBasicPanel> defenderStackWindow;
 
 
+	std::shared_ptr<QuickSpellPanel> quickSpellWindow;
+
 	std::shared_ptr<TurnTimerWidget> attackerTimerWidget;
 	std::shared_ptr<TurnTimerWidget> attackerTimerWidget;
 	std::shared_ptr<TurnTimerWidget> defenderTimerWidget;
 	std::shared_ptr<TurnTimerWidget> defenderTimerWidget;
 
 
@@ -68,12 +71,16 @@ class BattleWindow : public InterfaceObjectConfigurable
 	PossiblePlayerBattleAction lastAlternativeAction;
 	PossiblePlayerBattleAction lastAlternativeAction;
 	void showAlternativeActionIcon(PossiblePlayerBattleAction);
 	void showAlternativeActionIcon(PossiblePlayerBattleAction);
 
 
+	void useSpellIfPossible(int slot);
+
 	/// flip battle queue visibility to opposite
 	/// flip battle queue visibility to opposite
 	void toggleQueueVisibility();
 	void toggleQueueVisibility();
 	void createQueue();
 	void createQueue();
 
 
 	void toggleStickyHeroWindowsVisibility();
 	void toggleStickyHeroWindowsVisibility();
+	void toggleStickyQuickSpellVisibility();
 	void createStickyHeroInfoWindows();
 	void createStickyHeroInfoWindows();
+	void createQuickSpellWindow();
 	void createTimerInfoWindows();
 	void createTimerInfoWindows();
 
 
 	std::shared_ptr<BattleConsole> buildBattleConsole(const JsonNode &) const;
 	std::shared_ptr<BattleConsole> buildBattleConsole(const JsonNode &) const;
@@ -94,6 +101,10 @@ public:
 	void hideStickyHeroWindows();
 	void hideStickyHeroWindows();
 	void showStickyHeroWindows();
 	void showStickyHeroWindows();
 
 
+	/// Toggle permanent quickspell windows visibility
+	void hideStickyQuickSpellWindow();
+	void showStickyQuickSpellWindow();
+
 	/// Event handler for netpack changing hero mana points
 	/// Event handler for netpack changing hero mana points
 	void heroManaPointsChanged(const CGHeroInstance * hero);
 	void heroManaPointsChanged(const CGHeroInstance * hero);
 
 

+ 13 - 0
client/gui/Shortcut.h

@@ -186,6 +186,19 @@ enum class EShortcut
 	BATTLE_TOGGLE_HEROES_STATS,
 	BATTLE_TOGGLE_HEROES_STATS,
 	BATTLE_OPEN_ACTIVE_UNIT,
 	BATTLE_OPEN_ACTIVE_UNIT,
 	BATTLE_OPEN_HOVERED_UNIT,
 	BATTLE_OPEN_HOVERED_UNIT,
+	BATTLE_TOGGLE_QUICKSPELL,
+	BATTLE_SPELL_SHORTCUT_0,
+	BATTLE_SPELL_SHORTCUT_1,
+	BATTLE_SPELL_SHORTCUT_2,
+	BATTLE_SPELL_SHORTCUT_3,
+	BATTLE_SPELL_SHORTCUT_4,
+	BATTLE_SPELL_SHORTCUT_5,
+	BATTLE_SPELL_SHORTCUT_6,
+	BATTLE_SPELL_SHORTCUT_7,
+	BATTLE_SPELL_SHORTCUT_8,
+	BATTLE_SPELL_SHORTCUT_9,
+	BATTLE_SPELL_SHORTCUT_10,
+	BATTLE_SPELL_SHORTCUT_11,
 
 
 	MARKET_DEAL,
 	MARKET_DEAL,
 	MARKET_MAX_AMOUNT,
 	MARKET_MAX_AMOUNT,

+ 13 - 0
client/gui/ShortcutHandler.cpp

@@ -222,6 +222,19 @@ EShortcut ShortcutHandler::findShortcut(const std::string & identifier ) const
 		{"battleTacticsNext",        EShortcut::BATTLE_TACTICS_NEXT       },
 		{"battleTacticsNext",        EShortcut::BATTLE_TACTICS_NEXT       },
 		{"battleTacticsEnd",         EShortcut::BATTLE_TACTICS_END        },
 		{"battleTacticsEnd",         EShortcut::BATTLE_TACTICS_END        },
 		{"battleSelectAction",       EShortcut::BATTLE_SELECT_ACTION      },
 		{"battleSelectAction",       EShortcut::BATTLE_SELECT_ACTION      },
+		{"battleToggleQuickSpell",   EShortcut::BATTLE_TOGGLE_QUICKSPELL   },
+		{"battleSpellShortcut0",     EShortcut::BATTLE_SPELL_SHORTCUT_0   },
+		{"battleSpellShortcut1",     EShortcut::BATTLE_SPELL_SHORTCUT_1   },
+		{"battleSpellShortcut2",     EShortcut::BATTLE_SPELL_SHORTCUT_2   },
+		{"battleSpellShortcut3",     EShortcut::BATTLE_SPELL_SHORTCUT_3   },
+		{"battleSpellShortcut4",     EShortcut::BATTLE_SPELL_SHORTCUT_4   },
+		{"battleSpellShortcut5",     EShortcut::BATTLE_SPELL_SHORTCUT_5   },
+		{"battleSpellShortcut6",     EShortcut::BATTLE_SPELL_SHORTCUT_6   },
+		{"battleSpellShortcut7",     EShortcut::BATTLE_SPELL_SHORTCUT_7   },
+		{"battleSpellShortcut8",     EShortcut::BATTLE_SPELL_SHORTCUT_8   },
+		{"battleSpellShortcut9",     EShortcut::BATTLE_SPELL_SHORTCUT_9   },
+		{"battleSpellShortcut10",    EShortcut::BATTLE_SPELL_SHORTCUT_10  },
+		{"battleSpellShortcut11",    EShortcut::BATTLE_SPELL_SHORTCUT_11  },
 		{"spectateTrackHero",        EShortcut::SPECTATE_TRACK_HERO       },
 		{"spectateTrackHero",        EShortcut::SPECTATE_TRACK_HERO       },
 		{"spectateSkipBattle",       EShortcut::SPECTATE_SKIP_BATTLE      },
 		{"spectateSkipBattle",       EShortcut::SPECTATE_SKIP_BATTLE      },
 		{"spectateSkipBattleResult", EShortcut::SPECTATE_SKIP_BATTLE_RESULT },
 		{"spectateSkipBattleResult", EShortcut::SPECTATE_SKIP_BATTLE_RESULT },

+ 7 - 0
client/widgets/Buttons.cpp

@@ -67,6 +67,11 @@ void CButton::addCallback(const std::function<void()> & callback)
 	this->callback += callback;
 	this->callback += callback;
 }
 }
 
 
+void CButton::addPopupCallback(const std::function<void()> & callback)
+{
+	this->callbackPopup += callback;
+}
+
 void ButtonBase::setTextOverlay(const std::string & Text, EFonts font, ColorRGBA color)
 void ButtonBase::setTextOverlay(const std::string & Text, EFonts font, ColorRGBA color)
 {
 {
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);
 	OBJECT_CONSTRUCTION_CUSTOM_CAPTURING(255-DISPOSE);
@@ -289,6 +294,8 @@ void CButton::clickCancel(const Point & cursorPosition)
 
 
 void CButton::showPopupWindow(const Point & cursorPosition)
 void CButton::showPopupWindow(const Point & cursorPosition)
 {
 {
+	callbackPopup();
+
 	if(!helpBox.empty()) //there is no point to show window with nothing inside...
 	if(!helpBox.empty()) //there is no point to show window with nothing inside...
 		CRClickPopup::createAndPush(helpBox);
 		CRClickPopup::createAndPush(helpBox);
 }
 }

+ 2 - 0
client/widgets/Buttons.h

@@ -69,6 +69,7 @@ public:
 class CButton : public ButtonBase
 class CButton : public ButtonBase
 {
 {
 	CFunctionList<void()> callback;
 	CFunctionList<void()> callback;
+	CFunctionList<void()> callbackPopup;
 
 
 	std::array<std::string, 4> hoverTexts; //texts for statusbar, if empty - first entry will be used
 	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> borderColor; // mapping of button state to border color
@@ -90,6 +91,7 @@ public:
 
 
 	/// adds one more callback to on-click actions
 	/// adds one more callback to on-click actions
 	void addCallback(const std::function<void()> & callback);
 	void addCallback(const std::function<void()> & callback);
+	void addPopupCallback(const std::function<void()> & callback);
 
 
 	void addHoverText(EButtonState state, const std::string & text);
 	void addHoverText(EButtonState state, const std::string & text);
 
 

+ 19 - 2
client/windows/CSpellWindow.cpp

@@ -98,13 +98,15 @@ public:
 	}
 	}
 };
 };
 
 
-CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells):
+CSpellWindow::CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells, std::function<void(SpellID)> onSpellSelect):
 	CWindowObject(PLAYER_COLORED | (settings["gameTweaks"]["enableLargeSpellbook"].Bool() ? BORDERED : 0)),
 	CWindowObject(PLAYER_COLORED | (settings["gameTweaks"]["enableLargeSpellbook"].Bool() ? BORDERED : 0)),
 	battleSpellsOnly(openOnBattleSpells),
 	battleSpellsOnly(openOnBattleSpells),
 	selectedTab(4),
 	selectedTab(4),
 	currentPage(0),
 	currentPage(0),
 	myHero(_myHero),
 	myHero(_myHero),
 	myInt(_myInt),
 	myInt(_myInt),
+	openOnBattleSpells(openOnBattleSpells),
+	onSpellSelect(onSpellSelect),
 	isBigSpellbook(settings["gameTweaks"]["enableLargeSpellbook"].Bool()),
 	isBigSpellbook(settings["gameTweaks"]["enableLargeSpellbook"].Bool()),
 	spellsPerPage(24),
 	spellsPerPage(24),
 	offL(-11),
 	offL(-11),
@@ -293,6 +295,14 @@ void CSpellWindow::processSpells()
 	for(auto const & spell : CGI->spellh->objects)
 	for(auto const & spell : CGI->spellh->objects)
 	{
 	{
 		bool searchTextFound = !searchBox || boost::algorithm::contains(boost::algorithm::to_lower_copy(spell->getNameTranslated()), boost::algorithm::to_lower_copy(searchBox->getText()));
 		bool searchTextFound = !searchBox || boost::algorithm::contains(boost::algorithm::to_lower_copy(spell->getNameTranslated()), boost::algorithm::to_lower_copy(searchBox->getText()));
+
+		if(onSpellSelect)
+		{
+			if(spell->isCombat() == openOnBattleSpells && !spell->isSpecial() && !spell->isCreatureAbility())
+				mySpells.push_back(spell.get());
+			continue;
+		}
+
 		if(!spell->isCreatureAbility() && myHero->canCastThisSpell(spell.get()) && searchTextFound)
 		if(!spell->isCreatureAbility() && myHero->canCastThisSpell(spell.get()) && searchTextFound)
 			mySpells.push_back(spell.get());
 			mySpells.push_back(spell.get());
 	}
 	}
@@ -602,6 +612,13 @@ void CSpellWindow::SpellArea::clickPressed(const Point & cursorPosition)
 {
 {
 	if(mySpell)
 	if(mySpell)
 	{
 	{
+		if(owner->onSpellSelect)
+		{
+			owner->onSpellSelect(mySpell->id);
+			owner->fexitb();
+			return;
+		}
+
 		auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero);
 		auto spellCost = owner->myInt->cb->getSpellCost(mySpell, owner->myHero);
 		if(spellCost > owner->myHero->mana) //insufficient mana
 		if(spellCost > owner->myHero->mana) //insufficient mana
 		{
 		{
@@ -738,7 +755,7 @@ void CSpellWindow::SpellArea::setSpell(const CSpell * spell)
 		}
 		}
 
 
 		ColorRGBA firstLineColor, secondLineColor;
 		ColorRGBA firstLineColor, secondLineColor;
-		if(spellCost > owner->myHero->mana) //hero cannot cast this spell
+		if(spellCost > owner->myHero->mana && !owner->onSpellSelect) //hero cannot cast this spell
 		{
 		{
 			firstLineColor = Colors::WHITE;
 			firstLineColor = Colors::WHITE;
 			secondLineColor = Colors::ORANGE;
 			secondLineColor = Colors::ORANGE;

+ 4 - 1
client/windows/CSpellWindow.h

@@ -115,8 +115,11 @@ class CSpellWindow : public CWindowObject
 
 
 	std::shared_ptr<IImage> createBigSpellBook();
 	std::shared_ptr<IImage> createBigSpellBook();
 
 
+	bool openOnBattleSpells;
+	std::function<void(SpellID)> onSpellSelect; //external processing of selected spell
+
 public:
 public:
-	CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells = true);
+	CSpellWindow(const CGHeroInstance * _myHero, CPlayerInterface * _myInt, bool openOnBattleSpells = true, std::function<void(SpellID)> onSpellSelect = nullptr);
 	~CSpellWindow();
 	~CSpellWindow();
 
 
 	void fexitb();
 	void fexitb();

+ 5 - 1
config/schemas/settings.json

@@ -372,7 +372,7 @@
 			"type" : "object",
 			"type" : "object",
 			"additionalProperties" : false,
 			"additionalProperties" : false,
 			"default" : {},
 			"default" : {},
-			"required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "rangeLimitHighlightOnHover", "showQueue", "swipeAttackDistance", "queueSize", "stickyHeroInfoWindows", "enableAutocombatSpells", "endWithAutocombat", "queueSmallSlots", "queueSmallOutside" ],
+			"required" : [ "speedFactor", "mouseShadow", "cellBorders", "stackRange", "movementHighlightOnHover", "rangeLimitHighlightOnHover", "showQueue", "swipeAttackDistance", "queueSize", "stickyHeroInfoWindows", "enableAutocombatSpells", "endWithAutocombat", "queueSmallSlots", "queueSmallOutside", "enableQuickSpellPanel" ],
 			"properties" : {
 			"properties" : {
 				"speedFactor" : {
 				"speedFactor" : {
 					"type" : "number",
 					"type" : "number",
@@ -430,6 +430,10 @@
 				"queueSmallOutside" : {
 				"queueSmallOutside" : {
 					"type": "boolean",
 					"type": "boolean",
 					"default": false
 					"default": false
+				},
+				"enableQuickSpellPanel" : {
+					"type": "boolean",
+					"default": true
 				}
 				}
 			}
 			}
 		},
 		},

+ 13 - 0
config/shortcutsConfig.json

@@ -64,6 +64,19 @@
 		"battleOpenHoveredUnit":    "V",
 		"battleOpenHoveredUnit":    "V",
 		"battleRetreat":            "R",
 		"battleRetreat":            "R",
 		"battleSelectAction":       "S",
 		"battleSelectAction":       "S",
+		"battleToggleQuickSpell":   "T",
+		"battleSpellShortcut0":     "1",
+		"battleSpellShortcut1":     "2",
+		"battleSpellShortcut2":     "3",
+		"battleSpellShortcut3":     "4",
+		"battleSpellShortcut4":     "5",
+		"battleSpellShortcut5":     "6",
+		"battleSpellShortcut6":     "7",
+		"battleSpellShortcut7":     "8",
+		"battleSpellShortcut8":     "9",
+		"battleSpellShortcut9":     "0",
+		"battleSpellShortcut10":    "N",
+		"battleSpellShortcut11":    "M",
 		"battleSurrender":          "S",
 		"battleSurrender":          "S",
 		"battleTacticsEnd":         [ "Return", "Keypad Enter"],
 		"battleTacticsEnd":         [ "Return", "Keypad Enter"],
 		"battleTacticsNext":        "Space",
 		"battleTacticsNext":        "Space",