فهرست منبع

Moved battle actions handling into a separate class

Ivan Savenko 2 سال پیش
والد
کامیت
02fe0425c7

+ 2 - 0
client/CMakeLists.txt

@@ -6,6 +6,7 @@ set(client_SRCS
 		battle/CBattleControlPanel.cpp
 		battle/CBattleInterfaceClasses.cpp
 		battle/CBattleInterface.cpp
+		battle/CBattleActionsController.cpp
 		battle/CBattleFieldController.cpp
 		battle/CBattleObstacleController.cpp
 		battle/CBattleProjectileController.cpp
@@ -85,6 +86,7 @@ set(client_HEADERS
 		battle/CBattleControlPanel.h
 		battle/CBattleInterfaceClasses.h
 		battle/CBattleInterface.h
+		battle/CBattleActionsController.h
 		battle/CBattleFieldController.h
 		battle/CBattleObstacleController.h
 		battle/CBattleProjectileController.h

+ 768 - 0
client/battle/CBattleActionsController.cpp

@@ -0,0 +1,768 @@
+/*
+ * CBattleActionsController.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 "CBattleActionsController.h"
+
+#include "CBattleControlPanel.h"
+#include "CBattleStacksController.h"
+#include "CBattleInterface.h"
+#include "CBattleFieldController.h"
+#include "CBattleSiegeController.h"
+#include "CBattleInterfaceClasses.h"
+
+#include "../gui/CCursorHandler.h"
+#include "../gui/CGuiHandler.h"
+#include "../gui/CIntObject.h"
+#include "../windows/CCreatureWindow.h"
+#include "../CGameInfo.h"
+#include "../CPlayerInterface.h"
+#include "../../CCallback.h"
+#include "../../lib/CStack.h"
+#include "../../lib/battle/BattleAction.h"
+#include "../../lib/spells/CSpellHandler.h"
+#include "../../lib/spells/ISpellMechanics.h"
+#include "../../lib/spells/Problem.h"
+#include "../../lib/CGeneralTextHandler.h"
+
+static std::string formatDmgRange(std::pair<ui32, ui32> dmgRange)
+{
+	if (dmgRange.first != dmgRange.second)
+		return (boost::format("%d - %d") % dmgRange.first % dmgRange.second).str();
+	else
+		return (boost::format("%d") % dmgRange.first).str();
+}
+
+
+CBattleActionsController::CBattleActionsController(CBattleInterface * owner):
+	owner(owner),
+	creatureCasting(false),
+	spellDestSelectMode(false),
+	spellToCast(nullptr),
+	sp(nullptr)
+{
+	currentAction = PossiblePlayerBattleAction::INVALID;
+	selectedAction = PossiblePlayerBattleAction::INVALID;
+}
+
+void CBattleActionsController::endCastingSpell()
+{
+	if(spellDestSelectMode)
+	{
+		spellToCast.reset();
+
+		sp = nullptr;
+		spellDestSelectMode = false;
+		CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER);
+
+		if(owner->stacksController->getActiveStack())
+		{
+			possibleActions = getPossibleActionsForStack(owner->stacksController->getActiveStack()); //restore actions after they were cleared
+			owner->myTurn = true;
+		}
+	}
+	else
+	{
+		if(owner->stacksController->getActiveStack())
+		{
+			possibleActions = getPossibleActionsForStack(owner->stacksController->getActiveStack());
+			GH.fakeMouseMove();
+		}
+	}
+}
+
+void CBattleActionsController::enterCreatureCastingMode()
+{
+	//silently check for possible errors
+	if (!owner->myTurn)
+		return;
+
+	if (owner->tacticsMode)
+		return;
+
+	//hero is casting a spell
+	if (spellDestSelectMode)
+		return;
+
+	if (!owner->stacksController->getActiveStack())
+		return;
+
+	if (!owner->stacksController->activeStackSpellcaster())
+		return;
+
+	//random spellcaster
+	if (owner->stacksController->activeStackSpellToCast() == SpellID::NONE)
+		return;
+
+	if (vstd::contains(possibleActions, PossiblePlayerBattleAction::NO_LOCATION))
+	{
+		const spells::Caster * caster = owner->stacksController->getActiveStack();
+		const CSpell * spell = owner->stacksController->activeStackSpellToCast().toSpell();
+
+		spells::Target target;
+		target.emplace_back();
+
+		spells::BattleCast cast(owner->curInt->cb.get(), caster, spells::Mode::CREATURE_ACTIVE, spell);
+
+		auto m = spell->battleMechanics(&cast);
+		spells::detail::ProblemImpl ignored;
+
+		const bool isCastingPossible = m->canBeCastAt(target, ignored);
+
+		if (isCastingPossible)
+		{
+			owner->myTurn = false;
+			owner->giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, owner->stacksController->activeStackSpellToCast());
+			owner->stacksController->setSelectedStack(nullptr);
+
+			CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER);
+		}
+	}
+	else
+	{
+		possibleActions = getPossibleActionsForStack(owner->stacksController->getActiveStack());
+
+		auto actionFilterPredicate = [](const PossiblePlayerBattleAction x)
+		{
+			return (x != PossiblePlayerBattleAction::ANY_LOCATION) && (x != PossiblePlayerBattleAction::NO_LOCATION) &&
+				(x != PossiblePlayerBattleAction::FREE_LOCATION) && (x != PossiblePlayerBattleAction::AIMED_SPELL_CREATURE) &&
+				(x != PossiblePlayerBattleAction::OBSTACLE);
+		};
+
+		vstd::erase_if(possibleActions, actionFilterPredicate);
+		GH.fakeMouseMove();
+	}
+}
+
+std::vector<PossiblePlayerBattleAction> CBattleActionsController::getPossibleActionsForStack(const CStack *stack)
+{
+	BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass
+	data.creatureSpellToCast = owner->stacksController->activeStackSpellToCast();
+	data.tacticsMode = owner->tacticsMode;
+	auto allActions = owner->curInt->cb->getClientActionsForStack(stack, data);
+
+	return std::vector<PossiblePlayerBattleAction>(allActions);
+}
+
+void CBattleActionsController::reorderPossibleActionsPriority(const CStack * stack, MouseHoveredHexContext context)
+{
+	if(owner->tacticsMode || possibleActions.empty()) return; //this function is not supposed to be called in tactics mode or before getPossibleActionsForStack
+
+	auto assignPriority = [&](PossiblePlayerBattleAction const & item) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it
+	{
+		switch(item)
+		{
+		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+		case PossiblePlayerBattleAction::ANY_LOCATION:
+		case PossiblePlayerBattleAction::NO_LOCATION:
+		case PossiblePlayerBattleAction::FREE_LOCATION:
+		case PossiblePlayerBattleAction::OBSTACLE:
+			if(!stack->hasBonusOfType(Bonus::NO_SPELLCAST_BY_DEFAULT) && context == MouseHoveredHexContext::OCCUPIED_HEX)
+				return 1;
+			else
+				return 100;//bottom priority
+			break;
+		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
+			return 2; break;
+		case PossiblePlayerBattleAction::RISE_DEMONS:
+			return 3; break;
+		case PossiblePlayerBattleAction::SHOOT:
+			return 4; break;
+		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
+			return 5; break;
+		case PossiblePlayerBattleAction::ATTACK:
+			return 6; break;
+		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
+			return 7; break;
+		case PossiblePlayerBattleAction::MOVE_STACK:
+			return 8; break;
+		case PossiblePlayerBattleAction::CATAPULT:
+			return 9; break;
+		case PossiblePlayerBattleAction::HEAL:
+			return 10; break;
+		default:
+			return 200; break;
+		}
+	};
+
+	auto comparer = [&](PossiblePlayerBattleAction const & lhs, PossiblePlayerBattleAction const & rhs)
+	{
+		return assignPriority(lhs) > assignPriority(rhs);
+	};
+
+	std::make_heap(possibleActions.begin(), possibleActions.end(), comparer);
+}
+
+void CBattleActionsController::castThisSpell(SpellID spellID)
+{
+	spellToCast = std::make_shared<BattleAction>();
+	spellToCast->actionType = EActionType::HERO_SPELL;
+	spellToCast->actionSubtype = spellID; //spell number
+	spellToCast->stackNumber = (owner->attackingHeroInstance->tempOwner == owner->curInt->playerID) ? -1 : -2;
+	spellToCast->side = owner->defendingHeroInstance ? (owner->curInt->playerID == owner->defendingHeroInstance->tempOwner) : false;
+	spellDestSelectMode = true;
+	creatureCasting = false;
+
+	//choosing possible targets
+	const CGHeroInstance *castingHero = (owner->attackingHeroInstance->tempOwner == owner->curInt->playerID) ? owner->attackingHeroInstance : owner->defendingHeroInstance;
+	assert(castingHero); // code below assumes non-null hero
+	sp = spellID.toSpell();
+	PossiblePlayerBattleAction spellSelMode = owner->curInt->cb->getCasterAction(sp, castingHero, spells::Mode::HERO);
+
+	if (spellSelMode == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location
+	{
+		spellToCast->aimToHex(BattleHex::INVALID);
+		owner->curInt->cb->battleMakeAction(spellToCast.get());
+		endCastingSpell();
+	}
+	else
+	{
+		possibleActions.clear();
+		possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment
+		GH.fakeMouseMove();//update cursor
+	}
+}
+
+
+void CBattleActionsController::handleHex(BattleHex myNumber, int eventType)
+{
+	if (!owner->myTurn || !owner->battleActionsStarted) //we are not permit to do anything
+		return;
+
+	// This function handles mouse move over hexes and l-clicking on them.
+	// First we decide what happens if player clicks on this hex and set appropriately
+	// consoleMsg, cursorFrame/Type and prepare lambda realizeAction.
+	//
+	// Then, depending whether it was hover/click we either call the action or set tooltip/cursor.
+
+	//used when hovering -> tooltip message and cursor to be set
+	std::string consoleMsg;
+	bool setCursor = true; //if we want to suppress setting cursor
+	ECursor::ECursorTypes cursorType = ECursor::COMBAT;
+	int cursorFrame = ECursor::COMBAT_POINTER; //TODO: is this line used?
+
+	//used when l-clicking -> action to be called upon the click
+	std::function<void()> realizeAction;
+
+	//Get stack on the hex - first try to grab the alive one, if not found -> allow dead stacks.
+	const CStack * shere = owner->curInt->cb->battleGetStackByPos(myNumber, true);
+	if(!shere)
+		shere = owner->curInt->cb->battleGetStackByPos(myNumber, false);
+
+	if(!owner->stacksController->getActiveStack())
+		return;
+
+	bool ourStack = false;
+	if (shere)
+		ourStack = shere->owner == owner->curInt->playerID;
+
+	//stack may have changed, update selection border
+	owner->stacksController->setHoveredStack(shere);
+
+	localActions.clear();
+	illegalActions.clear();
+
+	reorderPossibleActionsPriority(owner->stacksController->getActiveStack(), shere ? MouseHoveredHexContext::OCCUPIED_HEX : MouseHoveredHexContext::UNOCCUPIED_HEX);
+	const bool forcedAction = possibleActions.size() == 1;
+
+	for (PossiblePlayerBattleAction action : possibleActions)
+	{
+		bool legalAction = false; //this action is legal and can be performed
+		bool notLegal = false; //this action is not legal and should display message
+
+		switch (action)
+		{
+			case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
+				if (shere && ourStack)
+					legalAction = true;
+				break;
+			case PossiblePlayerBattleAction::MOVE_TACTICS:
+			case PossiblePlayerBattleAction::MOVE_STACK:
+			{
+				if (!(shere && shere->alive())) //we can walk on dead stacks
+				{
+					if(canStackMoveHere(owner->stacksController->getActiveStack(), myNumber))
+						legalAction = true;
+				}
+				break;
+			}
+			case PossiblePlayerBattleAction::ATTACK:
+			case PossiblePlayerBattleAction::WALK_AND_ATTACK:
+			case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
+			{
+				if(owner->curInt->cb->battleCanAttack(owner->stacksController->getActiveStack(), shere, myNumber))
+				{
+					if (owner->fieldController->isTileAttackable(myNumber)) // move isTileAttackable to be part of battleCanAttack?
+					{
+						owner->fieldController->setBattleCursor(myNumber); // temporary - needed for following function :(
+						BattleHex attackFromHex = owner->fieldController->fromWhichHexAttack(myNumber);
+
+						if (attackFromHex >= 0) //we can be in this line when unreachable creature is L - clicked (as of revision 1308)
+							legalAction = true;
+					}
+				}
+			}
+				break;
+			case PossiblePlayerBattleAction::SHOOT:
+				if(owner->curInt->cb->battleCanShoot(owner->stacksController->getActiveStack(), myNumber))
+					legalAction = true;
+				break;
+			case PossiblePlayerBattleAction::ANY_LOCATION:
+				if (myNumber > -1) //TODO: this should be checked for all actions
+				{
+					if(isCastingPossibleHere(owner->stacksController->getActiveStack(), shere, myNumber))
+						legalAction = true;
+				}
+				break;
+			case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+				if(shere && isCastingPossibleHere(owner->stacksController->getActiveStack(), shere, myNumber))
+					legalAction = true;
+				break;
+			case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
+			{
+				if(shere && ourStack && shere != owner->stacksController->getActiveStack() && shere->alive()) //only positive spells for other allied creatures
+				{
+					int spellID = owner->curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), shere, CBattleInfoCallback::RANDOM_GENIE);
+					if(spellID > -1)
+					{
+						legalAction = true;
+					}
+				}
+			}
+				break;
+			case PossiblePlayerBattleAction::OBSTACLE:
+				if(isCastingPossibleHere(owner->stacksController->getActiveStack(), shere, myNumber))
+					legalAction = true;
+				break;
+			case PossiblePlayerBattleAction::TELEPORT:
+			{
+				//todo: move to mechanics
+				ui8 skill = 0;
+				if (creatureCasting)
+					skill = owner->stacksController->getActiveStack()->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell());
+				else
+					skill = owner->getActiveHero()->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell());
+				//TODO: explicitely save power, skill
+				if (owner->curInt->cb->battleCanTeleportTo(owner->stacksController->getSelectedStack(), myNumber, skill))
+					legalAction = true;
+				else
+					notLegal = true;
+			}
+				break;
+			case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice
+				if (shere && shere != owner->stacksController->getSelectedStack() && ourStack && shere->alive())
+					legalAction = true;
+				else
+					notLegal = true;
+				break;
+			case PossiblePlayerBattleAction::FREE_LOCATION:
+				legalAction = true;
+				if(!isCastingPossibleHere(owner->stacksController->getActiveStack(), shere, myNumber))
+				{
+					legalAction = false;
+					notLegal = true;
+				}
+				break;
+			case PossiblePlayerBattleAction::CATAPULT:
+				if (owner->siegeController && owner->siegeController->isCatapultAttackable(myNumber))
+					legalAction = true;
+				break;
+			case PossiblePlayerBattleAction::HEAL:
+				if (shere && ourStack && shere->canBeHealed())
+					legalAction = true;
+				break;
+			case PossiblePlayerBattleAction::RISE_DEMONS:
+				if (shere && ourStack && !shere->alive())
+				{
+					if (!(shere->hasBonusOfType(Bonus::UNDEAD)
+						|| shere->hasBonusOfType(Bonus::NON_LIVING)
+						|| shere->hasBonusOfType(Bonus::GARGOYLE)
+						|| shere->summoned
+						|| shere->isClone()
+						|| shere->hasBonusOfType(Bonus::SIEGE_WEAPON)
+						))
+						legalAction = true;
+				}
+				break;
+		}
+		if (legalAction)
+			localActions.push_back (action);
+		else if (notLegal || forcedAction)
+			illegalActions.push_back (action);
+	}
+	illegalAction = PossiblePlayerBattleAction::INVALID; //clear it in first place
+
+	if (vstd::contains(localActions, selectedAction)) //try to use last selected action by default
+		currentAction = selectedAction;
+	else if (localActions.size()) //if not possible, select first available action (they are sorted by suggested priority)
+		currentAction = localActions.front();
+	else //no legal action possible
+	{
+		currentAction = PossiblePlayerBattleAction::INVALID; //don't allow to do anything
+
+		if (vstd::contains(illegalActions, selectedAction))
+			illegalAction = selectedAction;
+		else if (illegalActions.size())
+			illegalAction = illegalActions.front();
+		else if (shere && ourStack && shere->alive()) //last possibility - display info about our creature
+		{
+			currentAction = PossiblePlayerBattleAction::CREATURE_INFO;
+		}
+		else
+			illegalAction = PossiblePlayerBattleAction::INVALID; //we should never be here
+	}
+
+	bool isCastingPossible = false;
+	bool secondaryTarget = false;
+
+	if (currentAction > PossiblePlayerBattleAction::INVALID)
+	{
+		switch (currentAction) //display console message, realize selected action
+		{
+			case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
+				consoleMsg = (boost::format(CGI->generaltexth->allTexts[481]) % shere->getName()).str(); //Select %s
+				realizeAction = [=](){ owner->stackActivated(shere); };
+				break;
+			case PossiblePlayerBattleAction::MOVE_TACTICS:
+			case PossiblePlayerBattleAction::MOVE_STACK:
+				if (owner->stacksController->getActiveStack()->hasBonusOfType(Bonus::FLYING))
+				{
+					cursorFrame = ECursor::COMBAT_FLY;
+					consoleMsg = (boost::format(CGI->generaltexth->allTexts[295]) % owner->stacksController->getActiveStack()->getName()).str(); //Fly %s here
+				}
+				else
+				{
+					cursorFrame = ECursor::COMBAT_MOVE;
+					consoleMsg = (boost::format(CGI->generaltexth->allTexts[294]) % owner->stacksController->getActiveStack()->getName()).str(); //Move %s here
+				}
+
+				realizeAction = [=]()
+				{
+					if(owner->stacksController->getActiveStack()->doubleWide())
+					{
+						std::vector<BattleHex> acc = owner->curInt->cb->battleGetAvailableHexes(owner->stacksController->getActiveStack());
+						BattleHex shiftedDest = myNumber.cloneInDirection(owner->stacksController->getActiveStack()->destShiftDir(), false);
+						if(vstd::contains(acc, myNumber))
+							owner->giveCommand(EActionType::WALK, myNumber);
+						else if(vstd::contains(acc, shiftedDest))
+							owner->giveCommand(EActionType::WALK, shiftedDest);
+					}
+					else
+					{
+						owner->giveCommand(EActionType::WALK, myNumber);
+					}
+				};
+				break;
+			case PossiblePlayerBattleAction::ATTACK:
+			case PossiblePlayerBattleAction::WALK_AND_ATTACK:
+			case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
+				{
+					owner->fieldController->setBattleCursor(myNumber); //handle direction of cursor and attackable tile
+					setCursor = false; //don't overwrite settings from the call above //TODO: what does it mean?
+
+					bool returnAfterAttack = currentAction == PossiblePlayerBattleAction::ATTACK_AND_RETURN;
+
+					realizeAction = [=]()
+					{
+						BattleHex attackFromHex = owner->fieldController->fromWhichHexAttack(myNumber);
+						if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308)
+						{
+							auto command = new BattleAction(BattleAction::makeMeleeAttack(owner->stacksController->getActiveStack(), myNumber, attackFromHex, returnAfterAttack));
+							owner->sendCommand(command, owner->stacksController->getActiveStack());
+						}
+					};
+
+					TDmgRange damage = owner->curInt->cb->battleEstimateDamage(owner->stacksController->getActiveStack(), shere);
+					std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg
+					consoleMsg = (boost::format(CGI->generaltexth->allTexts[36]) % shere->getName() % estDmgText).str(); //Attack %s (%s damage)
+				}
+				break;
+			case PossiblePlayerBattleAction::SHOOT:
+			{
+				if (owner->curInt->cb->battleHasShootingPenalty(owner->stacksController->getActiveStack(), myNumber))
+					cursorFrame = ECursor::COMBAT_SHOOT_PENALTY;
+				else
+					cursorFrame = ECursor::COMBAT_SHOOT;
+
+				realizeAction = [=](){owner->giveCommand(EActionType::SHOOT, myNumber);};
+				TDmgRange damage = owner->curInt->cb->battleEstimateDamage(owner->stacksController->getActiveStack(), shere);
+				std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg
+				//printing - Shoot %s (%d shots left, %s damage)
+				consoleMsg = (boost::format(CGI->generaltexth->allTexts[296]) % shere->getName() % owner->stacksController->getActiveStack()->shots.available() % estDmgText).str();
+			}
+				break;
+			case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+				sp = CGI->spellh->objects[creatureCasting ? owner->stacksController->activeStackSpellToCast() : spellToCast->actionSubtype]; //necessary if creature has random Genie spell at same time
+				consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[27]) % sp->name % shere->getName()); //Cast %s on %s
+				switch (sp->id)
+				{
+					case SpellID::SACRIFICE:
+					case SpellID::TELEPORT:
+						owner->stacksController->setSelectedStack(shere); //remember first target
+						secondaryTarget = true;
+						break;
+				}
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::ANY_LOCATION:
+				sp = CGI->spellh->objects[creatureCasting ? owner->stacksController->activeStackSpellToCast() : spellToCast->actionSubtype]; //necessary if creature has random Genie spell at same time
+				consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell
+				sp = nullptr;
+				consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[301]) % shere->getName()); //Cast a spell on %
+				creatureCasting = true;
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::TELEPORT:
+				consoleMsg = CGI->generaltexth->allTexts[25]; //Teleport Here
+				cursorFrame = ECursor::COMBAT_TELEPORT;
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::OBSTACLE:
+				consoleMsg = CGI->generaltexth->allTexts[550];
+				//TODO: remove obstacle cursor
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::SACRIFICE:
+				consoleMsg = (boost::format(CGI->generaltexth->allTexts[549]) % shere->getName()).str(); //sacrifice the %s
+				cursorFrame = ECursor::COMBAT_SACRIFICE;
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::FREE_LOCATION:
+				consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s
+				isCastingPossible = true;
+				break;
+			case PossiblePlayerBattleAction::HEAL:
+				cursorFrame = ECursor::COMBAT_HEAL;
+				consoleMsg = (boost::format(CGI->generaltexth->allTexts[419]) % shere->getName()).str(); //Apply first aid to the %s
+				realizeAction = [=](){ owner->giveCommand(EActionType::STACK_HEAL, myNumber); }; //command healing
+				break;
+			case PossiblePlayerBattleAction::RISE_DEMONS:
+				cursorType = ECursor::SPELLBOOK;
+				realizeAction = [=]()
+				{
+					owner->giveCommand(EActionType::DAEMON_SUMMONING, myNumber);
+				};
+				break;
+			case PossiblePlayerBattleAction::CATAPULT:
+				cursorFrame = ECursor::COMBAT_SHOOT_CATAPULT;
+				realizeAction = [=](){ owner->giveCommand(EActionType::CATAPULT, myNumber); };
+				break;
+			case PossiblePlayerBattleAction::CREATURE_INFO:
+			{
+				cursorFrame = ECursor::COMBAT_QUERY;
+				consoleMsg = (boost::format(CGI->generaltexth->allTexts[297]) % shere->getName()).str();
+				realizeAction = [=](){ GH.pushIntT<CStackWindow>(shere, false); };
+				break;
+			}
+		}
+	}
+	else //no possible valid action, display message
+	{
+		switch (illegalAction)
+		{
+			case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
+			case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
+				cursorFrame = ECursor::COMBAT_BLOCKED;
+				consoleMsg = CGI->generaltexth->allTexts[23];
+				break;
+			case PossiblePlayerBattleAction::TELEPORT:
+				cursorFrame = ECursor::COMBAT_BLOCKED;
+				consoleMsg = CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination
+				break;
+			case PossiblePlayerBattleAction::SACRIFICE:
+				consoleMsg = CGI->generaltexth->allTexts[543]; //choose army to sacrifice
+				break;
+			case PossiblePlayerBattleAction::FREE_LOCATION:
+				cursorFrame = ECursor::COMBAT_BLOCKED;
+				consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[181]) % sp->name); //No room to place %s here
+				break;
+			default:
+				if (myNumber == -1)
+					CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); //set neutral cursor over menu etc.
+				else
+					cursorFrame = ECursor::COMBAT_BLOCKED;
+				break;
+		}
+	}
+
+	if (isCastingPossible) //common part
+	{
+		switch (currentAction) //don't use that with teleport / sacrifice
+		{
+			case PossiblePlayerBattleAction::TELEPORT: //FIXME: more generic solution?
+			case PossiblePlayerBattleAction::SACRIFICE:
+				break;
+			default:
+				cursorType = ECursor::SPELLBOOK;
+				cursorFrame = 0;
+				if (consoleMsg.empty() && sp)
+					consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s
+				break;
+		}
+
+		realizeAction = [=]()
+		{
+			if(secondaryTarget) //select that target now
+			{
+
+				possibleActions.clear();
+				switch (sp->id.toEnum())
+				{
+					case SpellID::TELEPORT: //don't cast spell yet, only select target
+						spellToCast->aimToUnit(shere);
+						possibleActions.push_back(PossiblePlayerBattleAction::TELEPORT);
+						break;
+					case SpellID::SACRIFICE:
+						spellToCast->aimToHex(myNumber);
+						possibleActions.push_back(PossiblePlayerBattleAction::SACRIFICE);
+						break;
+				}
+			}
+			else
+			{
+				if (creatureCasting)
+				{
+					if (sp)
+					{
+						owner->giveCommand(EActionType::MONSTER_SPELL, myNumber, owner->stacksController->activeStackSpellToCast());
+					}
+					else //unknown random spell
+					{
+						owner->giveCommand(EActionType::MONSTER_SPELL, myNumber);
+					}
+				}
+				else
+				{
+					assert(sp);
+					switch (sp->id.toEnum())
+					{
+					case SpellID::SACRIFICE:
+						spellToCast->aimToUnit(shere);//victim
+						break;
+					default:
+						spellToCast->aimToHex(myNumber);
+						break;
+					}
+					owner->curInt->cb->battleMakeAction(spellToCast.get());
+					endCastingSpell();
+				}
+				owner->stacksController->setSelectedStack(nullptr);
+			}
+		};
+	}
+
+	{
+		if (eventType == CIntObject::MOVE)
+		{
+			if (setCursor)
+				CCS->curh->changeGraphic(cursorType, cursorFrame);
+			owner->controlPanel->console->write(consoleMsg);
+		}
+		if (eventType == CIntObject::LCLICK && realizeAction)
+		{
+			//opening creature window shouldn't affect myTurn...
+			if ((currentAction != PossiblePlayerBattleAction::CREATURE_INFO) && !secondaryTarget)
+			{
+				owner->myTurn = false; //tends to crash with empty calls
+			}
+			realizeAction();
+			if (!secondaryTarget) //do not replace teleport or sacrifice cursor
+				CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER);
+			owner->controlPanel->console->clear();
+		}
+	}
+}
+
+
+bool CBattleActionsController::isCastingPossibleHere(const CStack *sactive, const CStack *shere, BattleHex myNumber)
+{
+	creatureCasting = owner->stacksController->activeStackSpellcaster() && !spellDestSelectMode; //TODO: allow creatures to cast aimed spells
+
+	bool isCastingPossible = true;
+
+	int spellID = -1;
+	if (creatureCasting)
+	{
+		if (owner->stacksController->activeStackSpellToCast() != SpellID::NONE && (shere != sactive)) //can't cast on itself
+			spellID = owner->stacksController->activeStackSpellToCast(); //TODO: merge with SpellTocast?
+	}
+	else //hero casting
+	{
+		spellID = spellToCast->actionSubtype;
+	}
+
+
+	sp = nullptr;
+	if (spellID >= 0)
+		sp = CGI->spellh->objects[spellID];
+
+	if (sp)
+	{
+		const spells::Caster *caster = creatureCasting ? static_cast<const spells::Caster *>(sactive) : static_cast<const spells::Caster *>(owner->curInt->cb->battleGetMyHero());
+		if (caster == nullptr)
+		{
+			isCastingPossible = false;//just in case
+		}
+		else
+		{
+			const spells::Mode mode = creatureCasting ? spells::Mode::CREATURE_ACTIVE : spells::Mode::HERO;
+
+			spells::Target target;
+			target.emplace_back(myNumber);
+
+			spells::BattleCast cast(owner->curInt->cb.get(), caster, mode, sp);
+
+			auto m = sp->battleMechanics(&cast);
+			spells::detail::ProblemImpl problem; //todo: display problem in status bar
+
+			isCastingPossible = m->canBeCastAt(target, problem);
+		}
+	}
+	else
+		isCastingPossible = false;
+	if (!myNumber.isAvailable() && !shere) //empty tile outside battlefield (or in the unavailable border column)
+			isCastingPossible = false;
+
+	return isCastingPossible;
+}
+
+bool CBattleActionsController::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber)
+{
+	std::vector<BattleHex> acc = owner->curInt->cb->battleGetAvailableHexes(stackToMove);
+	BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false);
+
+	if (vstd::contains(acc, myNumber))
+		return true;
+	else if (stackToMove->doubleWide() && vstd::contains(acc, shiftedDest))
+		return true;
+	else
+		return false;
+}
+
+void CBattleActionsController::activateStack()
+{
+	const CStack * s = owner->stacksController->getActiveStack();
+	if(s)
+		possibleActions = getPossibleActionsForStack(s);
+}
+
+bool CBattleActionsController::spellcastingModeActive()
+{
+	return spellDestSelectMode;
+}
+
+SpellID CBattleActionsController::selectedSpell()
+{
+	if (!spellToCast)
+		return SpellID::NONE;
+	return SpellID(spellToCast->actionSubtype);
+}

+ 57 - 0
client/battle/CBattleActionsController.h

@@ -0,0 +1,57 @@
+/*
+ * CBattleActionsController.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 "../../lib/battle/CBattleInfoCallback.h"
+
+class BattleAction;
+class CBattleInterface;
+
+enum class MouseHoveredHexContext
+{
+	UNOCCUPIED_HEX,
+	OCCUPIED_HEX
+};
+
+class CBattleActionsController
+{
+	CBattleInterface * owner;
+
+	std::vector<PossiblePlayerBattleAction> possibleActions; //all actions possible to call at the moment by player
+	std::vector<PossiblePlayerBattleAction> localActions; //actions possible to take on hovered hex
+	std::vector<PossiblePlayerBattleAction> illegalActions; //these actions display message in case of illegal target
+	PossiblePlayerBattleAction currentAction; //action that will be performed on l-click
+	PossiblePlayerBattleAction selectedAction; //last action chosen (and saved) by player
+	PossiblePlayerBattleAction illegalAction; //most likely action that can't be performed here
+
+	bool creatureCasting; //if true, stack currently aims to cats a spell
+	bool spellDestSelectMode; //if true, player is choosing destination for his spell - only for GUI / console
+	std::shared_ptr<BattleAction> spellToCast; //spell for which player is choosing destination
+	const CSpell *sp; //spell pointer for convenience
+
+	bool isCastingPossibleHere (const CStack *sactive, const CStack *shere, BattleHex myNumber);
+	bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber); //TODO: move to BattleState / callback
+
+	std::vector<PossiblePlayerBattleAction> getPossibleActionsForStack (const CStack *stack); //called when stack gets its turn
+	void reorderPossibleActionsPriority(const CStack * stack, MouseHoveredHexContext context);
+
+public:
+	CBattleActionsController(CBattleInterface * owner);
+
+	void activateStack();
+	void endCastingSpell(); //ends casting spell (eg. when spell has been cast or canceled)
+	void enterCreatureCastingMode();
+
+	SpellID selectedSpell();
+	bool spellcastingModeActive();
+	void castThisSpell(SpellID spellID); //called when player has chosen a spell from spellbook
+	void handleHex(BattleHex myNumber, int eventType);
+
+};

+ 13 - 9
client/battle/CBattleControlPanel.cpp

@@ -12,6 +12,7 @@
 #include "CBattleInterface.h"
 #include "CBattleInterfaceClasses.h"
 #include "CBattleStacksController.h"
+#include "CBattleActionsController.h"
 #include "../widgets/Buttons.h"
 #include "../CGameInfo.h"
 #include "../CBitmapHandler.h"
@@ -67,13 +68,16 @@ void CBattleControlPanel::tacticPhaseEnded()
 {
 	OBJ_CONSTRUCTION_CAPTURING_ALL_NO_DISPOSE;
 
+	btactNext.reset();
+	btactEnd.reset();
+
 	menu = std::make_shared<CPicture>("CBAR.BMP", 0, 0);
 	menu->colorize(owner->curInt->playerID);
 }
 
 void CBattleControlPanel::bOptionsf()
 {
-	if (owner->spellDestSelectMode) //we are casting a spell
+	if (owner->actionsController->spellcastingModeActive())
 		return;
 
 	CCS->curh->changeGraphic(ECursor::ADVENTURE,0);
@@ -85,7 +89,7 @@ void CBattleControlPanel::bOptionsf()
 
 void CBattleControlPanel::bSurrenderf()
 {
-	if(owner->spellDestSelectMode) //we are casting a spell
+	if (owner->actionsController->spellcastingModeActive())
 		return;
 
 	int cost = owner->curInt->cb->battleGetSurrenderCost();
@@ -105,7 +109,7 @@ void CBattleControlPanel::bSurrenderf()
 
 void CBattleControlPanel::bFleef()
 {
-	if (owner->spellDestSelectMode) //we are casting a spell
+	if (owner->actionsController->spellcastingModeActive())
 		return;
 
 	if ( owner->curInt->cb->battleCanFlee() )
@@ -153,7 +157,7 @@ void CBattleControlPanel::reallySurrender()
 
 void CBattleControlPanel::bAutofightf()
 {
-	if(owner->spellDestSelectMode) //we are casting a spell
+	if (owner->actionsController->spellcastingModeActive())
 		return;
 
 	//Stop auto-fight mode
@@ -180,7 +184,7 @@ void CBattleControlPanel::bAutofightf()
 
 void CBattleControlPanel::bSpellf()
 {
-	if (owner->spellDestSelectMode) //we are casting a spell
+	if (owner->actionsController->spellcastingModeActive())
 		return;
 
 	if (!owner->myTurn)
@@ -222,7 +226,7 @@ void CBattleControlPanel::bSpellf()
 
 void CBattleControlPanel::bWaitf()
 {
-	if (owner->spellDestSelectMode) //we are casting a spell
+	if (owner->actionsController->spellcastingModeActive())
 		return;
 
 	if (owner->stacksController->getActiveStack() != nullptr)
@@ -231,7 +235,7 @@ void CBattleControlPanel::bWaitf()
 
 void CBattleControlPanel::bDefencef()
 {
-	if (owner->spellDestSelectMode) //we are casting a spell
+	if (owner->actionsController->spellcastingModeActive())
 		return;
 
 	if (owner->stacksController->getActiveStack() != nullptr)
@@ -240,7 +244,7 @@ void CBattleControlPanel::bDefencef()
 
 void CBattleControlPanel::bConsoleUpf()
 {
-	if (owner->spellDestSelectMode) //we are casting a spell
+	if (owner->actionsController->spellcastingModeActive())
 		return;
 
 	console->scrollUp();
@@ -248,7 +252,7 @@ void CBattleControlPanel::bConsoleUpf()
 
 void CBattleControlPanel::bConsoleDownf()
 {
-	if (owner->spellDestSelectMode) //we are casting a spell
+	if (owner->actionsController->spellcastingModeActive())
 		return;
 
 	console->scrollDown();

+ 4 - 3
client/battle/CBattleFieldController.cpp

@@ -10,6 +10,7 @@
 #include "StdInc.h"
 #include "CBattleFieldController.h"
 #include "CBattleInterface.h"
+#include "CBattleActionsController.h"
 #include "CBattleInterfaceClasses.h"
 #include "CBattleSiegeController.h"
 #include "CBattleStacksController.h"
@@ -220,12 +221,12 @@ void CBattleFieldController::showHighlightedHexes(SDL_Surface *to)
 
 				spells::Mode mode = spells::Mode::HERO;
 
-				if(owner->spellToCast)//hero casts spell
+				if(owner->actionsController->spellcastingModeActive())//hero casts spell
 				{
-					spell = SpellID(owner->spellToCast->actionSubtype).toSpell();
+					spell = owner->actionsController->selectedSpell().toSpell();
 					caster = owner->getActiveHero();
 				}
-				else if(owner->stacksController->activeStackSpellToCast() != SpellID::NONE && owner->creatureCasting)//stack casts spell
+				else if(owner->stacksController->activeStackSpellToCast() != SpellID::NONE)//stack casts spell
 				{
 					spell = SpellID(owner->stacksController->activeStackSpellToCast()).toSpell();
 					caster = owner->stacksController->getActiveStack();

+ 13 - 714
client/battle/CBattleInterface.cpp

@@ -11,6 +11,7 @@
 #include "CBattleInterface.h"
 
 #include "CBattleAnimations.h"
+#include "CBattleActionsController.h"
 #include "CBattleInterfaceClasses.h"
 #include "CCreatureAnimation.h"
 #include "CBattleProjectileController.h"
@@ -62,7 +63,6 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 		const SDL_Rect & myRect,
 		std::shared_ptr<CPlayerInterface> att, std::shared_ptr<CPlayerInterface> defen, std::shared_ptr<CPlayerInterface> spectatorInt)
 	: attackingHeroInstance(hero1), defendingHeroInstance(hero2), animCount(0),
-	creatureCasting(false), spellDestSelectMode(false), spellToCast(nullptr), sp(nullptr),
 	attackerInt(att), defenderInt(defen), curInt(att),
 	myTurn(false), moveStarted(false), moveSoundHander(-1), bresult(nullptr), battleActionsStarted(false)
 {
@@ -129,6 +129,7 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 	//preparing menu background and terrain
 	fieldController.reset( new CBattleFieldController(this));
 	stacksController.reset( new CBattleStacksController(this));
+	actionsController.reset( new CBattleActionsController(this));
 
 	//loading hero animations
 	if(hero1) // attacking hero
@@ -197,8 +198,6 @@ CBattleInterface::CBattleInterface(const CCreatureSet *army1, const CCreatureSet
 
 	CCS->soundh->setCallback(battleIntroSoundChannel, onIntroPlayed);
 
-	currentAction = PossiblePlayerBattleAction::INVALID;
-	selectedAction = PossiblePlayerBattleAction::INVALID;
 	addUsedEvents(RCLICK | MOVE | KEYBOARD);
 	controlPanel->blockUI(true);
 }
@@ -300,28 +299,28 @@ void CBattleInterface::keyPressed(const SDL_KeyboardEvent & key)
 	}
 	else if(key.keysym.sym == SDLK_f && key.state == SDL_PRESSED)
 	{
-		enterCreatureCastingMode();
+		actionsController->enterCreatureCastingMode();
 	}
 	else if(key.keysym.sym == SDLK_ESCAPE)
 	{
 		if(!battleActionsStarted)
 			CCS->soundh->stopSound(battleIntroSoundChannel);
 		else
-			endCastingSpell();
+			actionsController->endCastingSpell();
 	}
 }
 void CBattleInterface::mouseMoved(const SDL_MouseMotionEvent &sEvent)
 {
 	BattleHex selectedHex = fieldController->getHoveredHex();
 
-	handleHex(selectedHex, MOVE);
+	actionsController->handleHex(selectedHex, MOVE);
 }
 
 void CBattleInterface::clickRight(tribool down, bool previousState)
 {
 	if (!down)
 	{
-		endCastingSpell();
+		actionsController->endCastingSpell();
 	}
 }
 
@@ -453,7 +452,7 @@ const CGHeroInstance * CBattleInterface::getActiveHero()
 
 void CBattleInterface::hexLclicked(int whichOne)
 {
-	handleHex(whichOne, LCLICK);
+	actionsController->handleHex(whichOne, LCLICK);
 }
 
 void CBattleInterface::stackIsCatapulting(const CatapultAttack & ca)
@@ -618,36 +617,6 @@ void CBattleInterface::setHeroAnimation(ui8 side, int phase)
 	}
 }
 
-void CBattleInterface::castThisSpell(SpellID spellID)
-{
-	spellToCast = std::make_shared<BattleAction>();
-	spellToCast->actionType = EActionType::HERO_SPELL;
-	spellToCast->actionSubtype = spellID; //spell number
-	spellToCast->stackNumber = (attackingHeroInstance->tempOwner == curInt->playerID) ? -1 : -2;
-	spellToCast->side = defendingHeroInstance ? (curInt->playerID == defendingHeroInstance->tempOwner) : false;
-	spellDestSelectMode = true;
-	creatureCasting = false;
-
-	//choosing possible targets
-	const CGHeroInstance *castingHero = (attackingHeroInstance->tempOwner == curInt->playerID) ? attackingHeroInstance : defendingHeroInstance;
-	assert(castingHero); // code below assumes non-null hero
-	sp = spellID.toSpell();
-	PossiblePlayerBattleAction spellSelMode = curInt->cb->getCasterAction(sp, castingHero, spells::Mode::HERO);
-
-	if (spellSelMode == PossiblePlayerBattleAction::NO_LOCATION) //user does not have to select location
-	{
-		spellToCast->aimToHex(BattleHex::INVALID);
-		curInt->cb->battleMakeAction(spellToCast.get());
-		endCastingSpell();
-	}
-	else
-	{
-		possibleActions.clear();
-		possibleActions.push_back (spellSelMode); //only this one action can be performed at the moment
-		GH.fakeMouseMove();//update cursor
-	}
-}
-
 void CBattleInterface::displayBattleLog(const std::vector<MetaString> & battleLog)
 {
 	for(const auto & line : battleLog)
@@ -798,158 +767,10 @@ void CBattleInterface::activateStack()
 	myTurn = true;
 	queue->update();
 	fieldController->redrawBackgroundWithHexes();
-	possibleActions = getPossibleActionsForStack(s);
+	actionsController->activateStack();
 	GH.fakeMouseMove();
 }
 
-void CBattleInterface::endCastingSpell()
-{
-	if(spellDestSelectMode)
-	{
-		spellToCast.reset();
-
-		sp = nullptr;
-		spellDestSelectMode = false;
-		CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER);
-
-		if(stacksController->getActiveStack())
-		{
-			possibleActions = getPossibleActionsForStack(stacksController->getActiveStack()); //restore actions after they were cleared
-			myTurn = true;
-		}
-	}
-	else
-	{
-		if(stacksController->getActiveStack())
-		{
-			possibleActions = getPossibleActionsForStack(stacksController->getActiveStack());
-			GH.fakeMouseMove();
-		}
-	}
-}
-
-void CBattleInterface::enterCreatureCastingMode()
-{
-	//silently check for possible errors
-	if (!myTurn)
-		return;
-
-	if (tacticsMode)
-		return;
-
-	//hero is casting a spell
-	if (spellDestSelectMode)
-		return;
-
-	if (!stacksController->getActiveStack())
-		return;
-
-	if (!stacksController->activeStackSpellcaster())
-		return;
-
-	//random spellcaster
-	if (stacksController->activeStackSpellToCast() == SpellID::NONE)
-		return;
-
-	if (vstd::contains(possibleActions, PossiblePlayerBattleAction::NO_LOCATION))
-	{
-		const spells::Caster * caster = stacksController->getActiveStack();
-		const CSpell * spell = stacksController->activeStackSpellToCast().toSpell();
-
-		spells::Target target;
-		target.emplace_back();
-
-		spells::BattleCast cast(curInt->cb.get(), caster, spells::Mode::CREATURE_ACTIVE, spell);
-
-		auto m = spell->battleMechanics(&cast);
-		spells::detail::ProblemImpl ignored;
-
-		const bool isCastingPossible = m->canBeCastAt(target, ignored);
-
-		if (isCastingPossible)
-		{
-			myTurn = false;
-			giveCommand(EActionType::MONSTER_SPELL, BattleHex::INVALID, stacksController->activeStackSpellToCast());
-			stacksController->setSelectedStack(nullptr);
-
-			CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER);
-		}
-	}
-	else
-	{
-		possibleActions = getPossibleActionsForStack(stacksController->getActiveStack());
-
-		auto actionFilterPredicate = [](const PossiblePlayerBattleAction x)
-		{
-			return (x != PossiblePlayerBattleAction::ANY_LOCATION) && (x != PossiblePlayerBattleAction::NO_LOCATION) &&
-				(x != PossiblePlayerBattleAction::FREE_LOCATION) && (x != PossiblePlayerBattleAction::AIMED_SPELL_CREATURE) &&
-				(x != PossiblePlayerBattleAction::OBSTACLE);
-		};
-
-		vstd::erase_if(possibleActions, actionFilterPredicate);
-		GH.fakeMouseMove();
-	}
-}
-
-std::vector<PossiblePlayerBattleAction> CBattleInterface::getPossibleActionsForStack(const CStack *stack)
-{
-	BattleClientInterfaceData data; //hard to get rid of these things so for now they're required data to pass
-	data.creatureSpellToCast = stacksController->activeStackSpellToCast();
-	data.tacticsMode = tacticsMode;
-	auto allActions = curInt->cb->getClientActionsForStack(stack, data);
-
-	return std::vector<PossiblePlayerBattleAction>(allActions);
-}
-
-void CBattleInterface::reorderPossibleActionsPriority(const CStack * stack, MouseHoveredHexContext context)
-{
-	if(tacticsMode || possibleActions.empty()) return; //this function is not supposed to be called in tactics mode or before getPossibleActionsForStack
-
-	auto assignPriority = [&](PossiblePlayerBattleAction const & item) -> uint8_t //large lambda assigning priority which would have to be part of possibleActions without it
-	{
-		switch(item)
-		{
-		case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-		case PossiblePlayerBattleAction::ANY_LOCATION:
-		case PossiblePlayerBattleAction::NO_LOCATION:
-		case PossiblePlayerBattleAction::FREE_LOCATION:
-		case PossiblePlayerBattleAction::OBSTACLE:
-			if(!stack->hasBonusOfType(Bonus::NO_SPELLCAST_BY_DEFAULT) && context == MouseHoveredHexContext::OCCUPIED_HEX)
-				return 1;
-			else
-				return 100;//bottom priority
-			break;
-		case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
-			return 2; break;
-		case PossiblePlayerBattleAction::RISE_DEMONS:
-			return 3; break;
-		case PossiblePlayerBattleAction::SHOOT:
-			return 4; break;
-		case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
-			return 5; break;
-		case PossiblePlayerBattleAction::ATTACK:
-			return 6; break;
-		case PossiblePlayerBattleAction::WALK_AND_ATTACK:
-			return 7; break;
-		case PossiblePlayerBattleAction::MOVE_STACK:
-			return 8; break;
-		case PossiblePlayerBattleAction::CATAPULT:
-			return 9; break;
-		case PossiblePlayerBattleAction::HEAL:
-			return 10; break;
-		default:
-			return 200; break;
-		}
-	};
-
-	auto comparer = [&](PossiblePlayerBattleAction const & lhs, PossiblePlayerBattleAction const & rhs)
-	{
-		return assignPriority(lhs) > assignPriority(rhs);
-	};
-
-	std::make_heap(possibleActions.begin(), possibleActions.end(), comparer);
-}
-
 void CBattleInterface::endAction(const BattleAction* action)
 {
 	const CStack *stack = curInt->cb->battleGetStackByID(action->stackNumber);
@@ -1114,531 +935,6 @@ void CBattleInterface::tacticNextStack(const CStack * current)
 
 }
 
-std::string formatDmgRange(std::pair<ui32, ui32> dmgRange)
-{
-	if (dmgRange.first != dmgRange.second)
-		return (boost::format("%d - %d") % dmgRange.first % dmgRange.second).str();
-	else
-		return (boost::format("%d") % dmgRange.first).str();
-}
-
-bool CBattleInterface::canStackMoveHere(const CStack * stackToMove, BattleHex myNumber)
-{
-	std::vector<BattleHex> acc = curInt->cb->battleGetAvailableHexes(stackToMove);
-	BattleHex shiftedDest = myNumber.cloneInDirection(stackToMove->destShiftDir(), false);
-
-	if (vstd::contains(acc, myNumber))
-		return true;
-	else if (stackToMove->doubleWide() && vstd::contains(acc, shiftedDest))
-		return true;
-	else
-		return false;
-}
-
-void CBattleInterface::handleHex(BattleHex myNumber, int eventType)
-{
-	if (!myTurn || !battleActionsStarted) //we are not permit to do anything
-		return;
-
-	// This function handles mouse move over hexes and l-clicking on them.
-	// First we decide what happens if player clicks on this hex and set appropriately
-	// consoleMsg, cursorFrame/Type and prepare lambda realizeAction.
-	//
-	// Then, depending whether it was hover/click we either call the action or set tooltip/cursor.
-
-	//used when hovering -> tooltip message and cursor to be set
-	std::string consoleMsg;
-	bool setCursor = true; //if we want to suppress setting cursor
-	ECursor::ECursorTypes cursorType = ECursor::COMBAT;
-	int cursorFrame = ECursor::COMBAT_POINTER; //TODO: is this line used?
-
-	//used when l-clicking -> action to be called upon the click
-	std::function<void()> realizeAction;
-
-	//Get stack on the hex - first try to grab the alive one, if not found -> allow dead stacks.
-	const CStack * shere = curInt->cb->battleGetStackByPos(myNumber, true);
-	if(!shere)
-		shere = curInt->cb->battleGetStackByPos(myNumber, false);
-
-	if(!stacksController->getActiveStack())
-		return;
-
-	bool ourStack = false;
-	if (shere)
-		ourStack = shere->owner == curInt->playerID;
-
-	//stack may have changed, update selection border
-	stacksController->setHoveredStack(shere);
-
-	localActions.clear();
-	illegalActions.clear();
-
-	reorderPossibleActionsPriority(stacksController->getActiveStack(), shere ? MouseHoveredHexContext::OCCUPIED_HEX : MouseHoveredHexContext::UNOCCUPIED_HEX);
-	const bool forcedAction = possibleActions.size() == 1;
-
-	for (PossiblePlayerBattleAction action : possibleActions)
-	{
-		bool legalAction = false; //this action is legal and can be performed
-		bool notLegal = false; //this action is not legal and should display message
-
-		switch (action)
-		{
-			case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
-				if (shere && ourStack)
-					legalAction = true;
-				break;
-			case PossiblePlayerBattleAction::MOVE_TACTICS:
-			case PossiblePlayerBattleAction::MOVE_STACK:
-			{
-				if (!(shere && shere->alive())) //we can walk on dead stacks
-				{
-					if(canStackMoveHere(stacksController->getActiveStack(), myNumber))
-						legalAction = true;
-				}
-				break;
-			}
-			case PossiblePlayerBattleAction::ATTACK:
-			case PossiblePlayerBattleAction::WALK_AND_ATTACK:
-			case PossiblePlayerBattleAction::ATTACK_AND_RETURN:
-			{
-				if(curInt->cb->battleCanAttack(stacksController->getActiveStack(), shere, myNumber))
-				{
-					if (fieldController->isTileAttackable(myNumber)) // move isTileAttackable to be part of battleCanAttack?
-					{
-						fieldController->setBattleCursor(myNumber); // temporary - needed for following function :(
-						BattleHex attackFromHex = fieldController->fromWhichHexAttack(myNumber);
-
-						if (attackFromHex >= 0) //we can be in this line when unreachable creature is L - clicked (as of revision 1308)
-							legalAction = true;
-					}
-				}
-			}
-				break;
-			case PossiblePlayerBattleAction::SHOOT:
-				if(curInt->cb->battleCanShoot(stacksController->getActiveStack(), myNumber))
-					legalAction = true;
-				break;
-			case PossiblePlayerBattleAction::ANY_LOCATION:
-				if (myNumber > -1) //TODO: this should be checked for all actions
-				{
-					if(isCastingPossibleHere(stacksController->getActiveStack(), shere, myNumber))
-						legalAction = true;
-				}
-				break;
-			case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-				if(shere && isCastingPossibleHere(stacksController->getActiveStack(), shere, myNumber))
-					legalAction = true;
-				break;
-			case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
-			{
-				if(shere && ourStack && shere != stacksController->getActiveStack() && shere->alive()) //only positive spells for other allied creatures
-				{
-					int spellID = curInt->cb->battleGetRandomStackSpell(CRandomGenerator::getDefault(), shere, CBattleInfoCallback::RANDOM_GENIE);
-					if(spellID > -1)
-					{
-						legalAction = true;
-					}
-				}
-			}
-				break;
-			case PossiblePlayerBattleAction::OBSTACLE:
-				if(isCastingPossibleHere(stacksController->getActiveStack(), shere, myNumber))
-					legalAction = true;
-				break;
-			case PossiblePlayerBattleAction::TELEPORT:
-			{
-				//todo: move to mechanics
-				ui8 skill = 0;
-				if (creatureCasting)
-					skill = stacksController->getActiveStack()->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell());
-				else
-					skill = getActiveHero()->getEffectLevel(SpellID(SpellID::TELEPORT).toSpell());
-				//TODO: explicitely save power, skill
-				if (curInt->cb->battleCanTeleportTo(stacksController->getSelectedStack(), myNumber, skill))
-					legalAction = true;
-				else
-					notLegal = true;
-			}
-				break;
-			case PossiblePlayerBattleAction::SACRIFICE: //choose our living stack to sacrifice
-				if (shere && shere != stacksController->getSelectedStack() && ourStack && shere->alive())
-					legalAction = true;
-				else
-					notLegal = true;
-				break;
-			case PossiblePlayerBattleAction::FREE_LOCATION:
-				legalAction = true;
-				if(!isCastingPossibleHere(stacksController->getActiveStack(), shere, myNumber))
-				{
-					legalAction = false;
-					notLegal = true;
-				}
-				break;
-			case PossiblePlayerBattleAction::CATAPULT:
-				if (siegeController && siegeController->isCatapultAttackable(myNumber))
-					legalAction = true;
-				break;
-			case PossiblePlayerBattleAction::HEAL:
-				if (shere && ourStack && shere->canBeHealed())
-					legalAction = true;
-				break;
-			case PossiblePlayerBattleAction::RISE_DEMONS:
-				if (shere && ourStack && !shere->alive())
-				{
-					if (!(shere->hasBonusOfType(Bonus::UNDEAD)
-						|| shere->hasBonusOfType(Bonus::NON_LIVING)
-						|| shere->hasBonusOfType(Bonus::GARGOYLE)
-						|| shere->summoned
-						|| shere->isClone()
-						|| shere->hasBonusOfType(Bonus::SIEGE_WEAPON)
-						))
-						legalAction = true;
-				}
-				break;
-		}
-		if (legalAction)
-			localActions.push_back (action);
-		else if (notLegal || forcedAction)
-			illegalActions.push_back (action);
-	}
-	illegalAction = PossiblePlayerBattleAction::INVALID; //clear it in first place
-
-	if (vstd::contains(localActions, selectedAction)) //try to use last selected action by default
-		currentAction = selectedAction;
-	else if (localActions.size()) //if not possible, select first available action (they are sorted by suggested priority)
-		currentAction = localActions.front();
-	else //no legal action possible
-	{
-		currentAction = PossiblePlayerBattleAction::INVALID; //don't allow to do anything
-
-		if (vstd::contains(illegalActions, selectedAction))
-			illegalAction = selectedAction;
-		else if (illegalActions.size())
-			illegalAction = illegalActions.front();
-		else if (shere && ourStack && shere->alive()) //last possibility - display info about our creature
-		{
-			currentAction = PossiblePlayerBattleAction::CREATURE_INFO;
-		}
-		else
-			illegalAction = PossiblePlayerBattleAction::INVALID; //we should never be here
-	}
-
-	bool isCastingPossible = false;
-	bool secondaryTarget = false;
-
-	if (currentAction > PossiblePlayerBattleAction::INVALID)
-	{
-		switch (currentAction) //display console message, realize selected action
-		{
-			case PossiblePlayerBattleAction::CHOOSE_TACTICS_STACK:
-				consoleMsg = (boost::format(CGI->generaltexth->allTexts[481]) % shere->getName()).str(); //Select %s
-				realizeAction = [=](){ stackActivated(shere); };
-				break;
-			case PossiblePlayerBattleAction::MOVE_TACTICS:
-			case PossiblePlayerBattleAction::MOVE_STACK:
-				if (stacksController->getActiveStack()->hasBonusOfType(Bonus::FLYING))
-				{
-					cursorFrame = ECursor::COMBAT_FLY;
-					consoleMsg = (boost::format(CGI->generaltexth->allTexts[295]) % stacksController->getActiveStack()->getName()).str(); //Fly %s here
-				}
-				else
-				{
-					cursorFrame = ECursor::COMBAT_MOVE;
-					consoleMsg = (boost::format(CGI->generaltexth->allTexts[294]) % stacksController->getActiveStack()->getName()).str(); //Move %s here
-				}
-
-				realizeAction = [=]()
-				{
-					if(stacksController->getActiveStack()->doubleWide())
-					{
-						std::vector<BattleHex> acc = curInt->cb->battleGetAvailableHexes(stacksController->getActiveStack());
-						BattleHex shiftedDest = myNumber.cloneInDirection(stacksController->getActiveStack()->destShiftDir(), false);
-						if(vstd::contains(acc, myNumber))
-							giveCommand(EActionType::WALK, myNumber);
-						else if(vstd::contains(acc, shiftedDest))
-							giveCommand(EActionType::WALK, shiftedDest);
-					}
-					else
-					{
-						giveCommand(EActionType::WALK, myNumber);
-					}
-				};
-				break;
-			case PossiblePlayerBattleAction::ATTACK:
-			case PossiblePlayerBattleAction::WALK_AND_ATTACK:
-			case PossiblePlayerBattleAction::ATTACK_AND_RETURN: //TODO: allow to disable return
-				{
-					fieldController->setBattleCursor(myNumber); //handle direction of cursor and attackable tile
-					setCursor = false; //don't overwrite settings from the call above //TODO: what does it mean?
-
-					bool returnAfterAttack = currentAction == PossiblePlayerBattleAction::ATTACK_AND_RETURN;
-
-					realizeAction = [=]()
-					{
-						BattleHex attackFromHex = fieldController->fromWhichHexAttack(myNumber);
-						if(attackFromHex.isValid()) //we can be in this line when unreachable creature is L - clicked (as of revision 1308)
-						{
-							auto command = new BattleAction(BattleAction::makeMeleeAttack(stacksController->getActiveStack(), myNumber, attackFromHex, returnAfterAttack));
-							sendCommand(command, stacksController->getActiveStack());
-						}
-					};
-
-					TDmgRange damage = curInt->cb->battleEstimateDamage(stacksController->getActiveStack(), shere);
-					std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg
-					consoleMsg = (boost::format(CGI->generaltexth->allTexts[36]) % shere->getName() % estDmgText).str(); //Attack %s (%s damage)
-				}
-				break;
-			case PossiblePlayerBattleAction::SHOOT:
-			{
-				if (curInt->cb->battleHasShootingPenalty(stacksController->getActiveStack(), myNumber))
-					cursorFrame = ECursor::COMBAT_SHOOT_PENALTY;
-				else
-					cursorFrame = ECursor::COMBAT_SHOOT;
-
-				realizeAction = [=](){giveCommand(EActionType::SHOOT, myNumber);};
-				TDmgRange damage = curInt->cb->battleEstimateDamage(stacksController->getActiveStack(), shere);
-				std::string estDmgText = formatDmgRange(std::make_pair((ui32)damage.first, (ui32)damage.second)); //calculating estimated dmg
-				//printing - Shoot %s (%d shots left, %s damage)
-				consoleMsg = (boost::format(CGI->generaltexth->allTexts[296]) % shere->getName() % stacksController->getActiveStack()->shots.available() % estDmgText).str();
-			}
-				break;
-			case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-				sp = CGI->spellh->objects[creatureCasting ? stacksController->activeStackSpellToCast() : spellToCast->actionSubtype]; //necessary if creature has random Genie spell at same time
-				consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[27]) % sp->name % shere->getName()); //Cast %s on %s
-				switch (sp->id)
-				{
-					case SpellID::SACRIFICE:
-					case SpellID::TELEPORT:
-						stacksController->setSelectedStack(shere); //remember first target
-						secondaryTarget = true;
-						break;
-				}
-				isCastingPossible = true;
-				break;
-			case PossiblePlayerBattleAction::ANY_LOCATION:
-				sp = CGI->spellh->objects[creatureCasting ? stacksController->activeStackSpellToCast() : spellToCast->actionSubtype]; //necessary if creature has random Genie spell at same time
-				consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s
-				isCastingPossible = true;
-				break;
-			case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL: //we assume that teleport / sacrifice will never be available as random spell
-				sp = nullptr;
-				consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[301]) % shere->getName()); //Cast a spell on %
-				creatureCasting = true;
-				isCastingPossible = true;
-				break;
-			case PossiblePlayerBattleAction::TELEPORT:
-				consoleMsg = CGI->generaltexth->allTexts[25]; //Teleport Here
-				cursorFrame = ECursor::COMBAT_TELEPORT;
-				isCastingPossible = true;
-				break;
-			case PossiblePlayerBattleAction::OBSTACLE:
-				consoleMsg = CGI->generaltexth->allTexts[550];
-				//TODO: remove obstacle cursor
-				isCastingPossible = true;
-				break;
-			case PossiblePlayerBattleAction::SACRIFICE:
-				consoleMsg = (boost::format(CGI->generaltexth->allTexts[549]) % shere->getName()).str(); //sacrifice the %s
-				cursorFrame = ECursor::COMBAT_SACRIFICE;
-				isCastingPossible = true;
-				break;
-			case PossiblePlayerBattleAction::FREE_LOCATION:
-				consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s
-				isCastingPossible = true;
-				break;
-			case PossiblePlayerBattleAction::HEAL:
-				cursorFrame = ECursor::COMBAT_HEAL;
-				consoleMsg = (boost::format(CGI->generaltexth->allTexts[419]) % shere->getName()).str(); //Apply first aid to the %s
-				realizeAction = [=](){ giveCommand(EActionType::STACK_HEAL, myNumber); }; //command healing
-				break;
-			case PossiblePlayerBattleAction::RISE_DEMONS:
-				cursorType = ECursor::SPELLBOOK;
-				realizeAction = [=]()
-				{
-					giveCommand(EActionType::DAEMON_SUMMONING, myNumber);
-				};
-				break;
-			case PossiblePlayerBattleAction::CATAPULT:
-				cursorFrame = ECursor::COMBAT_SHOOT_CATAPULT;
-				realizeAction = [=](){ giveCommand(EActionType::CATAPULT, myNumber); };
-				break;
-			case PossiblePlayerBattleAction::CREATURE_INFO:
-			{
-				cursorFrame = ECursor::COMBAT_QUERY;
-				consoleMsg = (boost::format(CGI->generaltexth->allTexts[297]) % shere->getName()).str();
-				realizeAction = [=](){ GH.pushIntT<CStackWindow>(shere, false); };
-				break;
-			}
-		}
-	}
-	else //no possible valid action, display message
-	{
-		switch (illegalAction)
-		{
-			case PossiblePlayerBattleAction::AIMED_SPELL_CREATURE:
-			case PossiblePlayerBattleAction::RANDOM_GENIE_SPELL:
-				cursorFrame = ECursor::COMBAT_BLOCKED;
-				consoleMsg = CGI->generaltexth->allTexts[23];
-				break;
-			case PossiblePlayerBattleAction::TELEPORT:
-				cursorFrame = ECursor::COMBAT_BLOCKED;
-				consoleMsg = CGI->generaltexth->allTexts[24]; //Invalid Teleport Destination
-				break;
-			case PossiblePlayerBattleAction::SACRIFICE:
-				consoleMsg = CGI->generaltexth->allTexts[543]; //choose army to sacrifice
-				break;
-			case PossiblePlayerBattleAction::FREE_LOCATION:
-				cursorFrame = ECursor::COMBAT_BLOCKED;
-				consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[181]) % sp->name); //No room to place %s here
-				break;
-			default:
-				if (myNumber == -1)
-					CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER); //set neutral cursor over menu etc.
-				else
-					cursorFrame = ECursor::COMBAT_BLOCKED;
-				break;
-		}
-	}
-
-	if (isCastingPossible) //common part
-	{
-		switch (currentAction) //don't use that with teleport / sacrifice
-		{
-			case PossiblePlayerBattleAction::TELEPORT: //FIXME: more generic solution?
-			case PossiblePlayerBattleAction::SACRIFICE:
-				break;
-			default:
-				cursorType = ECursor::SPELLBOOK;
-				cursorFrame = 0;
-				if (consoleMsg.empty() && sp)
-					consoleMsg = boost::str(boost::format(CGI->generaltexth->allTexts[26]) % sp->name); //Cast %s
-				break;
-		}
-
-		realizeAction = [=]()
-		{
-			if(secondaryTarget) //select that target now
-			{
-
-				possibleActions.clear();
-				switch (sp->id.toEnum())
-				{
-					case SpellID::TELEPORT: //don't cast spell yet, only select target
-						spellToCast->aimToUnit(shere);
-						possibleActions.push_back(PossiblePlayerBattleAction::TELEPORT);
-						break;
-					case SpellID::SACRIFICE:
-						spellToCast->aimToHex(myNumber);
-						possibleActions.push_back(PossiblePlayerBattleAction::SACRIFICE);
-						break;
-				}
-			}
-			else
-			{
-				if (creatureCasting)
-				{
-					if (sp)
-					{
-						giveCommand(EActionType::MONSTER_SPELL, myNumber, stacksController->activeStackSpellToCast());
-					}
-					else //unknown random spell
-					{
-						giveCommand(EActionType::MONSTER_SPELL, myNumber);
-					}
-				}
-				else
-				{
-					assert(sp);
-					switch (sp->id.toEnum())
-					{
-					case SpellID::SACRIFICE:
-						spellToCast->aimToUnit(shere);//victim
-						break;
-					default:
-						spellToCast->aimToHex(myNumber);
-						break;
-					}
-					curInt->cb->battleMakeAction(spellToCast.get());
-					endCastingSpell();
-				}
-				stacksController->setSelectedStack(nullptr);
-			}
-		};
-	}
-
-	{
-		if (eventType == MOVE)
-		{
-			if (setCursor)
-				CCS->curh->changeGraphic(cursorType, cursorFrame);
-			controlPanel->console->write(consoleMsg);
-		}
-		if (eventType == LCLICK && realizeAction)
-		{
-			//opening creature window shouldn't affect myTurn...
-			if ((currentAction != PossiblePlayerBattleAction::CREATURE_INFO) && !secondaryTarget)
-			{
-				myTurn = false; //tends to crash with empty calls
-			}
-			realizeAction();
-			if (!secondaryTarget) //do not replace teleport or sacrifice cursor
-				CCS->curh->changeGraphic(ECursor::COMBAT, ECursor::COMBAT_POINTER);
-			controlPanel->console->clear();
-		}
-	}
-}
-
-bool CBattleInterface::isCastingPossibleHere(const CStack *sactive, const CStack *shere, BattleHex myNumber)
-{
-	creatureCasting = stacksController->activeStackSpellcaster() && !spellDestSelectMode; //TODO: allow creatures to cast aimed spells
-
-	bool isCastingPossible = true;
-
-	int spellID = -1;
-	if (creatureCasting)
-	{
-		if (stacksController->activeStackSpellToCast() != SpellID::NONE && (shere != sactive)) //can't cast on itself
-			spellID = stacksController->activeStackSpellToCast(); //TODO: merge with SpellTocast?
-	}
-	else //hero casting
-	{
-		spellID = spellToCast->actionSubtype;
-	}
-
-
-	sp = nullptr;
-	if (spellID >= 0)
-		sp = CGI->spellh->objects[spellID];
-
-	if (sp)
-	{
-		const spells::Caster *caster = creatureCasting ? static_cast<const spells::Caster *>(sactive) : static_cast<const spells::Caster *>(curInt->cb->battleGetMyHero());
-		if (caster == nullptr)
-		{
-			isCastingPossible = false;//just in case
-		}
-		else
-		{
-			const spells::Mode mode = creatureCasting ? spells::Mode::CREATURE_ACTIVE : spells::Mode::HERO;
-
-			spells::Target target;
-			target.emplace_back(myNumber);
-
-			spells::BattleCast cast(curInt->cb.get(), caster, mode, sp);
-
-			auto m = sp->battleMechanics(&cast);
-			spells::detail::ProblemImpl problem; //todo: display problem in status bar
-
-			isCastingPossible = m->canBeCastAt(target, problem);
-		}
-	}
-	else
-		isCastingPossible = false;
-	if (!myNumber.isAvailable() && !shere) //empty tile outside battlefield (or in the unavailable border column)
-			isCastingPossible = false;
-
-	return isCastingPossible;
-}
-
 void CBattleInterface::obstaclePlaced(const CObstacleInstance & oi)
 {
 	obstacleController->obstaclePlaced(oi);
@@ -1784,8 +1080,6 @@ void CBattleInterface::showBattlefieldObjects(SDL_Surface *to)
 	showHexEntry(objects.afterAll);
 }
 
-
-
 void CBattleInterface::showBattleEffects(SDL_Surface *to, const std::vector<const BattleEffect *> &battleEffects)
 {
 	for (auto & elem : battleEffects)
@@ -1852,3 +1146,8 @@ BattleObjectsByHex CBattleInterface::sortObjectsByHex()
 
 	return sorted;
 }
+
+void CBattleInterface::castThisSpell(SpellID spellID)
+{
+	actionsController->castThisSpell(spellID);
+}

+ 3 - 27
client/battle/CBattleInterface.h

@@ -64,6 +64,7 @@ class CBattleObstacleController;
 class CBattleFieldController;
 class CBattleControlPanel;
 class CBattleStacksController;
+class CBattleActionsController;
 
 /// Small struct which contains information about the id of the attacked stack, the damage dealt,...
 struct StackAttackedInfo
@@ -109,12 +110,6 @@ struct BattleObjectsByHex
 	std::array<HexData, GameConstants::BFIELD_SIZE> hex;
 };
 
-enum class MouseHoveredHexContext
-{
-	UNOCCUPIED_HEX,
-	OCCUPIED_HEX
-};
-
 /// Big class which handles the overall battle interface actions and it is also responsible for
 /// drawing everything correctly.
 class CBattleInterface : public WindowBase
@@ -135,16 +130,6 @@ private:
 	ui8 animCount;
 
 	bool tacticsMode;
-	bool creatureCasting; //if true, stack currently aims to cats a spell
-	bool spellDestSelectMode; //if true, player is choosing destination for his spell - only for GUI / console
-	std::shared_ptr<BattleAction> spellToCast; //spell for which player is choosing destination
-	const CSpell *sp; //spell pointer for convenience
-	std::vector<PossiblePlayerBattleAction> possibleActions; //all actions possible to call at the moment by player
-	std::vector<PossiblePlayerBattleAction> localActions; //actions possible to take on hovered hex
-	std::vector<PossiblePlayerBattleAction> illegalActions; //these actions display message in case of illegal target
-	PossiblePlayerBattleAction currentAction; //action that will be performed on l-click
-	PossiblePlayerBattleAction selectedAction; //last action chosen (and saved) by player
-	PossiblePlayerBattleAction illegalAction; //most likely action that can't be performed here
 	bool battleActionsStarted; //used for delaying battle actions until intro sound stops
 	int battleIntroSoundChannel; //required as variable for disabling it via ESC key
 
@@ -154,12 +139,6 @@ private:
 	void activateStack(); //sets activeStack to stackToActivate etc. //FIXME: No, it's not clear at all
 	void requestAutofightingAIToTakeAction();
 
-	std::vector<PossiblePlayerBattleAction> getPossibleActionsForStack (const CStack *stack); //called when stack gets its turn
-	void endCastingSpell(); //ends casting spell (eg. when spell has been cast or canceled)
-	void reorderPossibleActionsPriority(const CStack * stack, MouseHoveredHexContext context);
-
-	//force active stack to cast a spell if possible
-	void enterCreatureCastingMode();
 
 	void giveCommand(EActionType action, BattleHex tile = BattleHex(), si32 additional = -1);
 	void sendCommand(BattleAction *& command, const CStack * actor = nullptr);
@@ -181,12 +160,12 @@ public:
 	std::unique_ptr<CBattleObstacleController> obstacleController;
 	std::unique_ptr<CBattleFieldController> fieldController;
 	std::unique_ptr<CBattleStacksController> stacksController;
+	std::unique_ptr<CBattleActionsController> actionsController;
 
 	static CondSh<bool> animsAreDisplayed; //for waiting with the end of battle for end of anims
 	static CondSh<BattleAction *> givenCommand; //data != nullptr if we have i.e. moved current unit
 
 	bool myTurn; //if true, interface is active (commands can be ordered)
-
 	bool moveStarted; //if true, the creature that is already moving is going to make its first step
 	int moveSoundHander; // sound handler used when moving a unit
 
@@ -251,10 +230,6 @@ public:
 	void hideQueue();
 	void showQueue();
 
-	void handleHex(BattleHex myNumber, int eventType);
-	bool isCastingPossibleHere (const CStack *sactive, const CStack *shere, BattleHex myNumber);
-	bool canStackMoveHere (const CStack *sactive, BattleHex MyNumber); //TODO: move to BattleState / callback
-
 	void obstaclePlaced(const CObstacleInstance & oi);
 
 	void gateStateChanged(const EGateState state);
@@ -286,4 +261,5 @@ public:
 	friend class CBattleFieldController;
 	friend class CBattleControlPanel;
 	friend class CBattleStacksController;
+	friend class CBattleActionsController;
 };

+ 2 - 1
client/battle/CBattleInterfaceClasses.cpp

@@ -11,6 +11,7 @@
 #include "CBattleInterfaceClasses.h"
 
 #include "CBattleInterface.h"
+#include "CBattleActionsController.h"
 #include "CBattleSiegeController.h"
 #include "CBattleFieldController.h"
 #include "CBattleStacksController.h"
@@ -191,7 +192,7 @@ void CBattleHero::hover(bool on)
 
 void CBattleHero::clickLeft(tribool down, bool previousState)
 {
-	if(myOwner->spellDestSelectMode) //we are casting a spell
+	if(myOwner->actionsController->spellcastingModeActive()) //we are casting a spell
 		return;
 
 	if(boost::logic::indeterminate(down))